diff --git a/MobileGL/MG_Test/Wire/RemoteClientControls.inc b/MobileGL/MG_Test/Wire/RemoteClientControls.inc index 5595755f..4906bbb7 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientControls.inc +++ b/MobileGL/MG_Test/Wire/RemoteClientControls.inc @@ -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(); + const Uint32 createOp = static_cast(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(self) = + static_cast(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(createRecords), + static_cast(held), static_cast(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 diff --git a/docs/Disaggregated/notes/p12/UNPUBLISH-GUARD-SIGNALS.md b/docs/Disaggregated/notes/p12/UNPUBLISH-GUARD-SIGNALS.md index 4aae2ccf..258fcc77 100644 --- a/docs/Disaggregated/notes/p12/UNPUBLISH-GUARD-SIGNALS.md +++ b/docs/Disaggregated/notes/p12/UNPUBLISH-GUARD-SIGNALS.md @@ -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. 复算