[Docs] (MG_Test, P12): the dropped-records repair is gone, not merely narrowed - the first-use work removed the last row that read an applier refusal, and the case that pins the debt

handoff-2 section 3.3 asks for a gate on the one scope that empties the applier's
object records while the frontend objects live on (MGPipeApplierReleaseObjectRecords,
"latent" because its only caller in the tree is MG_Test). The gate was expected green
for a CHANGED descriptor: the respecify is refused, RespecifyOnce turns the refusal
into a create plus one retry. It is not green.

MEASURED:
    dropped-records probe: creates=0 applier_records=0 published=1

Nothing is emitted at all. The refusal can only reach the emitter through an answer
the row READS, and P12 made both rows answer themselves for a "confirmed" object:
the respecify is fire-and-forget (MGPipeResourceRespecifyWantsItsReply) and the
params row writes a PROVISIONAL kStatusOk (Wire_SetTextureParams). "Confirmed" is
MGPipeHandleIsPublished, which answers "did a create GO OUT" - and it stays true
after the records are dropped, which is the scope's whole definition. The only row
left that reads the real answer is create, and create is only sent when the latch is
clear - the one thing that stops being true.

So section 3.1's "clearing the latch is not enough" is the smaller half: nothing
clears the latch either, and nothing can until the applier can say "I hold nothing for
this handle" - section 3.2's third option, the reverse channel, i.e. the protocol
surface the handoff hoped to avoid paying for. That hope was priced on the trigger
being unreachable; it is still unreachable (no production caller), so the call stands,
but the cost of a production caller is now known to be the whole thing.

THE CASE IS WRITTEN AS A DEBT RATHER THAN A SPEC, and says so in its name and its
comment: it pins today's three numbers, so whoever lands the signal sees THIS case go
red and knows the repair started working. Removing the heal from RespecifyOnce would
not make it red - nothing reaches the heal - which is exactly why the assertion is on
the applier's table and the latch rather than on a count (handoff-2 section 3.3: "a
count cannot show this class of error").

