diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index bd806aa8..272d30ec 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -470,11 +470,34 @@ namespace MobileGL::MG_Remote::Client { } else { MG_Pipe::MGPipeClearRespecifiedLevel(record); } - Int32 status = 0; - const Uint64 seq = - session.EmitAndWait(MGPWireOp::ResourceRespecify, &record, sizeof(record), nullptr, - 0, nullptr, 0, &status); - (void)seq; + // P12: THE BUFFER HALF'S ANSWER IS READ BY NOBODY. Both call sites discard this + // record's Bool outright (PipeFill.cpp:1105 and :1142: `MGPipeRouteResourceRespecify( + // desc, nullptr);`), so for a buffer the wait buys a value that is thrown away - pure + // cost, and on the device run this row was the second largest source of round trips in + // the frame. It goes fire-and-forget, with the same provisional-accept shape + // resource_subdata has had since item B. + // + // THE TEXTURE HALF KEEPS ITS ANSWER, and that is deliberate rather than left over: + // there the Bool is the self-heal trigger AND the gate on the acceptance mirror, and + // advancing that mirror on an answer nobody waited for is exactly ID-18 M3 + // (TextureEmit.h:885-888: "a refused respecify must leave LastDesc naming the + // descriptor that actually landed, or the next identical call is suppressed against a + // record that was never stored"). A texture respecify still waits until that mirror + // can be advanced on something better than a provisional accept. + const Bool bufferHalf = + static_cast(record.Target) == + MG_Pipe::MGPipeResourceTarget::Buffer; + Int32 status = Wire::ReplySink::kStatusError; + if (bufferHalf) { + const Uint64 seq = session.EmitAndWaitTails( + MGPWireOp::ResourceRespecify, &record, sizeof(record), nullptr, 0, nullptr, 0, + &status, nullptr, /*wantReply=*/false); + status = seq == Wire::kInvalidSeq ? Wire::ReplySink::kStatusDeclined + : Wire::ReplySink::kStatusOk; + } else { + (void)session.EmitAndWait(MGPWireOp::ResourceRespecify, &record, sizeof(record), + nullptr, 0, nullptr, 0, &status); + } ++g_emitted; // ERROR IS NOT A DECLINE (M4 / codex 5 / R-5). status is 0 OK / 1 DECLINED / 2 ERROR; // an escape may not fold 2 into `false`, which is what a bare `return status == 0` diff --git a/MobileGL/MG_Test/Wire/RemoteClientControls.inc b/MobileGL/MG_Test/Wire/RemoteClientControls.inc index e552e1cd..a8f52f5e 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientControls.inc +++ b/MobileGL/MG_Test/Wire/RemoteClientControls.inc @@ -296,11 +296,18 @@ TEST(RemoteClientControls, LongFenceWaitBudgetStillWakesOnShutdown) { } TEST(RemoteClientControls, ResourceRespecifyErrorIsNotADecline) { + // THE TEXTURE HALF, WHICH IS THE HALF THAT STILL READS ITS ANSWER (P12). This case used a + // zeroed descriptor, which is a BUFFER target - and the buffer half is now fire-and-forget + // because both of its call sites discard the Bool (PipeFill.cpp:1105, :1142). A row that reads + // no answer cannot fold ERROR into a decision, so the rule this case exists for moved to the + // half that does read one: the transport fault is caught here, at the next row that waits, + // instead of at a record whose verdict nobody ever looked at. const auto child = RunInChild([] { StartControlSession(); ErrorPeer peer; peer.Install(); MGPResourceDesc desc{}; + desc.Target = static_cast(MG_Pipe::MGPipeResourceTarget::Tex2D); (void)MGPipeRouteResourceRespecify(desc, nullptr); }); ExpectNamedAbort(child, "Fatal{ReplyError, \"resource_respecify\"}"); @@ -1094,4 +1101,54 @@ TEST(RemoteClientControls, AParameterChangeForAConfirmedObjectTakesNoReply) { }); ExpectChildSuccess(child); } +// ===================================================================================== +// P12: a BUFFER respecify takes no reply; a TEXTURE one still does (deliberately) +// ===================================================================================== +// +// The two halves of resource_respecify are not the same row. The buffer half's Bool is discarded +// at BOTH call sites (PipeFill.cpp:1105, :1142), so its wait bought a value that was thrown away - +// fire-and-forget costs nothing and saves a round trip per glBufferData, which is the shape a +// chunk uploader produces thousands of times per frame. The texture half's Bool is load-bearing: +// it triggers the self-heal AND gates the acceptance mirror, and advancing that mirror on a +// provisional accept is ID-18 M3 (TextureEmit.h:885-888). So this case asserts BOTH directions - +// the asymmetry is the design, not an oversight. +// +// THE RED, RUN ONCE: take the bufferHalf branch out of Wire_Escape_ResourceRespecify and exit 155 +// fires (the buffer respecify starts waiting again). +TEST(RemoteClientControls, ABufferRespecifyTakesNoReplyWhileATextureOneStillDoes) { + 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 respecifyOp = static_cast(MGPWireOp::ResourceRespecify); + Uint8 seed[64]; + std::memset(seed, 0x5a, sizeof(seed)); + // ---- the BUFFER half: records out, replies not taken ---- + GLuint buffer = 0; + MG_Impl::GLImpl::GenBuffers(1, &buffer); + MG_Impl::GLImpl::BindBuffer(0x8892, buffer); + MG_Impl::GLImpl::BufferData(0x8892, sizeof(seed), seed, 0x88E4); + MGP_FILL(DrawArrays); + const Uint64 recordsBefore = Transport::LinkMetricsRecordsFor(respecifyOp); + const Uint64 waitsBefore = Transport::LinkMetricsReplyWaitsFor(respecifyOp); + MG_Impl::GLImpl::BufferData(0x8892, sizeof(seed) * 2, seed, 0x88E4); + MGP_FILL(DrawArrays); + if (Transport::LinkMetricsRecordsFor(respecifyOp) <= recordsBefore) ::_exit(154); + if (Transport::LinkMetricsReplyWaitsFor(respecifyOp) != waitsBefore) ::_exit(155); + // ---- the TEXTURE half: the deliberate exception ---- + Uint8 pixels[8 * 8 * 4]; + std::memset(pixels, 0x21, sizeof(pixels)); + 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); + if (Transport::LinkMetricsReplyWaitsFor(respecifyOp) == waitsBefore) ::_exit(156); + ClientSessionInstance().Stop(); + }); + ExpectChildSuccess(child); +} #endif diff --git a/docs/Disaggregated/notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md b/docs/Disaggregated/notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md index 50d86bda..a1484f58 100644 --- a/docs/Disaggregated/notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md +++ b/docs/Disaggregated/notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md @@ -218,7 +218,7 @@ create 被拒 → 闩不置位 → 下一条参数记录照样阻塞取真答案 **下一个 20,633**:`ResourceRespecify` 仍逐条等(在 frame 25 里占 20,633/25,176 = 82%)。它与 params 同形(答案只用于 `NoteRespecified` 与自愈), "对象已确认"这个前提同样成立——按同一条规则处理,估算落在 **~4.5k waits ≈ 24 s** 量级(create 那 4,536 是确认本身,应当保留)。 - +### 2.12 `ResourceRespecify` 的两半根本不同:buffer 是白送的,texture 才是那 20,611按"已确认对象不再等"处理这一行时,代码里两半的差别是决定性的:- **buffer 半**:`PipeFill.cpp:1105` 与 `:1142` **两处都把返回值丢掉**(`MGPipeRouteResourceRespecify(desc, nullptr);`), 所以那条等待买到的值当场被扔——改 fire-and-forget 是**纯收益、语义零变化**。- **texture 半**:返回值既是自愈触发器,又是**验收镜像 `LastDesc` 的闸**, `TextureEmit.h:885-888` 明写不许在未确认的答案上前进(ID-18 M3:否则下一次相同调用会被去重掉,而服务端从没存过)。于是本轮只落 buffer 半(提交见下),并用它把两半的占比量了出来——**同一帧的 `by_op`**:| | waits | `by_op` ||---|---|---|| 只做了 params(frame 25) | 25,176 | `2=4536, `**`3=20633`**`, 5=6, 9=1` || 再放开 buffer 半(frame 10) | **25,154** | `2=4536, `**`3=20611`**`, 5=6, 9=1` |**buffer 半只值 22 次等待(0.1%)——20,633 里几乎全是 texture respecify**,也就是带 ID-18 M3 约束的那一半。墙钟 134.4 s → 130.9 s(同一帧、同样 101,421,396 B staged)。**texture 半的正解(下一步,尚未实现)**:让镜像**只在真答案上前进**——fire-and-forget 之后 `NoteRespecified` 收 `accepted && !provisional`,于是最坏情况退化成"多补发一条相同 respecify"(安全方向),而绝不会变成"该重发的被去重掉"。这需要在 MG_Pipe 的回复信箱上把"临时接受"和"真接受"分开(信箱本来就有"最后一次调用"的纪律,`MGPipeTakeReplyBool` 就是这么读的),改动落在 route/mailbox + `EmitResourceRespecify` 一处判断。按同一模型估算:**25,154 → ~4.5k waits ≈ 24 s 量级**(create 那 4,536 是确认本身)。 ## 3. 剩余(下一轮的直接入口) 1. **把记录数压下来(新的头号项)**。§2.5 的真机数字说:那一帧剩下的 43,232 次等待全在「必须等」的三条 row 上,