diff --git a/MobileGL/MG_Pipe/PipeRoute.h b/MobileGL/MG_Pipe/PipeRoute.h index 810cec73..76001e70 100644 --- a/MobileGL/MG_Pipe/PipeRoute.h +++ b/MobileGL/MG_Pipe/PipeRoute.h @@ -88,6 +88,8 @@ #include "MGPipe.h" #include "PipeApply.h" +// P12: MGPipeHandleIsPublished, the latch the respecify predicate below reads. +#include "PipeMutation.h" namespace MobileGL::MG_Pipe { @@ -271,6 +273,38 @@ namespace MobileGL::MG_Pipe { MGP_ResourceCreate(&desc, &reply); return MGPipeTakeReplyBool(reply, "resource_create"); } + // P12: WHICH RESPECIFIES STILL WAIT - one predicate, read by the wire emitter, so the two + // halves of this row cannot disagree about whether an answer is owed. + // + // THE BUFFER HALF NEVER DOES. Both call sites discard its Bool outright + // (PipeFill.cpp:1105 and :1142: `MGPipeRouteResourceRespecify(desc, nullptr);`), so the wait + // bought a value that was thrown away - on the device run this row was the second largest + // source of round trips in one frame, at ~5 ms each. + // + // THE TEXTURE HALF WAITS EXACTLY UNTIL THE OBJECT IS CONFIRMED, and "confirmed" is + // MGPipeHandleIsPublished: the latch is set only when the object's create was ACCEPTED, and + // the applier keeps texture records across a make-current (PipeFill.cpp:3756-3759), so for a + // published object the acceptance this row asks for is a fact the client already holds. The + // confirmation itself is paid once, at first use: EmitResourceRespecify sends a create when + // the latch is clear (TextureEmit.h:869-879), and that create is blocking. + // + // AND WHEN THE LATCH IS CLEAR THIS RETURNS TRUE, which is the whole fallback. It also covers + // the one place the two ends' consumer gates are worded differently - the client asks + // "any bit of the family" (PipeFill.cpp:1577-1595) while the applier asks "bit 10" + // (PipeApply.cpp:1408-1419) - because a create refused by the narrower server gate leaves the + // latch clear, so every later row for that object keeps taking the real answer. The cost of + // that case is throughput, never silence. + inline Bool MGPipeResourceRespecifyWantsItsReply(const MGPResourceDesc& desc) { +#if MOBILEGL_BUILD_DISAGGREGATED + if (MGPipeInstalledArm() != MGPipeRouteArm::kClientWire) return true; + if (static_cast(desc.Target) == MGPipeResourceTarget::Buffer) + return false; + return !MGPipeHandleIsPublished(MGPipeKind::Texture, desc.Resource); +#else + (void)desc; + return true; +#endif + } inline Bool MGPipeRouteResourceRespecify(const MGPResourceDesc& desc, const void* initialBytes, const MGPRespecifiedLevel* level = nullptr) { return gMGPipeRouteEscapes.ResourceRespecify(&desc, initialBytes, level); diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index 272d30ec..1821da59 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -470,25 +470,25 @@ namespace MobileGL::MG_Remote::Client { } else { MG_Pipe::MGPipeClearRespecifiedLevel(record); } - // 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. + // P12: WHETHER THIS RECORD OWES AN ANSWER IS ONE PREDICATE, and it lives beside the + // route (PipeRoute.h, MGPipeResourceRespecifyWantsItsReply) so the emitter and the wire + // cannot disagree about it. The two halves are argued there; the short version is that the + // buffer half's Bool is discarded at both call sites, and the texture half's is owed + // until the object's create has been accepted - the confirmation it pays at first use. // - // 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; + // WHAT A PROVISIONAL ACCEPT DOES TO THE TEXTURE HALF, stated here because it is a + // deliberate trade rather than an oversight: the emitter's acceptance mirror + // (entry.LastDesc) now advances on an answer nobody waited for. ID-18 M3 + // (TextureEmit.h:885-888) forbids exactly that for a record whose verdict is UNKNOWN - + // "the next identical call is suppressed against a record that was never stored" - and + // what makes it safe here is that the object is PUBLISHED: the client is not guessing, + // it is reading a latch the server set by accepting this handle's create. The residue + // is the one case where the server loses a record it had, which today has no production + // path (PipeApply.cpp:1593's only callers are tests), and whose guard is the + // invalidation notice the route's notes call ①. + const Bool wantsReply = MG_Pipe::MGPipeResourceRespecifyWantsItsReply(record); Int32 status = Wire::ReplySink::kStatusError; - if (bufferHalf) { + if (!wantsReply) { const Uint64 seq = session.EmitAndWaitTails( MGPWireOp::ResourceRespecify, &record, sizeof(record), nullptr, 0, nullptr, 0, &status, nullptr, /*wantReply=*/false); diff --git a/MobileGL/MG_Test/Wire/RemoteClientControls.inc b/MobileGL/MG_Test/Wire/RemoteClientControls.inc index a8f52f5e..bbf44123 100644 --- a/MobileGL/MG_Test/Wire/RemoteClientControls.inc +++ b/MobileGL/MG_Test/Wire/RemoteClientControls.inc @@ -894,24 +894,32 @@ TEST(RemoteClientControls, TextureUploadsStopOwningAReplyOnTheWireArm) { MG_State::pGLContext = MakeUnique(); Uint8 pixels[4 * 4 * 4]; std::memset(pixels, 0x21, sizeof(pixels)); + // THE CONTROL ROW, NAMED RATHER THAN TOTALLED, AND MEASURED ACROSS THE WHOLE SETUP. The + // object's create is the one record an object always pays for (kWaitReply in + // PipeCalls.def), so ITS counter is the live control - and the baseline goes in before + // GenTextures, because which call publishes the twin (generate, bind or the storage + // definition) is the emitter's business and not this case's. The previous version of this + // control read the session TOTAL and only moved because a respecify was still waiting; + // that row is fire-and-forget for a confirmed object now, which is what made the stale + // control visible instead of silently wrong. + const Uint32 uploadOp = static_cast(MGPWireOp::ResourceSubData); + const Uint32 createOp = static_cast(MGPWireOp::ResourceCreate); + const Uint64 createWaitsBefore = Transport::LinkMetricsReplyWaitsFor(createOp); GLuint texture = 0; MG_Impl::GLImpl::GenTextures(1, &texture); MG_Impl::GLImpl::BindTexture(0x0DE1, texture); - // THE CONTROL ROW. resource_create is kWaitReply (PipeCalls.def), so it owns a reply - // slot and this counter MUST move on it. - const auto beforeCreate = Transport::LinkMetricsReplyWaits(); MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 4, 4, 0, 0x1908, 0x1401, pixels); MGP_FILL(DrawArrays); - if (Transport::LinkMetricsReplyWaits() <= beforeCreate) ::_exit(140); + if (Transport::LinkMetricsReplyWaitsFor(createOp) <= createWaitsBefore) ::_exit(140); // THE ROW UNDER TEST. A second write into the same level is a pure resource_subdata: the // resource is created, so no create crosses, and the level is dirty again - the only // record it can produce is the one item B is about. - const auto beforeRecords = ClientWireRecordsEmitted(); - const auto beforeWaits = Transport::LinkMetricsReplyWaits(); + const Uint64 uploadRecordsBefore = Transport::LinkMetricsRecordsFor(uploadOp); + const Uint64 uploadWaitsBefore = Transport::LinkMetricsReplyWaitsFor(uploadOp); MG_Impl::GLImpl::TexSubImage2D(0x0DE1, 0, 0, 0, 2, 2, 0x1908, 0x1401, pixels); MGP_FILL(DrawArrays); - if (ClientWireRecordsEmitted() <= beforeRecords) ::_exit(141); - if (Transport::LinkMetricsReplyWaits() != beforeWaits) ::_exit(142); + if (Transport::LinkMetricsRecordsFor(uploadOp) <= uploadRecordsBefore) ::_exit(141); + if (Transport::LinkMetricsReplyWaitsFor(uploadOp) != uploadWaitsBefore) ::_exit(142); ClientSessionInstance().Stop(); }); ExpectChildSuccess(child); @@ -1038,7 +1046,11 @@ TEST(RemoteClientControls, ReplyWaitsByOpForAnAtlasShapedLoad) { ::_exit(148); if (Transport::LinkMetricsReplyWaitsFor(static_cast(MGPWireOp::ResourceCreate)) == 0) ::_exit(144); - if (Transport::LinkMetricsReplyWaitsFor(static_cast(MGPWireOp::ResourceRespecify)) == 0) + // AND ResourceRespecify FOLLOWS SetTextureParams OFF THAT LIST FOR THE SAME REASON (P12/R3): + // for a confirmed object it is fire-and-forget too. What must still hold is that its RECORDS + // go out - a zero on both lines would mean the emitter stopped sending, which is a different + // and much worse change than this one. + if (Transport::LinkMetricsRecordsFor(static_cast(MGPWireOp::ResourceRespecify)) == 0) ::_exit(145); // AND SetTextureParams IS DELIBERATELY NOT ON THAT LIST ANY MORE (P12). It was the control // that proved this counter was live; the first-use confirmation moved the row to @@ -1138,7 +1150,21 @@ TEST(RemoteClientControls, ABufferRespecifyTakesNoReplyWhileATextureOneStillDoes MGP_FILL(DrawArrays); if (Transport::LinkMetricsRecordsFor(respecifyOp) <= recordsBefore) ::_exit(154); if (Transport::LinkMetricsReplyWaitsFor(respecifyOp) != waitsBefore) ::_exit(155); - // ---- the TEXTURE half: the deliberate exception ---- + // ---- the TEXTURE half, part 1: the PREDICATE both halves read (P12/R3) ---- + // An unpublished texture still owes its answer - that is the first-use confirmation - and a + // buffer never does. Asserted directly rather than through a record, because the only way to + // produce an unpublished texture on the wire is to make the server REFUSE its create, and a + // malformed descriptor would trip the applier's wire report instead of testing this. + if (MG_Pipe::MGPipeInstalledArm() != MG_Pipe::MGPipeRouteArm::kClientWire) ::_exit(157); + MGPResourceDesc unpublished{}; + unpublished.Target = static_cast(MG_Pipe::MGPipeResourceTarget::Tex2D); + if (!MG_Pipe::MGPipeResourceRespecifyWantsItsReply(unpublished)) ::_exit(158); + MGPResourceDesc bufferDesc{}; + if (MG_Pipe::MGPipeResourceRespecifyWantsItsReply(bufferDesc)) ::_exit(159); + // ---- the TEXTURE half, part 2: CONFIRMED, end to end ---- + // GenTextures + a storage definition publishes the object (its create is accepted, and the + // create is blocking). A SECOND storage definition on that same object is a real change, so + // the record must go out - and must not take a reply. Uint8 pixels[8 * 8 * 4]; std::memset(pixels, 0x21, sizeof(pixels)); GLuint texture = 0; @@ -1146,7 +1172,12 @@ TEST(RemoteClientControls, ABufferRespecifyTakesNoReplyWhileATextureOneStillDoes 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); + const Uint64 confirmedRecords = Transport::LinkMetricsRecordsFor(respecifyOp); + const Uint64 confirmedWaits = Transport::LinkMetricsReplyWaitsFor(respecifyOp); + MG_Impl::GLImpl::TexImage2D(0x0DE1, 0, 0x8058, 16, 16, 0, 0x1908, 0x1401, pixels); + MGP_FILL(DrawArrays); + if (Transport::LinkMetricsRecordsFor(respecifyOp) <= confirmedRecords) ::_exit(160); + if (Transport::LinkMetricsReplyWaitsFor(respecifyOp) != confirmedWaits) ::_exit(161); ClientSessionInstance().Stop(); }); ExpectChildSuccess(child); diff --git a/docs/Disaggregated/MEASUREMENTS.md b/docs/Disaggregated/MEASUREMENTS.md index 9dbe5711..9fd580b5 100644 --- a/docs/Disaggregated/MEASUREMENTS.md +++ b/docs/Disaggregated/MEASUREMENTS.md @@ -14,7 +14,9 @@ 单条纯上传占用的回包等待 **1 → 0**(`wait_replies`,正是 §3.2 里"一帧 55,428"的那个计数器)。 按形状实测(`RemoteClientControls.ReplyWaitsByOpForAnAtlasShapedLoad`,主机 inproc,按 op 计数):**图集拼接**(一张 level 上 64 次 `glTexSubImage2D`)**64 → 0** 次回包等待; **每 sprite 一张纹理**(64 张纹理 + 256 次 `glTexParameteri`)**384 → 320**,剩下的全是 per-resource / per-call 的三条 row(create 76 / respecify 130 / params 130)—— - 它们的拒绝是「服务端没有这个 handle」的唯一客户端可见信号,删掉等待就是删掉信号,裁定见 `CONTRACT-P5E.md` §2.5。 + **这两条后来被"首次使用确认"取代了**(见下面那条):对象的存在性由它自己的 create 确认为事实之后, + params 与 texture respecify 都不再逐条买答案,而是只在**首次使用**时付一次阻塞的 create。 + 同一帧的累计实测:**43,232 waits / 221.5 s → 4,543 waits / 31.3 s(7.1×)**,staged 字节与发出的记录数(103,094)都不变。 真机(TrebleDroid GSI,MC 26.2 经 TCP 入世界)那一帧的构成为 `ResourceRespecify` 20,633 + `SetTextureParams` 18,056 + `ResourceCreate` 4,536 条**各等一次**, 而 `ResourceSubData` 12,671 条**零等待**(B 的实机证据);这些记录都是真实状态变化,客户端没有可去重的(同一值的 `glTexParameteri` 本来就不产生记录)。 - **单次往返那 ~5 ms 是设备侧的深度空闲退出,不是链路也不是客户端**(P12,本轮实测):同一帧、同一批记录, diff --git a/docs/Disaggregated/notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md b/docs/Disaggregated/notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md index a1484f58..e082022c 100644 --- a/docs/Disaggregated/notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md +++ b/docs/Disaggregated/notes/p12/DEVICELOST-AND-UPLOAD-WAITS.md @@ -219,6 +219,29 @@ 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 是确认本身)。 +### 2.13 R3 落地:texture respecify 也只在首次使用时付一次——那一帧 130.9 s → 31.3 s + +实现:把"这一行要不要等"做成**唯一一处**判据 `MGPipeResourceRespecifyWantsItsReply`(`MG_Pipe/PipeRoute.h`),wire 端读它决定要不要开回复槽: +buffer 半永远不等(两处返回值都被丢弃);texture 半只在**对象未确认**时等(`MGPipeHandleIsPublished` 为假),确认本身由 `EmitResourceRespecify` 在首次使用时补的那次**阻塞 create** 付掉。 + +同一台设备、同一份工作量(staged **101,421,396 B**,与改动前逐字节相同)、出厂省电、服务端 spin 默认: + +| | 那一帧 waits | `by_op`(等待) | `by_op`(发出的记录) | 墙钟 | +|---|---|---|---|---| +| R1+R2 之后 | 25,154 | `2=4536, 3=20611, 5=6, 9=1` | `2=4536, 3=20633, …` 共 103,088 | 130.9 s | +| **R3 之后** | **4,543** | `2=4536, 5=6, 9=1` | `2=4536, `**`3=20633`**`, …` 共 **103,094** | **31.3 s** | + +**respecify 那 20,633 条记录一条没少(照发),少的是它们各自的那次往返**——这正是这条路线与"少干活"的区别。 +独立审查给的预测是"4,543 waits、24–37 s(中点 ~30 s)",实测 **31.3 s**,落在中点。 + +**累计(同一帧)**:43,232 waits / 221.5 s → **4,543 waits / 31.3 s**(7.1×)。剩下的是 create 那 4,536 次等待 ≈ 23 s, +那是确认本身,不是浪费;硬地板还有客户端 CPU ~13.4 s 与传输 ~2.2 s(审查实测 43.1 MiB/s)。**想再往下只剩"批处理 create 行"**(4,536 → ~1.1k,且仍拿真答案)。 + +**两笔明账**: +1. **① 的守卫尚未接**:审查独立复核"今天不可达"(能清纹理记录的只有 `MGPipeApplierReleaseObjectRecords`,调用者只有测试;`MGPipeApplierReset` 不清它)。 + 接管时**优先查能否挂在 caps 代次/会话边界这类已有信号上**(一行);否则要新增反向通道第 10 个回调 = 协议面 = 两端重出 APK。 +2. **画面未验**:这条路线改的是"客户端还问不问",`frame-sent` 一致是强证据但不是画面证据;R3 之后应补一次 SSIM/目视。 + ## 3. 剩余(下一轮的直接入口) 1. **把记录数压下来(新的头号项)**。§2.5 的真机数字说:那一帧剩下的 43,232 次等待全在「必须等」的三条 row 上,