Also rewritten: notes/p12/UNPUBLISH-GUARD-SIGNALS.md sections 4 and 6 now carry this
as 4.2 (sections 2 and 3 stand: neither caps generation nor FreshlyPrimed is a signal).
172/172.
This commit is contained in:
BZLZHH
2026-09-26 00:50:00 +08:00
parent d2efd1d79d
commit d011395939
2 changed files with 137 additions and 17 deletions
@@ -1406,4 +1406,96 @@ TEST(RemoteClientControls, ARefusedCreateUnlatchesItsObjectAndItsNextCreateBlock
});
ExpectChildSuccess(child);
}
// =====================================================================================
// P12 (handoff-2 §3.3): WHAT THE DROPPED RECORDS ARE REPAIRED BY - AND WHAT THEY ARE NOT
// =====================================================================================
//
// MGPipeApplierReleaseObjectRecords is the served context's teardown and the ONLY scope that
// empties the applier's object records while the frontend objects live on in the share group.
// handoff-2 §3 calls the guard for it "latent" because no production code calls it; what has to
// hold when one does is that the client REPAIRS it, and the repair on paper is the one that
// already existed: the applier REFUSES a record for a handle it holds nothing for, and
// RespecifyOnce turns that refusal into a create plus one retry.
//
// THIS CASE MEASURES WHETHER THAT STILL HAPPENS, AND THE ANSWER IS NO - the P12 first-use work
// took the signal away. The refusal can only reach the emitter through an answer the row READS,
// and both rows that used to read it now answer themselves: the respecify is fire-and-forget for
// a "confirmed" object (MGPipeResourceRespecifyWantsItsReply) and the params row posts a
// PROVISIONAL kStatusOk for one (Wire_SetTextureParams), while "confirmed" is
// MGPipeHandleIsPublished - WHICH STAYS TRUE AFTER THE RECORDS ARE DROPPED, because it answers
// "did a create GO OUT", not "does the applier still hold it". So the applier is left holding
// nothing, the client is left believing the object is published, and every later fire-and-forget
// row for it is answered provisionally. handoff-2 §3.1's "clearing the latch alone is not enough"
// is the smaller half of this; the larger half is that nothing clears the latch, and nothing can
// until the applier can say so (its own §3.2 option 3, the reverse channel).
//
// THE ASSERTIONS BELOW ARE THEREFORE A DEBT, NOT A SPEC, and they are written to be LOUD about it:
// they pin today's numbers so that whoever lands the signal sees THIS case go red and knows the
// repair started working. notes/p12/UNPUBLISH-GUARD-SIGNALS.md §4-§6 has the rest.
TEST(RemoteClientControls, DroppedApplierRecordsAreNotRepairedTodayAndThisCaseIsTheDebt) {
const auto child = RunInChild([] {
::setenv("MOBILEGL_PIPE_STATS", "1", 1);
CapsPeer backend;
StartControlSession(&backend);
MG_Config::Features.PipePush |= kMGPipeSubsystemResources |
kMGPipeSubsystemTextureResources | kMGPipeSubsystemSamplers;
MG_State::pGLContext = MakeUnique<MG_State::GLState::GLContext>();
const Uint32 createOp = static_cast<Uint32>(MGPWireOp::ResourceCreate);
Uint8 pixels[16 * 16 * 4];
std::memset(pixels, 0x21, sizeof(pixels));
// The applier's own table, read where it is legal to read it: on the apply thread, through
// the same probe channel the rest of this file uses.
const auto applierTextureRecords = [] {
Uint64 count = 0;
(void)Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void* self) {
*static_cast<Uint64*>(self) =
static_cast<Uint64>(MG_Pipe::MGPipeApplier().TextureResources.size());
return MOBILEGL_OK;
}, &count);
return count;
};
GLuint texture = 0;
MG_Impl::GLImpl::GenTextures(1, &texture);
MG_Impl::GLImpl::BindTexture(0x0DE1, texture);
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 8, 8, 0, 0x1908, 0x1401, pixels);
MGP_FILL(DrawArrays);
const auto object = MG_State::pGLContext->GetTextureObject(texture);
if (object == nullptr) ::_exit(183);
const MG_Pipe::MGPipeHandle handle = MG_Pipe::MGPipeTextureEmitterInstance().FindTexture(*object);
if (MG_Pipe::MGPipeHandleIsNull(handle)) ::_exit(184);
// ---- IT IS REALLY THERE, AND THE LATCH AGREES ----
if (applierTextureRecords() == 0) ::_exit(185);
if (!MG_Pipe::MGPipeHandleIsPublished(MG_Pipe::MGPipeKind::Texture, handle)) ::_exit(186);
// ---- THE DROP, ON THE APPLIER'S OWN THREAD ----
if (Srv::ServerLoopInstance().RunProbeOnApplyThreadForTesting([](void*) {
MG_Pipe::MGPipeApplierReleaseObjectRecords();
return MOBILEGL_OK;
}, nullptr) != MOBILEGL_OK) ::_exit(187);
if (applierTextureRecords() != 0) ::_exit(188);
// THE HAZARD IN ONE LINE: the fact every converted row acts on is still true, and it is
// answering a question about the PAST ("a create went out") to a reader that needs the
// present ("the applier holds it").
if (!MG_Pipe::MGPipeHandleIsPublished(MG_Pipe::MGPipeKind::Texture, handle)) ::_exit(189);
// ---- THE RESHAPE THAT WOULD HAVE BEEN REFUSED ----
const Uint64 createRecordsBefore = Transport::LinkMetricsRecordsFor(createOp);
MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 16, 16, 0, 0x1908, 0x1401, pixels);
MGP_FILL(DrawArrays);
const Uint64 createRecords = Transport::LinkMetricsRecordsFor(createOp) - createRecordsBefore;
const Uint64 held = applierTextureRecords();
const Bool published = MG_Pipe::MGPipeHandleIsPublished(MG_Pipe::MGPipeKind::Texture, handle);
std::printf("dropped-records probe: creates=%llu applier_records=%llu published=%d\n",
static_cast<unsigned long long>(createRecords),
static_cast<unsigned long long>(held), static_cast<int>(published));
std::fflush(nullptr);
// TODAY'S NUMBERS, AND THE DEBT: no create was emitted (nothing was refused, because the
// row did not read its answer), the applier holds nothing, and the client still says the
// object is published. INVERT THESE THREE WHEN THE SIGNAL LANDS.
if (createRecords != 0) ::_exit(190);
if (held != 0) ::_exit(191);
if (!published) ::_exit(192);
ClientSessionInstance().Stop();
});
ExpectChildSuccess(child);
}
#endif
@@ -11,9 +11,13 @@
- **§3.2 候选 2(FreshlyPrimed)是同一个理由,而且它就是空的**:`TextureEmit.h:1257` 的
`void Reset() {}` 函数体为空,注释写明「Only LATCHES reset here - the applier's object records survive a
make-current」。
- **真正的缺口只有一种**:描述符**没变**的 respecify 会在发射端被去重吞掉(`TextureEmit.h:838` 的
`unchanged` 不读闩),连「拒绝」这个唯一的客户端可见信号都产生不了。描述符变了的那一半今天**已经能自愈**。
- **缺口的真实大小比 handoff §3.1 说的更大**:不是「清闩不够」,而是**已经没有东西会去清闩**。
P12 的首用化把两个曾经读答案的行都变成了「已确认对象不回读」(respecify 走
`MGPipeResourceRespecifyWantsItsReply` 的 fire-and-forget、params 走 `Wire_SetTextureParams` 的临时
`kStatusOk`),而「已确认」= `MGPipeHandleIsPublished`——**它回答的是「create 发出去过」,在记录被丢掉之后
仍然为真**。于是 applier 的拒绝再也到不了 emitter,`RespecifyOnce` 那条自愈链**今天对任何描述符都不成立**。
- **今天不可达**:`MGPipeApplierReleaseObjectRecords` 在生产代码里没有调用者(只有 MG_Test,实测 grep 见 §5)。
所以这是债而不是现网缺陷——但它比 handoff 估计的宽:**修它需要协议面**,不是清闩、也不是挂代次。
## 1. 失效时要清的三样,逐条落到符号
@@ -50,7 +54,9 @@ void Reset() {} // TextureEmit.h:1257
「a texture handle and the applier record it names are SHARE-GROUP OBJECT STATE, so nothing here is
per-context and **no re-publication path exists or may exist**」。所以候选 2 不是「一行」,是**推翻一条设计**。
## 4. 真正的缺口:去重在发射端,比闩更早
## 4. 缺口:从「去重吞掉」到「没有任何行会读到拒绝」
### 4.1 第一步:去重在发射端,比闩更早
`TextureEmit.h:838`:
@@ -58,15 +64,32 @@ per-context and **no re-publication path exists or may exist**」。所以候选
const Bool unchanged = entry.HasLastDesc && std::memcmp(&entry.LastDesc, &desc, sizeof(desc)) == 0;
~~~
**它不读闩。** 于是 `MGPipeApplierReleaseObjectRecords` 之后两条路不一样:
**它不读闩。** 所以描述符**没变**的 respecify 会在发射端直接返回(`if (unchanged) return;`),
一条记录都不过线,服务端也就没有机会拒绝——只清闩治不了这一半,要清的是镜像
(`entry.HasLastDesc` / `entry.HasParamsLatch`)。这一条是 handoff §3.1 已经点到的,这里是它的符号。
| 情形 | 今天的走向 | 结果 |
### 4.2 第二步(handoff 没写,本轮实测):描述符变了也不自愈了
handoff §3.3 假定「变了描述符」那一半今天能自愈:respecify 被拒 → `RespecifyOnce` 补 create 再重试。
**P12 的首用化把这半也拿掉了**,因为那条路径要读答案,而两个行现在都自己回答自己:
| 行 | P12 之后 | 拒绝还能不能到 emitter |
|---|---|---|
| 描述符**变了** | 不去重 → 闩仍为真所以不发 create → `RespecifyOnce` 发 respecify → applier 拒绝 → **补一个 create 再重试** → 接受 | **已自愈**(handoff §3.3 要的门,今天就是绿的) |
| 描述符**没变** | `if (unchanged) return;` | **一条记录都不过线**,服务端没有机会拒绝,自愈链断在这里 |
| `resource_respecify` | 对象已确认时 `MGPipeResourceRespecifyWantsItsReply` 返回 false(`PipeRoute.h:297-302`),`Wire_Escape_ResourceRespecify` 发 provisional accept | **不能**(调用方读到 true,`RespecifyOnce` 不进 heal 分支) |
| `set_texture_params` | `Wire_SetTextureParams` 对 confirmed 对象直接把 `status` 写成 `kStatusOk`(`WireTables.cpp`),不读答案 | **不能** |
所以「①」要清的其实是第 2 行,而它要的**不是清闩,是清镜像**(`HasLastDesc`/`HasParamsLatch`)——
只清闩的话那条「相同 desc」照样不发(handoff §3.1 已经点到,这里是它的符号)。
而「已确认」是 `MGPipeHandleIsPublished`,它的语义是**「这个 handle 的 create 发出去过」**,
在 `MGPipeApplierReleaseObjectRecords` 之后**依然为真**(`TextureEmit.h:1381-1389` 的注释自己写着这件事)。
于是唯一还会读真答案的行只剩 create,而 create 只在闩为假时才发——**闩为假这件事,正好是被丢掉记录之后不再发生的事**。
实测(`RemoteClientControls.DroppedApplierRecordsAreNotRepairedTodayAndThisCaseIsTheDebt`):
丢掉之后 reshape 同一个对象,`creates=0 applier_records=0 published=1`——没有 create、applier 手里没有记录、
而客户端仍然认为它是已发布的。
### 4.3 所以「①」到底是什么
它不是一个「清三样」的卫生动作,而是**一条让 applier 能说出「我手里没有这个 handle」的通道**——
也就是 handoff §3.2 的第 3 条(反向通道第 10 个回调),成本是协议面。前两条便宜候选见 §2/§3。
## 5. 可达性(本轮复核)
@@ -79,16 +102,21 @@ PipeApply.cpp:1593 void MGPipeApplierReleaseObjectRecords() { <- 定义
与 handoff §3.4 一致:**生产调用者为零**,唯一调用者是 MG_Test(`PipeWireCodecTest`/`StagedTextureStoreTest`
各一处、若干 `*EmitTest` 的 SetUp/TearDown)。闩按 `{kind, slot, gen}` 键、对象销毁会清闩,槽位复用安全。
## 6. 建议(本轮不做,理由在下面)
## 6. 建议(本轮做了第 3 条,其余不做,理由在下面)
1. **不加反向回调**:那是协议面(两端重出 APK),而触发者今天不存在。handoff §7 的判词是「今天是潜伏,
别为它付协议面的钱」。
2. **不挂代次**:理由见 §2/§3——它会在每次 make-current 上 over-fire,为一个不可达路径付真实成本。
3. **该做的是把缺口写成门**:用 `MGPipeApplierReleaseObjectRecords` 造「记录被丢掉」,
断言**描述符变了那一半必须自愈**(今天绿,红的方式是把 `RespecifyOnce` 的补 create 去掉),
并把**描述符没变那一半**作为已知缺口记账——它是这件事真正欠的债。
4. 如果哪天真出现生产调用者,正确形状是给 entry 记一个「本代次内确认过」的戳(caps 代次或会话代次),
代次一动就重发一次:它是 over-fire 方向,不用动 wire。
别为它付协议面的钱」。**注意这一条的价钱现在知道了**:§4.2 说明它不是一个可选的美化,
而是唯一的修法——所以「不值得付」的前提是「调用者确实不会出现」,一旦有人给
`MGPipeApplierReleaseObjectRecords` 加生产调用者,就必须付。
2. **不挂代次**:理由见 §2/§3——它会在每次 make-current 上 over-fire,为一个不可达路径付真实成本;
而且按 §4.2 它也**治不好**(闩清了、行还是不读答案)。
3. **已做**:`RemoteClientControls.DroppedApplierRecordsAreNotRepairedTodayAndThisCaseIsTheDebt`
用 `MGPipeApplierReleaseObjectRecords` 造「记录被丢掉」,读的是**applier 手里有没有记录**和**闩的状态**
(不是计数——handoff §3.3 那句「只看计数看不出这类错」),并把今天的三个数(`creates=0`、
`applier_records=0`、`published=1`)**当成债务 pin 住**:谁把信号接上,这条用例就红,
红得正好是「修复开始生效」的地方。
4. 真要做的话,形状是**给 entry 记一个「本代次内确认过」的戳**(caps 代次或会话代次),代次一动就重发一次
——它治 §4.1(去重),**治不了 §4.2**,因为 §4.2 缺的是「applier 说它没有」这件事本身。
## 7. 复算