[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
This commit is contained in:
2026-10-15 10:15:38 -04:00
parent 5370d917d0
commit f9c5e0adbc
7 changed files with 127 additions and 67 deletions
+11 -45
View File
@@ -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 -
+3 -8
View File
@@ -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);
+4 -7
View File
@@ -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
+6 -5
View File
@@ -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:
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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)。当前阶段与下一步只在那一页维护,这里不再复述。
## 一张图
+101
View File
@@ -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.