mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-11-07 01:53:24 +09:00
[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:
+11
-45
@@ -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 -
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)。当前阶段与下一步只在那一页维护,这里不再复述。
|
||||
|
||||
## 一张图
|
||||
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user