From f9c5e0adbcbb6f61baae427a716c774e09a0c169 Mon Sep 17 00:00:00 2001 From: Swung0x48 Date: Thu, 15 Oct 2026 08:49:35 -0400 Subject: [PATCH] [Docs] (docs/Disaggregated, docs/Performance, MG_Pipe, MG_Remote/Client, MobileGL): correct a stale default, stub banners, a call count and a status line - the create-window measurements move from Config.h to docs/Performance --- MobileGL/Config.h | 56 +++---------- MobileGL/ConfigLoader.cpp | 11 +-- MobileGL/MG_Pipe/PipeApply.h | 11 +-- MobileGL/MG_Pipe/PipeCalls.def | 11 +-- MobileGL/MG_Remote/Client/WireTables.cpp | 2 +- docs/Disaggregated/README.md | 2 +- docs/Performance/ipc-create-window.md | 101 +++++++++++++++++++++++ 7 files changed, 127 insertions(+), 67 deletions(-) create mode 100644 docs/Performance/ipc-create-window.md diff --git a/MobileGL/Config.h b/MobileGL/Config.h index 22783eda..1829c10c 100644 --- a/MobileGL/Config.h +++ b/MobileGL/Config.h @@ -645,51 +645,17 @@ namespace MobileGL::MG_Config { // restores the per-record barrier of R-1. Uint32 BatchWaits = 1; // MOBILEGL_IPC_CREATE_WINDOW: how many RESOURCE_CREATE answers may be outstanding before the - // client waits for one. 1 = every create waits (the shape R-5 shipped with); N > 1 = up to N - // creates go out fire-and-forget, their caller reads a PROVISIONAL accept, and the oldest - // answer is taken when the window fills. - // - // DEFAULT 2, AND IT TOOK THREE THINGS TO GET HERE. The deferral was inert for two - // measured reasons and one structural one, in the order they were found: - // 1. the reply buffer was a ring of the last 8 answers, and between two creates this - // client emits on the order of a thousand records - so the answer was always gone - // (`want=310` on every miss while the ring spanned [1175..1504]); - // 2. a residency budget that tried to take answers back early could not help, because - // what displaces an answer is what the SERVER sends, not what the client emits; - // 3. and so the fix was RETENTION - a link keeps a declared answer until its reader - // takes it (Transport/ILink.h's RetainsReplies, StreamLink's declared-answer store). - // - // MEASURED ON THE DEVICE, load frame, both ends rebuilt (create 4,536, records 103,097, - // frame-sent identical per op throughout): - // window 1 4,536 blocking takes 28.66 s (the shape R-5 shipped) - // window 2 2,500 17.27 s - // window 4 1,644 11.02 s - // and the round trip does NOT grow as the window deepens (5,285 / 4,082 us against 5,672 - // at 1), which is the measurement that says the ~5 ms is a WAKEUP the server batches away - // rather than its apply throughput - so amortising four creates per wakeup is a real 2.6x. - // - // 6 IS THE DEFAULT, AND IT IS WHERE THE CURVE FLATTENS - the sweep that says so is the - // one the paragraph above asked for, and it moved the ceiling too (kCreateWindowMax is 8 - // now; the old 4 was derived from a reply pool the window does not run on). Three - // interleaved runs per arm on one binary, load frame, create 4,536 sent in every arm: - // - // window 4 1,503 blocking takes 13.23 s median (spread 11.37-16.29) - // window 5 1,277 11.31 s (10.61-12.12) - // window 6 1,155 10.79 s ( 9.69-10.81) - // window 8 859 10.96 s ( 8.92-11.03) - // - // 4->5 buys 1.92 s, 5->6 buys 0.53 s, 6->8 buys NOTHING (-0.18 s, inside a session noise - // of ~1.5-2 s) - so 6 is the smallest window on the flat part and the extra answers a - // deeper one holds buy no time. AND PAST THIS POINT THE FRAME IS NOT WAIT-BOUND ANY MORE: - // window 8 pays 296 fewer blocking takes than window 6 and the wall clock does not move. - // The store is not the constraint either - its peak is logged as it crosses (StreamLink's - // P12 line) and reads 6..11 against a cap of 24 at every arm from 5 up. What is left is - // the constant ~350-400 creates that block for a reason depth does not reach, and the - // 101 MB / 103k records the SERVER has to apply. NOTE THE PREDICATE the whole mechanism rests - // on: an answer is retained because the wire DECLARED it will be read - NOT because its - // caller was fire-and-forget. The window's own creates are fire-and-forget AND read, so a - // flag keyed on wantReply would disable the very mechanism it exists to enable - // (docs/Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md sections 7-11). + // client waits for one. 1 turns the window off; N > 1 lets up to N creates go out without + // waiting: each caller reads a provisional accept, the answers are collected in order (the + // oldest once the window is full), and a create that comes back declined is un-published. + // The window runs only on a link that keeps a declared answer until its reader takes it (the + // stream link); on the shared-memory link it is always 1. + // Default 10: on a device world-load frame the wall clock stops improving at about 6, and a + // deeper window costs only entries in the link's declared-answer store, so 10 sits on the + // flat part of the curve. + // Range 1..12; a value outside it is ignored with a warning. 12 is kCreateWindowMax + // (WireTables.cpp), half of the stream link's 24-entry answer store. + // Measurements: docs/Performance/ipc-create-window.md. Uint32 CreateWindow = 10; // MOBILEGL_IPC_PBO_READBACK_SYNC (P9 W1, MG_Remote/CONTRACT-P9.md §1): 1 = a glReadPixels // or glGetTexImage into a bound GL_PIXEL_PACK_BUFFER takes the reply-slot form again - diff --git a/MobileGL/ConfigLoader.cpp b/MobileGL/ConfigLoader.cpp index cb9cda8b..7b355dd8 100644 --- a/MobileGL/ConfigLoader.cpp +++ b/MobileGL/ConfigLoader.cpp @@ -594,14 +594,9 @@ namespace MobileGL::MG_ConfigLoader { // filled between two of the client's drains, which is an ordinary backlog. ipc.EventWaitMs = QueryEnvUint32("MOBILEGL_IPC_EVENT_WAIT_MS", 2000, 1, 600000); ipc.BatchWaits = QueryEnvUint32("MOBILEGL_IPC_BATCH_WAITS", 1, 0, 1); - // The create window. DEFAULT 6, and it works because a link that declares RetainsReplies - // keeps a declared answer until its reader takes it - Config.h carries the three reasons it - // was inert before, the device numbers, and the sweep that put the default at 6 rather - // than 4 or 8 (the curve flattens at 6: 4->5 buys 1.92 s, 5->6 0.53 s, 6->8 nothing). - // Capped at 8: the retained set is bounded by the window's own depth, the store holds 24 - // declared answers, and the device sweep showed the wall clock still falling at the old - // cap of 4 (see WireTables.cpp's kCreateWindowMax). WireTables.cpp's static_assert and - // StreamLink's crossing log are the checks on this number. + // The create window: default 10, range 1..12. 12 is WireTables.cpp's kCreateWindowMax, half + // of the stream link's declared-answer store, and its static_assert checks that bound. + // Config.h has the semantics and the reason for the default. ipc.CreateWindow = QueryEnvUint32("MOBILEGL_IPC_CREATE_WINDOW", 10, 1, 12); // P9 W1: the pack-buffer readback's A/B and red-once control (Config.h says both). ipc.PboReadbackSync = QueryEnvUint32("MOBILEGL_IPC_PBO_READBACK_SYNC", 0, 0, 1); diff --git a/MobileGL/MG_Pipe/PipeApply.h b/MobileGL/MG_Pipe/PipeApply.h index 371e3f5d..5e3fd10b 100644 --- a/MobileGL/MG_Pipe/PipeApply.h +++ b/MobileGL/MG_Pipe/PipeApply.h @@ -1352,9 +1352,8 @@ namespace MobileGL::MG_Pipe { // never a copy (see MGPipeResourceOps). A null is a real answer wherever the payload says // the content is undefined. // - // AT THE CONTRACT COMMIT EVERY BODY BELOW IS A STUB. The signatures are what the client, - // the backend and the gates compile against, and the records above are what they write - // into; the bodies land in the two commits that follow this one on the same branch. + // The bodies are in PipeApply.cpp. The signatures are what the client, the backend and the + // gates compile against, and the records above are what they write into. // The scope of one resource_respecify, and it is an APPLIER-SIDE ARGUMENT and not a wire // record: it is not in PipeFields.def, it crosses no payload, and the transport reads the @@ -1594,10 +1593,8 @@ namespace MobileGL::MG_Pipe { // wrote it; every other target stores and returns. The branch is one comparison against // kMGPipeResourceTargetBuffer and it is where a mis-typed descriptor becomes visible. // - // AT THE CONTRACT COMMIT EVERY BODY BELOW IS A STUB, exactly as P3a's nine were: the - // signatures are what the client, the backend and the gates compile against and the - // records above are what they write into; the bodies land in the three commits that - // follow this one on the same branch. + // The bodies are in PipeApply.cpp. The signatures are what the client, the backend and the + // gates compile against, and the records above are what they write into. // set_framebuffer_state. Fully resolved - nothing in the record requires a lookup on the // far side. ContentHash covers every field including Fbo and DrawBuffers[8], which is what diff --git a/MobileGL/MG_Pipe/PipeCalls.def b/MobileGL/MG_Pipe/PipeCalls.def index 2029f366..1f2ea6db 100644 --- a/MobileGL/MG_Pipe/PipeCalls.def +++ b/MobileGL/MG_Pipe/PipeCalls.def @@ -79,13 +79,14 @@ // kCtxCso 13 CSO create/bind/delete // kCtxState 19 16 of the 18 set_* calls + the temporary set_residual_value_state // + P5c rv's set_context_values + P5e's set_program_bindings -// kCtxObject 10 set_texture_params (the 17th set_*) + 8 object-scoped transfers, plus -// P5c's object_death 1 -// kCtxVerb 18 3 context-reading transfer calls + the 10 commands, plus the five +// kCtxObject 11 set_texture_params (the 17th set_*) + 8 object-scoped transfers, plus +// P5c's object_death 1 and get_texture_image_to_buffer 1 +// kCtxVerb 20 3 context-reading transfer calls + the 10 commands, plus the five // P5b-appended verbs (bind_shader_image, patch_parameter, // bind_stream_output, set_storage_block_binding, -// copy_framebuffer_to_texture - MG_Remote/CONTRACT-P5B.md) -// total 81 +// copy_framebuffer_to_texture - MG_Remote/CONTRACT-P5B.md), plus +// delete_stream_output 1 and read_pixels_to_buffer 1 +// total 85 // // Reconciliation with the plan's headline numbers (section 4.4 / appendix A), because they // do not add up to a set of UNIQUE records and this file has to hold unique records: diff --git a/MobileGL/MG_Remote/Client/WireTables.cpp b/MobileGL/MG_Remote/Client/WireTables.cpp index 63ea6b36..55b354aa 100644 --- a/MobileGL/MG_Remote/Client/WireTables.cpp +++ b/MobileGL/MG_Remote/Client/WireTables.cpp @@ -395,7 +395,7 @@ namespace MobileGL::MG_Remote::Client { // other reply traffic. // (THE NARRATIVE ABOVE PREDATES RETENTION and ends at "the default is 1". It is kept as // the record of what was tried; what made the window work is the declared-answer store, - // and the default is 2 - Config.h carries the device numbers.) + // and the default is now 10 - docs/Performance/ipc-create-window.md carries the device numbers.) constexpr Uint32 kCreateWindowMax = 12; // THE POOL ARGUMENT THAT USED TO PIN THIS AT 4 IS GONE, AND WHY IT IS GONE IS THE ONE // THING TO RE-CHECK BEFORE RAISING IT AGAIN. It read: the pool is kDefaultReplySlotCount diff --git a/docs/Disaggregated/README.md b/docs/Disaggregated/README.md index aad4968d..569a0798 100644 --- a/docs/Disaggregated/README.md +++ b/docs/Disaggregated/README.md @@ -2,7 +2,7 @@ **做什么**:在 MobileGL 的前端(处理应用的 GL 调用)和后端(调用 GLES / Vulkan 驱动)之间立一份显式接口——前端把状态变化**推**给后端,后端不再直接读前端的内存。有了这份接口,前后端可以放在两个线程、两个进程,甚至两台机器上运行。 -**做到哪了**(2026-09-25):接口与拆分已完成到"两个进程、可经 TCP 跨机"。当前在做 **P12:让 Android 上的 server 自己开窗口,把画面直接显示到屏幕上**——审查后的主机定向测试和 Redmi 真机七项复测已通过;FCL 与跨机 TCP 两项验收仍未完成。→ [`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md) +**做到哪了**:见 [`CURRENT_STAGE_PROGRESS.md`](CURRENT_STAGE_PROGRESS.md)。当前阶段与下一步只在那一页维护,这里不再复述。 ## 一张图 diff --git a/docs/Performance/ipc-create-window.md b/docs/Performance/ipc-create-window.md new file mode 100644 index 00000000..410ca764 --- /dev/null +++ b/docs/Performance/ipc-create-window.md @@ -0,0 +1,101 @@ +# The client's create window (MOBILEGL_IPC_CREATE_WINDOW) + +Measured 2026-09-26 on the split stack (client and server in separate processes). These are the numbers +that used to live in the comment above `IpcTable::CreateWindow` in `MobileGL/Config.h`. The longer record, +with the logs and the attempts that failed, is +[`notes/p12/CREATE-WINDOW-MEASURED.md`](../Disaggregated/notes/p12/CREATE-WINDOW-MEASURED.md) (in Chinese). + +## What the knob does + +Every new buffer, texture or renderbuffer sends a `resource_create` record. When these numbers were taken, +the client waited for the server's answer to every one of them, because a create the server refused must be +un-published so that later records for that object take the blocking path. + +With a window of N, up to N creates go out without waiting. Each caller reads a provisional accept, the +answers are collected in order, and the oldest is waited for once the window is full. A create that comes +back declined is un-published when its answer is collected. 1 turns the window off. + +The window runs only on a link that keeps a declared answer until its reader takes it +(`ILink`'s `RetainsReplies`, the stream link's declared-answer store). On the shared-memory link it is +always 1. + +## Setup + +- Server: an Android phone (a generic system image on a MediaTek mt6893 with a Mali GPU, Android 14). +- Client: Minecraft on a separate machine, connected over TCP with the stream data plane. +- Workload: the single long frame in which the game loads its world. Every arm sends the same records: + 4,536 creates, 103,097 records in all and about 101 MB of staged bytes, with identical per-op counts. +- "Blocking waits" counts the times the client blocked on a create's answer, on the blocking path or while + collecting the oldest answer from a full window (`by_op 2` in the client's link metrics). "Wall clock" is + the duration of that frame. + +## Why the window did nothing at first + +The first version changed nothing: window 4 and window 1 both took 4,536 blocking waits and 28.64 s. It +took three findings, in this order, to make it work: + +1. The client kept answers in a ring of the last 8. Between two creates the client emits on the order of + a thousand records, so the answer was always gone by the time the client came back for it (at the first + miss the head wanted seq 310 while the ring held seqs 1,175 to 1,504, and it wanted 310 on every miss + after that). +2. A residency budget that tried to collect answers early could not help: what displaces an answer is + what the server sends, not what the client emits, and the server is behind. +3. So the fix was retention: a link keeps a declared answer until its reader takes it. + +The answer is kept because the wire declared that it will be read, not because the caller did not wait. +The window's own creates are both fire-and-forget and read, so a flag keyed on "the caller does not wait" +would switch off the very mechanism it exists to enable. + +## Results + +### With retention, both ends rebuilt + +| window | blocking waits | wall clock | round trip per blocking wait | +|---:|---:|---:|---:| +| 1 (the original blocking shape) | 4,536 | 28.66 s | 5,672 us | +| 2 | 2,500 | 17.27 s | 5,285 us | +| 4 | 1,644 | 11.02 s | 4,082 us | + +The round trip does not grow as the window deepens. The roughly 5 ms is a wakeup cost that the server +batches away, not its apply throughput, so collecting four creates per wakeup is a real 2.6x. + +A first sweep with three interleaved runs per arm showed the curve still falling at the old ceiling of 4 +(1 to 2 saved 10.29 s, 2 to 3 saved 3.74 s, 3 to 4 saved 1.83 s), so the ceiling was raised. + +### Sweep past 4 (one binary, three interleaved runs per arm) + +| window | blocking waits | wall clock, median | spread | +|---:|---:|---:|---:| +| 4 | 1,503 | 13.23 s | 11.37 - 16.29 s | +| 5 | 1,277 | 11.31 s | 10.61 - 12.12 s | +| 6 | 1,155 | 10.79 s | 9.69 - 10.81 s | +| 8 | 859 | 10.96 s | 8.92 - 11.03 s | + +## Reading + +- 4 to 5 saves 1.92 s, 5 to 6 saves 0.53 s, and 6 to 8 saves nothing (-0.18 s, inside the session noise + of about 1.5 to 2 s). The curve is flat from about 6. +- Past that point the frame is not limited by the window: window 8 has 296 fewer blocking waits than + window 6 and the wall clock does not move. +- The answer store is not the limit either. Its peak, logged by the stream link, read 6 to 11 entries + against a capacity of 24 at every window from 5 up. +- What remains is a roughly constant 350 to 400 creates that block for reasons the window depth does not + reach, and the 101 MB and 103k records the server has to apply. A later server-side timing at window 6 + put the server's apply time at 1.95 s of a 9.64 s frame, while the client's 1,161 blocking waits at + about 4.9 ms each came to 5.7 s. Sending the same bytes over USB instead of Wi-Fi did not change the + wall clock. + +## What shipped + +| commit | default | ceiling | +|---|---:|---:| +| `3a255580` (retention lands) | 2 | 4 | +| `102f2ca7` (sweep above) | 6 | 8 | +| `3a6c5c7f` (current) | 10 | 12 | + +No sweep beyond 8 is recorded. 10 sits on the flat part of the curve, and a deeper window costs only +entries in the stream link's answer store. The ceiling, `kCreateWindowMax` in +`MobileGL/MG_Remote/Client/WireTables.cpp`, is held by a `static_assert` to at most half of that store's +24 entries; the other half is for the one blocking create in flight and for answers a retired window has +left behind. The store's overflow is a named fatal error, never an eviction. The environment variable +accepts 1 to 12; a value outside that range is ignored with a warning and the default is used.