mirror of
https://github.com/MobileGL-Dev/MobileGL
synced 2026-09-08 04:08:32 +09:00
901d48a678083cbed73e044d5de5037e5ac0c91b
2625
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
901d48a678 |
[Fix] (MGPipe, Metrics, Config, CI): exchange the per-frame stats instead of racing a store, carry the three uncarried table entries, drop the inline host span from a buffer range, spell the buffer subdata range, and close the small gate holes
- S-1 PipeStats::OnPresent read each frame accumulator and then store(0)'d it; a Bump from a staging thread landing in between was lost from the Tracy plot and from every frame. Each accumulator is now exchange(0, relaxed) and the exchanged value is what is plotted, so every add lands in exactly one frame. - T-3 FdPassing without MSG_CMSG_CLOEXEC (macOS, BSD) handed back descriptors that survived exec; every received fd now gets FD_CLOEXEC by hand under !MSG_CMSG_CLOEXEC. MSG_NOSIGNAL is defined to 0 where the platform lacks it (FdPassing.cpp, Doorbell.cpp) and SO_NOSIGPIPE is set on the socketpair and on a SocketDoorbell's descriptor where it exists, so a write to a hung-up peer is EPIPE rather than a fatal signal. - T-4 the missing-flatbuffers fallback wrote OFF into the cache with FORCE, so a plain re-configure after `git submodule update` stayed OFF silently. It is a normal-variable set now, shadowing the cache for that configure only; verified by hiding flatbuffers.h, configuring with ON (warning, transport off, cache still ON) and re-configuring plainly with the header back (transport ON). - P-2 three LIVE GLFunctionsTable entries had no carrier: GetGpuTimestampNs (glGetInteger64v(GL_TIMESTAMP), a synchronous server answer), QueryCounterTimestamp (glQueryCounter, a one-shot stamp, not a begin/end pair) and WaitSync (the GPU-side wait FenceWait's client wait does not express). QueryTimestamp (MGPTimestampRequest, kCtxQuery, kReplySlot), QueryCounter (MGPQueryDesc with Kind = GL_TIMESTAMP, kCtxQuery) and FenceWaitServer (MGPFenceWait, kScreen) are APPENDED at the end of PipeCalls.def because the opcode is the position: SetSwapInterval stays 68, the three take 69-71, and PipeCatalogue.LateArrivalsAreAppendedWithoutRenumbering pins that. Header counts 71 (screen 11, query 8); the seven generators regenerated. - P-3 MGPBufferRange inlined a 32-byte MGHostSpan into every range of every class - dead space on every SSBO, atomic-counter and XFB range, and D-B8 says not to freeze the named-UBO payload before the stage-ubo-named numbers exist. The range is 24 bytes now; the host spans are an optional second var-tail behind the ranges, announced by MGPShaderBuffers::HostSpanCount (0 or Count), with set_shader_buffers keeping its kVarTail|kHostSpan flags. PipeCatalogue.BufferRangeCarriesNoInlineHostSpan pins the sizes, the flags and the comparator's view of the count. - P-4 QueryEnvUint64 parsed with base 0 (a leading zero meant octal: MOBILEGL_PIPE_PUSH=010 read as 8) and accepted -1 as every bit set; it is decimal or explicit 0x now and a '-' anywhere is refused with the warning (smoke through the integration binary: -1 and 12abc warn, 010 and 0x10 parse). The CI stdio gate's alternation now also catches fprintf(stdout, puts( and std::cout/cerr; it is green over MG_Backend and MG_State. MGPSubData states how the buffer half expresses [offset, size): UnionBox.X / UnionBox.W with Target == Buffer, Y = Z = 0, H = D = 1, one record bounded at a 2^31-1 offset and 2^32-1 size beyond which the emitter splits (the same rule the ring's half-capacity bound already imposes); MGPipeSetSubDataBufferRange / MGPipeSubDataBufferOffset / Size are the only spelling and PipeCatalogue.SubDataBufferRangeRidesInTheUnionBox pins the encoding and its bounds. gen_pipe.py now refuses, in both modes, a call payload named in PipeCalls.def with no field list in PipeFields.def (the four memcmp-fallback member types are the documented exception); shown by dropping P(MGPSwapInterval), which exits 1 naming the payload. - The MGPPixelPackState size assertion compared sizeof against itself; it asserts the literal 28 PixelStoreParameters measures. - Verified: ctest -L unit green in both the default and the split configuration, gen_pipe.py --check clean with the generated files committed, nm --defined-only of the default libMobileGL.so has no MG_Remote symbol, and the full integration-gpu suite passes (the *IsActuallyArmedWhenTheEnvironmentPinsItOn family trips under -j 8 as documented and passes serially). |
||
|
|
e8ee7b1a88 |
[Feat] (Backend, MGPipe): carry the six per-axis compute limits in DynamicBackendParameters so MGPCaps has every backend-owned indexed answer, and pin them against glGetIntegeri_v on both backends
- P-1: MGPCaps is DynamicBackendParameters by inclusion (plan B section 4.4.1), but that struct carried MaxComputeWorkGroupInvocations and no per-axis GL_MAX_COMPUTE_WORK_GROUP_COUNT / GL_MAX_COMPUTE_WORK_GROUP_SIZE - the six numbers that ARE the backend-owned indexed answers surviving the getter retirement (GL_Getter.cpp and CompileEnv.cpp ask GLFunctionsTable::GetIntegeri_v for exactly these, DirectVulkan answers them from VkPhysicalDeviceLimits), so the interface had a hole where its only genuine indexed carrier should be. DynamicBackendParameters now has MaxComputeWorkGroupCount[3] / MaxComputeWorkGroupSize[3] with the GL 4.3 minimums as the no-backend defaults; DirectGLES fills them from glGetIntegeri_v inside the loader's bracketed probe run (GLESCapabilities carries them, logged with the other limits) and DirectVulkan from maxComputeWorkGroupCount / maxComputeWorkGroupSize through the loader's SaturateToInt like every other limit. Raw driver answers, as the invocations limit is: the frontend floors them at the shared MIN_COMPUTE_WORK_GROUP_* minimums itself. - The GetIntegeri_v table path is untouched, as is GL_Getter and CompileEnv behaviour: retiring the getter in favour of the caps is P0.5, and this only makes sure the caps have what P0.5 needs. - PipeCalls.def's footer no longer claims that "only GL_COMPUTE_WORK_GROUP_SIZE is a real backend answer and it lives in MGPCaps": the six limits live in MGPCaps, and GL_COMPUTE_WORK_GROUP_SIZE is a frontend link artifact (ProgramObject::GetComputeLocalSize, what GL_Program.cpp answers from), which AdvertisedLimitsScenario.ComputeLocalSizeComesFromTheLinkedProgram already pins. The MGPCaps size assertion is a composition of sizeof(DynamicBackendParameters) and follows the struct. - AdvertisedLimitsScenario.ComputeWorkGroupLimitsAreTheCapsBlocksAnswer pins, on both lanes: answerability, the GL 4.3 floors, vector/indexed agreement, INVALID_VALUE past axis 2, and - through the new Harness/BackendCapsPeek translation unit, which is the one place the module looks past the GL API - that max(caps, minimum) equals the live glGetIntegeri_v answer axis by axis. Shown live by halving each backend's caps copy: both lanes fail with "MGPCaps carries 512 but glGetIntegeri_v answers 1024". On Android the module links the shipping .so (hidden visibility), so the peek returns false there and only the GL-visible half runs. ComputeWorkGroupCapabilities.TakesEveryAxisFromTheIndexedQuery in BackendLoaderTest pins the DirectGLES loader half against the fake driver, per axis and above the initialisers. - Verified: AdvertisedLimitsScenario 20/20 on DirectGLES and DirectVulkan (llvmpipe / lavapipe), BackendLoaderTest green. |
||
|
|
1154f9a00d |
[Fix] (MG_Remote, Transport): give the inproc doorbell a death state so Shutdown can join a parked waiter, and bound a ring record at half the capacity so a refusal can never look like backpressure
- T-1: InProcessChannel::Close rang each CondVarDoorbell once and claimed that unparks a peer mid-frame. It does not. Doorbell::Wait consumes the one ring, re-tests a condition nothing published, finds the bell alive (CondVarDoorbell never overrode Dead(); Doorbell.cpp had no death state at all) and with kWaitForever parks again for good - so Shutdown could never join a server thread sitting in the design's own steady state (spun, set consumerParked, blocked; plan section 8.1 inheriting the earlier plan's 6.2a). CondVarDoorbell now carries an atomic death latch: Kill() sets it under the mutex and notify_all's, Dead() reports it, Park returns false at once on a dead bell (and the wait predicate includes it, so a Kill cannot slip between the test and the wait), and Close kills both bells instead of ringing them. Same shape as SocketDoorbell's EOF latch; Notify stays the ordinary wakeup. - T-2: RingProducer::Reserve refused only total > capacity, but a record with capacity/2 < total <= capacity is unplaceable at every head offset where neither the space to the wrap boundary nor the space before it holds it - even in an EMPTY ring, because a wrap pad costs spaceToEnd bytes on top of the record. Concretely: head offset 16 of an empty 256-byte ring, a 248-byte record; FreeBytes() says 256, Reserve says nullptr, forever, and a producer waiting for FreeBytes() >= 248 stalls with nothing logged. The bound is now capacity/2, which is exact rather than conservative (worst case 2*total-8 <= capacity-8), exposed as MaxRecordBytes() for the emitter to chunk against; the minimum ring is two headers so the smallest record still fits the bound. Ring.h states Capacity()/2 as the chunking bound and the G3 header comment in gen_pipe.py now states the chunking rule plan section 8.2 asks G3 to define (PipeWire.inc regenerated). - Tests, each shown red with only the fix site reverted and green with it: InProcessTransportTest.ShutdownUnparksAWaiterWithNoDeadline (bounded join through a shared_ptr-owned waiter: 5 s red instead of a hung job; reverted it hangs and fails at 5051 ms), RingTest.RecordLargerThanHalfTheRingIsRefused (reverted, the 248-byte record is accepted), RingTest.RecordPlaceabilityDoesNotDependOnTheHeadOffset (the offset-0 vs offset-16 negative control), RingTest.HalfCapacityRecordFitsAtEveryHeadOffset (the positive half: the maximal record at all 32 head offsets of a 256-byte ring) and RingTest.RejectsARingTooSmallForTheSmallestRecord. |
||
|
|
7ef7c7e543 |
[Fix] (Spikes): answer the tier question for DirectGLES too, make an OK mean bytes round-tripped through a real GPU access, and exercise T3 in the direction that makes it a tier
- plan-B §8.3 asks which tier `AcquirePersistentMap` lands in, but the probe only
asked Vulkan. DirectGLES ("Espryt") reaches a persistent map through
`glBufferStorageEXT` + `glMapBufferRange(PERSISTENT|COHERENT)`, not a
`VkDeviceMemory` map, so a Vulkan-only answer decides nothing for that backend.
Add a GLES leg to T1: the exported fd imported with `glCreateMemoryObjectsEXT`
+ `glImportMemoryFdEXT` + `glBufferStorageMemEXT`, then mapped
PERSISTENT|COHERENT -- in-process first (isolates "GL can import this fd" from
"the fd survives a process boundary"), then cross-process (new `t1gl` child).
Drivers disagree about how the import must be phrased, so each attempt walks a
ladder over {dedicated flag} x {import size = memory requirement or the fd's
own size} x {buffer size} and reports the rung the driver accepted plus every
rejected rung with its GL error -- a driver *preference* must never be reported
as a missing capability. A driver that backs the storage but refuses
PERSISTENT|COHERENT is reported separately from one that refuses the storage:
that distinction is exactly T1 vs T2 for DirectGLES. The T0 GLES leg
(`EGL_ANDROID_get_native_client_buffer` + `glBufferStorageExternalEXT` +
persistent map, verified by `AHardwareBuffer_lock` on the client side) now
reports every step's GL enum and requires the persistent flags for OK.
- the verdict was unfalsifiable: T1 reported PARTIAL when neither leg had moved a
byte. Replace it with an explicit decisive-leg model -- OK only when every
decisive leg round-tripped in both directions, PARTIAL when at least one did,
FAIL otherwise with the failing step and its driver error named in `why:`.
Every row now opens with a per-leg trace (`vkimport[D]=rt gpu[D]=rt`). The raw
`mmap` leg is informational for opaque-fd (Vulkan forbids interpreting that
payload outside the driver, so a refusal is conformant) and decisive for
dma-buf, where a CPU mapping is the point of the handle type.
- T3 never ran the direction that would make it a tier: both ends were the
importing process. Add `T3-client-memfd-server-import` (new `t3c` child) -- the
client creates and writes the memfd, the server mmaps the received fd, imports
the client's host pointer into a `VkDeviceMemory`, reads what the client wrote,
writes back, and takes a GPU access on the client's memory, which the client
then verifies through its own mapping.
- no route touched the GPU, so an OK proved only that a map call returned a
pointer. Every tier row now takes a real GPU access before it can be OK:
`vkCmdCopyBuffer` out of the shared allocation into private staging (mismatch =
the GPU could not read what the peer wrote) plus `vkCmdFillBuffer` into it,
queue-idle and an explicit host-read barrier, with the peer checking the filled
region through its own mapping. VkCtx grows a queue and command pool for it.
- the device run executes in the `shell` SELinux domain, not the `untrusted_app`
domain MobileGL runs in, and the two do not share dmabuf/gralloc rules. Print
uid/pid/`/proc/self/attr/current` in a run-context header, repeat the caveat in
the summary, and document in README.md how to answer it for the real domain
later (exec the same binary from the trace app's spike hook, spike-A package)
without implementing that here.
- `vkStr()` returned a pointer into one static buffer while several results
routinely appear in one format call, so all of them showed the last one; it
returns std::string now, `memFlagStr` likewise, and `fmt`/`pr` carry
`format(printf)` so a missed `.c_str()` is a compile error rather than UB.
- `advertisedExportable` decided the status at the allocate site but not at the
`vkGetMemoryFdKHR` site. One rule at every export failure now
(`exportFailStatus`): advertised EXPORTABLE and then declining is FAIL, never
advertised is UNSUPPORTED. Export + map + fd is factored into `exportHostVisible`.
- `T0-ahb-blob-transfer` was recorded OK on the socket handoff alone. The handoff
keeps its own informational row; the tier row is now composed at the end from
the full import+map+compare+writeback chain over the Vulkan, GL and GPU legs.
- `mmapErrno` kept the first attempt's errno after the second-chance mmap
succeeded, so a working mapping carried a failure code; it is cleared on
success and the first errno moves into the note.
- a failed `glImportMemoryFdEXT` no longer closes the fd: EXT_memory_object_fd
does not say whether ownership still transfers on failure and Mesa closes it
either way, so closing risks a double close landing on the socket. Leaking a
handful of dups in a short-lived probe is the safe side of that trade.
- validated end to end on the host harness (lavapipe + llvmpipe,
`VK_DRIVER_FILES=lvp_icd.json EGL_PLATFORM=surfaceless`): T1-opaque-fd OK,
T3-external-memory-host OK, T3-memfd-cross-process OK,
T3-client-memfd-server-import OK, T1-dma-buf UNSUPPORTED (not advertised
exportable). The two T1-gles rows FAIL there with GL_OUT_OF_MEMORY on every
ladder rung although GL_DEVICE_UUID_EXT matches the Vulkan deviceUUID --
llvmpipe's GL does not implement importing a lavapipe opaque-fd allocation,
a Mesa interop gap recorded in README.md so a device FAIL stays attributable.
Rebuilt for arm64-v8a with NDK r27d (PIE, android-30); the device run is
pending, both device locks are held by another campaign.
|
||
|
|
6c7ad0a1bf |
[Feat] (Spikes): add the standalone external-memory probe that decides the persistent-map tier
- Plan B §11 P0 requires spike B ("external memory 导出,两台设备") to run before the
persistent-map decision of §8.3 can be taken: T0 (server imports a client
allocation), T1 (server exports its own HOST_VISIBLE|HOST_COHERENT allocation)
or T2 (AcquirePersistentMap returns nullptr, making the §5.10 client-side block
push mandatory). §8.3 says the answer must be measured on the two campaign
devices, and that a platform unknown must not block the interface work.
- tools/spikes/extmem_probe/ is a self-contained NDK command-line program: it
links vulkan/EGL/GLESv3/android/log and nothing from MobileGL, is configured by
its own CMakeLists with the android toolchain file, and is deliberately absent
from the project's build graph (the root CMakeLists only pulls in
tools/trace_replay), so the default ALL target is untouched.
- Phase A enumerates VK_KHR_external_memory_fd, VK_EXT_external_memory_dma_buf,
VK_EXT_external_memory_host, VK_ANDROID_external_memory_android_hardware_buffer
and, through a headless EGL pbuffer context, GL_EXT_memory_object{,_fd},
GL_EXT_external_buffer, GL_EXT_buffer_storage, GL_OES_EGL_image_external{,_essl3}
and EGL_ANDROID_get_native_client_buffer, plus the memory-type table and the
vkGetPhysicalDeviceExternalBufferProperties verdict per handle type for the exact
buffer usage set MobileGL needs.
- Route T1 allocates a HOST_VISIBLE|HOST_COHERENT buffer memory with
VkExportMemoryAllocateInfo, writes a pattern through vkMapMemory, exports an fd
with vkGetMemoryFdKHR (opaque-fd and, where advertised, dma-buf), hands it to a
second process over SCM_RIGHTS, and has that process both mmap() the fd and
import it into its own VkDeviceMemory + vkMapMemory. Both sides write and both
sides compare, so a copy-on-import or one-directional mapping is reported as
PARTIAL rather than as success.
- Route T0 has the second process allocate an AHardwareBuffer BLOB
(CPU_READ_OFTEN|CPU_WRITE_OFTEN|GPU_DATA_BUFFER), send it with
AHardwareBuffer_sendHandleToUnixSocket, and the first process import it three
ways -- AHardwareBuffer_lock, VkDeviceMemory via
VK_ANDROID_external_memory_android_hardware_buffer, and a GL buffer via
eglGetNativeClientBufferANDROID + glBufferStorageExternalEXT mapped
persistent/coherent -- with a write-back leg the allocating process verifies.
- Route T3 covers VK_EXT_external_memory_host: a memfd-backed mmap region aligned
to minImportedHostPointerAlignment, imported through
VkImportMemoryHostPointerInfoEXT, plus the same memfd handed to another process.
- The second process is /proc/self/exe re-exec'd with --child=<route> and one end
of a socketpair on fd 3. A bare fork() is not usable: neither side's Vulkan
driver survives fork, and both sides need live Vulkan. It is also the topology
the transport actually has (§8.1, inheriting PLAN.md §11.1-§11.6: the client
spawns the server), so the probe measures the arrangement that would ship.
- The probe also builds for the host with T0 compiled out. That is not scope
creep: a negative device result is only worth something if the harness is known
to report a working route as working. Running it on lavapipe did that, and paid
for itself immediately by exposing two harness bugs that would have produced
false negatives on the devices -- (a) the child wrote through its plain mmap
before reading through the Vulkan import, so on a driver whose exported fd maps
at an offset the probe overwrote the very payload the second read compares
(lavapipe reports payloadAt=4096); reads through both mappings now precede
writes through either, and the offset is searched for and reported; (b) an
export failure on a handle type the driver never advertised as EXPORTABLE was
classified FAIL instead of UNSUPPORTED (lavapipe's dma-buf answer).
- Output is a RESULT/summary table carrying the raw driver verdicts (VkResult
names, errno, GL enums) because those codes -- not a pass/fail bit -- are what
§8.3 needs in order to pick the tier.
- Built with NDK 27.3.13750724 for arm64-v8a / android-30, RelWithDebInfo, PIE,
warning-clean; host build clang/RelWithDebInfo, warning-clean.
|
||
|
|
38d4c2372c |
[Feat] (TraceApp, CI): pass arbitrary env vars through the retrace lane
- PLAN-B.md §8.2 and appendix B add a batch of new runtime switches
(MOBILEGL_PIPE_PUSH / _VERIFY / _STATS / _LEGACY_MEMOS / _TEXEL_RETAIN_MB /
_INDEX_MIRROR_MB, plus MOBILEGL_IPC_* later), and §11 P0 wants them parsed beside
the existing ones. Today every knob that has to reach an Android replay costs an
edit in five files - run_android_retrace_local.py, trace-replay-ci.sh,
TraceReplayActivity's request record, the JNI marshalling, and the setenv block in
trace_replay_core.cpp. That per-knob tax is what this replaces: one extra,
`--es mobilegl_env "K=V;K=V"`, carries all of them.
- Applied last, immediately before dlopen(libMobileGL.so), so it can also override
the dedicated fields above it - MobileGL's config is read during the load, and an
escape hatch that cannot beat the defaults is not one. An entry with no '=' unsets
the variable, which is the only way to clear a default the marshalling sets.
- The existing per-knob flags stay: they carry semantics beyond a setenv (use_angle
also selects a variant, the dump lists are joined, DirectVulkan forces the
R11G11B10F fallback), and rewriting them as env strings would move that logic into
the callers.
- Surface: --env / MOBILEGL_TRACE_ENV in trace-replay-ci.sh, repeatable --env
KEY=VALUE in run_android_retrace_local.py, `mobilegl_env` intent extra,
Request::envOverrides.
- The two-level parse now lives in trace_env_overrides.hpp, beside the semicolon
splitter it shares with the texture and FBO dump lists, and
tools/trace_replay/trace_env_overrides_test.cpp pins it: the empty entries a
trailing ';' leaves behind must not become unsetenv(""), `K=` must stay a Set of
the empty string rather than an Unset (a knob read with getenv() != nullptr sees
those as opposite answers), and only the FIRST '=' may separate, or a value
carrying '=' is truncated without a word of warning. The whole MOBILEGL_PIPE_*
batch rides on this parse, and the only lane that exercised it end to end was an
on-device retrace, which would have reported a splitting bug as "the knob had no
effect".
- The check is built and RUN at build time and mobilegl_trace_replay depends on it,
so `cmake --build ... --target mobilegl_trace_replay` - the exact command of
test.yml's "Build trace replay" job, which never invokes ctest - runs it. It is
assert-free on purpose: that lane configures Release, and <cassert> under NDEBUG
would compile every check into a green run that checked nothing. Negative control:
swapping find('=') for rfind('=') fails 2 checks, and keeping the splitter's empty
entries fails 2 more.
|
||
|
|
8a239177ac |
[Feat] (Build, TraceApp): ship and exec a second native binary on android
- PLAN-B.md §11 P0 lists spike A (the Android delivery chain) as a P0 deliverable, inherited verbatim from PLAN.md §15 P0; §8.1 inherits PLAN.md §11.1-§11.6, whose Android path needs a second process. Android gives an application no writable exec-able directory, so the only supported route is to name the binary lib*.so, let the packager put it in lib/<abi>/, and exec it out of getApplicationInfo().nativeLibraryDir. This builds that route end to end so the spike can be answered with evidence instead of folklore. - New root option MOBILEGL_BUILD_SERVER_SPIKE (OFF, ANDROID-only) adds the MobileGLServer target from tools/spikes/server_stub/main.cpp with PREFIX "lib" / SUFFIX ".so" and -fPIE/-pie: an .so name does not exempt the file from Android's PIE requirement. Its RUNTIME_OUTPUT_DIRECTORY is pointed at CMAKE_LIBRARY_OUTPUT_DIRECTORY, because AGP packages what lands in the per-ABI library output directory and CMake would otherwise put an executable elsewhere. - The option is opt-in on both sides. The plugin flavour cannot turn it on at all, and the trace flavour builds it only when asked, with `-Pmobilegl.buildServerSpike=ON` or MOBILEGL_BUILD_SERVER_SPIKE=ON in the environment; a flavour that silently carries an executable nothing loads is the kind of thing nobody notices until it ships. Verified both ways: assembleTraceDebug -Pmobilegl.buildServerSpike=ON packages lib/arm64-v8a/libMobileGLServer.so and `file` reports "ELF 64-bit LSB pie executable, ARM aarch64 ... interpreter /system/bin/linker64, for Android 26"; the same task with no property packages only libMobileGL.so and libtrace_replay_runner.so. - The stub prints one line to stdout and writes the same line to the file named by argv[1], then exits 0. The line carries pid/ppid/uid/gid and, decisively, the child's own /proc/self/attr/current: only `u:r:untrusted_app:...` proves an ordinary app process did the exec. An `adb run-as` shell runs in a different SELinux domain, so a success there would prove nothing. - RunSpawnSpike() starts the stub with argv [serverPath, markerPath], redirects the child's stdout/stderr into a captured file (an app process has stdout on /dev/null, so a printed line would otherwise vanish), waits for it, and reports exit status, signal, the exec errno, the parent's own SELinux context, the marker content and the captured stdout - to logcat, to the returned string, and to a <marker>.report file, because the Activity finishes immediately afterwards. - The child reports the errno of a REFUSED execve through a close-on-exec pipe. Without it the one datum the spike exists to produce is lost: the parent only ever sees a wait status, in which every reason has already been flattened into one exit code, and EACCES (SELinux, or a noexec mount) versus ENOEXEC (a packager that mangled the file) are opposite verdicts for the design. A successful exec closes the write end for free, so the parent reads EOF and reports execErrno=0. - fork/execve only. The earlier draft also carried a posix_spawn arm behind `__ANDROID_API__ >= 28`, which was dead code in every configuration this repo can build - bionic declares posix_spawn from API 28 and the root CMakeLists.txt pins MOBILEGL_ANDROID_API_LEVEL to 26 and refuses to configure lower - and would have silently become the production path, untested, on a minSdk bump. Keeping the arm that actually ships means the spike measures the code the server would really use. Nothing happens between fork and execve except open/dup2/execve/write/_exit, all async-signal-safe, because the parent is a multi-threaded JVM process. - The spike lives in its own TU, spawn_spike.cpp/.hpp, listed only by the trace APK's CMakeLists. Its sibling trace_replay_core.cpp is compiled verbatim by the DESKTOP mobilegl_trace_replay runner (tools/trace_replay/CMakeLists.txt names the same file), where <android/log.h> does not exist, so nothing Android-only may live there; spawn_spike.cpp carries an #error for anyone who adds it to that list. - The Activity runs the spike, and nothing else, when launched with the `mobilegl_spike_spawn` intent extra; that mode needs no trace, no golden and no render surface. It is a separate JNI entry point rather than another parameter on the 30-argument replay call, which it shares nothing with. - Not yet run on a device: both device locks are held by another campaign. The on-device verdict is the coordinator's step. |
||
|
|
87ee17c68c |
[Docs] (Disaggregated): fold the P0 measurements and corrections into the plan
- GL_COMPUTE_WORK_GROUP_SIZE is answered by MG_Impl from ProgramObject::GetComputeLocalSize, not by a backend; only the compute limits are caps, and the two dead table entries (GetInteger64i_v, GetProgramiv) were retired in P0 - the glRenderbufferStorage OOM-probe idiom appears in 0 of 41 fixtures, so kNeedsAck is carried by glBufferStorage only - FramebufferSrgb/DepthClamp: six readers of a constant false and zero readers respectively, no fixture enables either; recorded as a decision to take before the render-state chunk table freezes - the call catalogue is 68 unique records (screen 10, ctx-query 6, CSO 13, kCtxState 17, kCtxObject 9, kCtxVerb 13); PipeCalls.def is the single source of truth and the wire opcode is a line's position - measured layouts (MGPDrawInfo head 56 B, RenderStateParameters 1168 B, ResidualValueBlock 1248 B, MGPipeContext 464 B ...), the 926/73 MG_Impl mutator surface, the first per-draw accessor numbers on lavapipe (Espryt 20.65, Magma 15.54), the EndTransformFeedback null-as-capability trap, and the host-side spike results |
||
|
|
aa005720d0 |
[Test] (MG_Remote, Wire): pin the hung-up doorbell, the wakeup that must not be eaten, the ring's capacity ceiling and the publish-then-ring handoff
- FdPassingTest.SocketDoorbellStopsParkingWhenThePeerHangsUp builds a SOCK_STREAM socketpair - deliberately not FdPassing::CreateSocketPair's datagram pair, because only a stream end reports the hangup at all - closes the notifier, and asserts Park returns false, latches Dead(), stays latched, and that a Wait with kWaitForever gives up in under a second instead of spinning. - FdPassingTest.SocketDoorbellStillDeliversTheLastRingBeforeAHangup rings and then closes: detecting death must not swallow the wakeup already sitting in the socket buffer, since the peer's last publish is the one a waiter is most likely to be blocked on. - InProcessTransportTest.AFrameWakeupIsNotEatenByAWaiterOnDescriptors blocks two readers on one endpoint with two different predicates and requires the frame to arrive within 2s rather than "eventually, when a receive timed out". - RingTest.RejectsACapacityTheRecordHeaderCannotDescribe refuses 4 GiB from both roles without mapping anything (the constructor rejects before it touches the base pointer) and keeps 2 GiB accepted as the positive control. - RingTest.DoorbellHandoffWakesBothSidesOnEveryPublish runs 2000 records through the ring with real parking in both directions, in the publish-then- NotifyIfParked order the fences assume. It cannot prove the fence pairing - no test can, since x86 has to actually hold the release store in the store buffer across the flag read - but it exercises the exact call order, and a lost wakeup surfaces as a Wait that times out with work available (a red test) rather than as a hung CI job. - Result: 1429 unit tests pass with MOBILEGL_BUILD_DISAGGREGATED=ON (1424 before this commit; the wire subset is 47), 1382 pass with it OFF, and the same three pre-existing skips appear in both. Every new case was verified to fail with its fix reverted and to pass again with it restored. |
||
|
|
c1a7ffac94 |
[Fix] (MG_Remote, Transport): give descriptor offers their own condition variable, cap the ring at what a 32-bit size field can describe, and keep the frontend umbrella out of Framing.h
- InProcessChannel::Direction served two different predicates from one
condition_variable signalled with notify_one, so a SendFrame wakeup could be
delivered to a thread blocked in ReceiveFd, which re-tests its own predicate
and goes back to sleep - leaving a queued message undelivered until some
unrelated later event. ITransport narrows the contract to one dedicated
reader thread, but that is a comment, not a mechanism, and the first caller
that splits its reader should not have to discover this. Offers now have
their own fdCv, and Close() notifies both.
- RingProducer/RingConsumer accepted any power-of-two capacity while
RingRecordHeader::size is 32-bit by wire contract (plan section 8.1 ->
PLAN.md section 6.3: 8-byte RecHeader). At 4 GiB or more a record's size -
or a wrap filler's, which is sized by the distance to the boundary - would be
truncated on the way in, and the consumer would then bounds-check the
truncated value against the real one. kMaxRingCapacity rejects that at
construction, the same class of guard as the power-of-two and
smaller-than-a-header checks beside it. Unreachable today (SEG_CMD 8 MiB,
SEG_STAGE 32 MiB), which is the point of catching it now.
- Framing.h included MG_Util/Debug/Log.h, which includes Includes.h, the GL
frontend's umbrella header: 661 headers by `clang++ -H`. It is the one header
under Transport/ that broke the rule ITransport.h states for this layer
("nothing about a byte pipe needs the GL frontend's umbrella header"), which
matters when the server-side binary links this and when the include-graph
purity gate of plan section 10.3 (gate A, asserted on -H output rather than
on symbols) lands. Its three error paths now call WireLogError, declared in a
new dependency-free WireLog.h whose .cpp owns the umbrella. Framing.h is down
to 134 headers, none of them MG_State, Includes.h or Log.h.
- Two documentation corrections. ITransport::Shutdown documented a one-sided
"releases the endpoint" while InProcessTransport::Shutdown closes both
directions - which is what closing a socket does, so the spawn transport will
behave the same way; the interface now says whole-connection teardown, and
keeps the promise that queued messages stay readable until drained.
InProcessTransport.h cited a CMake option
MOBILEGL_BUILD_DISAGGREGATED_INPROC that grep finds nowhere: plan appendix B
reserves it for the role-isolation shim, this skeleton does not add it, and
the delivery mode is a runtime choice (MOBILEGL_TRANSPORT), not a build one.
- Evidence: both configurations reconfigured and rebuilt; nm --defined-only on
the OFF build still reports 0 MG_Remote symbols and the ldd dependency set is
byte-identical to the OFF link (plan section 10.3, the two surviving
byte-level equalities). Negative controls: removing the capacity ceiling
makes RingTest.RejectsACapacityTheRecordHeaderCannotDescribe fail on both
roles; collapsing fdCv back into cv makes
InProcessTransportTest.AFrameWakeupIsNotEatenByAWaiterOnDescriptors fail at
3950ms against its 2000ms bound.
|
||
|
|
bdd4bed431 |
[Fix] (MG_Remote, Transport): close the doorbell's lost-wakeup window with two seq_cst fences and stop a hung-up peer turning every park into a spin
- The header claimed the park flag's own seq_cst store and load closed the lost-wakeup window. They cannot: that Dekker argument needs all FOUR accesses in the seq_cst total order, and the other two are not in it - the watermark publish is a release store (RingProducer::Publish) and the condition re-test is an acquire load. There was no atomic_thread_fence anywhere under MG_Remote. On x86 a release store and a seq_cst load are both plain MOVs, so the notifier can read parked==0 while its head store still sits in the store buffer, and the waiter then parks on a stale watermark forever; ARMv8 survived only because STLR->LDAR is RCsc, which is luck, not the design. Doorbell::Wait now fences after announcing and NotifyIfParked fences before reading the flag - the pairing the standard actually guarantees ([atomics.order]) - and the header says so, including the other half of the contract: publish the watermark BEFORE ringing, because a fence only orders what precedes it. This is the claim the whole P5/P6 wait discipline (present credit, kNeedsAck blocking requests, full-ring escalation) will be built on, and its failure mode is a silent cross-process hang. Inherited design, plan section 8.1 (PLAN.md section 6.2/6.2a: bidirectional doorbell, MOBILEGL_IPC_SPIN_US default 50us, condvar for inproc). - SocketDoorbell::Park treated any poll() return > 0 as a wakeup and never looked at revents. Measured on this machine: an AF_UNIX SOCK_STREAM socketpair whose peer has closed returns revents=POLLIN|POLLHUP with recv()==0 immediately and forever. Park therefore returned true, Wait stored parked=0, found its condition still false and re-parked - so a waiter with kWaitForever burned a big core at full clock with no bound. That is exactly the pathology the bidirectional doorbell exists to prevent (a whole 16.6ms frame of a big core on a phone, competing with the GPU and the game's JVM), reached from the other side. Park now branches on revents, a new Drain() latches death on EOF (and on ECONNRESET/EPIPE from Notify), and the new Doorbell::Dead() lets Wait give up instead of re-parking on a descriptor that can never deliver another wakeup. - Same commit, same defect class: `fd` is documented as one end of an AF_UNIX socket pair, not "a socket or pipe end". Notify uses send(MSG_DONTWAIT| MSG_NOSIGNAL) and Park uses poll()+recv(), which a pipe end refuses with ENOTSOCK, and a SOCK_DGRAM pair reports no readiness at all when the peer closes (measured), so the spawn transport wants SOCK_STREAM. - Evidence: build-linux-split rebuilt clean; the wire suite is green (47/47). Negative control - reinstating the revents-blind Park makes FdPassingTest.SocketDoorbellStopsParkingWhenThePeerHangsUp fail on both Park assertions, and restoring this code turns it green again. |
||
|
|
10315e71f3 |
[Test] (MG_Remote, CI): cover the wire layer with five suites and gate the generated header with flatc-check
- MobileGL/MG_Test/Wire, 42 gtest cases in five binaries, ctest label `unit`, registered only when MOBILEGL_BUILD_DISAGGREGATED is ON so the default configuration is untouched. - FdPassingTest is the one the earlier branch could not have had: it forks a child that creates a shared segment, fills 64 KiB with a pattern and hands the descriptor over SCM_RIGHTS; the parent adopts it, maps it read-only and compares every byte. Everything in Feat/CS-Delta-IPC ran in one process, which is why `out->fd = -1` survived unnoticed. It also pins that a too-small sideband buffer is refused before the datagram is consumed (so no descriptor is dropped), that an empty sideband still carries its fd, and that a receive with no offer times out. The `spawn` SocketDoorbell is covered in the same file because it rides the same kind of socket: a parked waiter woken through NotifyIfParked, a clean timeout, and a wakeup that arrives before anyone parks being remembered and then consumed exactly once. - RingTest: control-page size/alignment/cache-line layout, a non-power-of-two capacity refused, payload alignment, 200 records driven through a 256-byte ring so the wrap filler path runs repeatedly and no record ever straddles the boundary, backpressure (full ring refuses, applied alone frees nothing, retired frees), a record larger than the ring refused, the generation bump refused while records are in flight and accepted once quiesced, a corrupt header refused instead of dispatched, and a 20000-record two-thread producer/consumer run checking order, content and the final cursor equality. - FramingTest: byte-at-a-time reassembly of two frames, the magic reading "MGLF" on the wire, empty payloads, a bad magic and an oversized length each latching the reader dead (the old code hung silently instead), the send-side cap, and the buffer-too-small contract keeping the message so the retry still finds it. - InProcessTransportTest: both directions, ordering, buffer-too-small, poll and timeout, shutdown draining queued messages before it closes, a blocked receiver woken by a send and by a shutdown, the frame cap, descriptor hand-off with its sideband, and three condvar doorbell cases - a parked waiter woken through NotifyIfParked, an already-true condition that must never park, and a clean timeout. - ProtocolSmokeTest: a Hello built and read back through the committed generated header, a Welcome carrying the four segment announcements, the same buffer travelling across the transport unchanged, a truncated buffer failing verification rather than being read, and the CtrlMsg / SegmentKind / LogLevel / FatalCode tag values frozen - they are wire numbers, so if that test has to be edited the schema change was a wire break. - CI: a `flatc-check` job in test.yml that checks out only the flatbuffers submodule, builds the pinned flatc through scripts/gen_protocol.py into the runner temp directory, regenerates and runs `git diff --exit-code` on protocol_generated.h. It needs no MobileGL build, so it does not depend on build-linux (plan B section 8.1 / earlier section 7.1: the committed header plus a CI regeneration check, and no flatc in the default build graph). - Verified: `ctest -L unit` is green in both configurations - 1382 cases with the option OFF and 1424 with it ON (the same 1382 plus these 42); the nm gate is 0 matches OFF and 93 ON; regeneration of protocol_generated.h is byte-identical, and the flatc-check gate goes red on a hand-edited header and green again after a clean regeneration. |
||
|
|
bfa087d0f7 |
[Feat] (MG_Remote, Transport): land the transport skeleton behind MOBILEGL_BUILD_DISAGGREGATED - framing, the SPSC ring, doorbells, shm segments and SCM_RIGHTS
- New CMake option MOBILEGL_BUILD_DISAGGREGATED (default OFF, plan B appendix B). ON appends the MG_Remote sources to SOURCE_FILES, puts 3rdparty/flatbuffers/include on the include path and defines MOBILEGL_BUILD_DISAGGREGATED=1. OFF compiles nothing from MG_Remote and adds no include path and no library, which is one of the two byte-level equalities plan B section 10.3 keeps: measured `nm --defined-only build-linux/libMobileGL.so | grep -ic MG_Remote` = 0 with the option OFF and 93 with it ON, with an identical ldd set in both configurations.
- The option forces itself OFF with message(WARNING) when 3rdparty/flatbuffers/include/flatbuffers/flatbuffers.h is missing, so a checkout without the submodule still configures and builds rather than failing a hundred lines later on a missing header.
- ITransport.h: SendFrame / ReceiveFrame / PeekFrameSize / ShareFd / ReceiveFd / Shutdown. ReceiveFrame's contract is the fix for a defect of the earlier branch: a destination buffer smaller than the pending message returns MOBILEGL_ERR_BUFFER_TOO_SMALL with the required size and KEEPS the message queued. The earlier transport failed the call and popped the message anyway, which wedges the stream permanently the first time a reader guesses a size wrong.
- Framing.h: [u32 'MGLF'][u32 len][payload], 64 MiB cap, validated on read. A bad magic or an oversized length latches the reader dead, is logged at ERROR and makes every later call return MOBILEGL_ERR_PROTOCOL_MISMATCH. This is the second inherited defect: Feat/CS-Delta-IPC's Framing.h:41-45 Feed() always returned OK and its header peek merely returned false, so a desynchronized stream became a silent permanent hang; its LocalSocketTransport.cpp:232-236 then allocated on the peer-supplied length with no cap. The magic is also byte-ordered so it reads "MGLF" on the wire, where the earlier constant spelled "FLGM".
- Ring.h/.cpp: RingControl exactly as the inherited design (plan B section 8.1 -> earlier section 6.2): two independent cursor triples (cmd and stage, each {head, appliedTail, retiredTail}), appliedSeq / submittedSeq / retiredSeq / completedFrameSerial / presentAckSerial, serverEpoch, ringGeneration, consumerParked, producerParked, eventRingFull, eventDropped. One 4096-byte page with each contended group on its own cache line, pinned by static_assert on size and alignment and by an offset test.
- The SPSC pair uses monotonic byte cursors and a power-of-two mask, so a torn read can never look like a valid earlier position. A record never straddles the wrap: the producer emits a kPad filler to the boundary, which is always a multiple of 8 and therefore always has room for a header. The producer reclaims against retiredTail rather than appliedTail, so the day the server borrows a ring slot into the GPU timeline (kRecBorrowSlot) it does not silently degrade to early recycling. HardDrainRing bumps ringGeneration only when the ring is quiesced, and leaves the cursors monotonic so cached offsets are recognisably stale.
- The consumer bounds-checks every record header before dispatch (8-aligned, at least a header, no larger than what the producer published, contiguous inside the mapping) and reports corruption to the caller instead of dispatching into undefined behaviour. That is the runtime half of the earlier section 6.3 discipline: SEG_CMD is written by another process, so a compile-time static_assert on record sizes proves nothing about what is in the mapping.
- Doorbell.h/.cpp: spin then park, both directions (earlier section 6.2a). CondVarDoorbell for inproc, SocketDoorbell for spawn (one byte, codes 0x01 ring-advanced and 0x02 watermark-advanced) - no futex, eventfd or named event anywhere. The lost-wakeup window is closed by ordering: the waiter stores its park flag seq_cst and then re-tests the condition, NotifyIfParked loads the same flag seq_cst after the watermark is published, so one of the two always sees the other. kDefaultSpinUs is 50, the MOBILEGL_IPC_SPIN_US default.
- ShmSegment.{h,cpp} + ShmSegmentPosix.cpp: memfd_create by raw syscall on desktop Linux (the glibc wrapper is too recent to rely on), ASharedMemory_create on Android (API 26; libc's memfd_create wrapper is API 30, above MobileGL's floor), shm_open + immediate shm_unlink as the fallback. Adopt() refuses a descriptor whose fstat size is smaller than the size the peer announced, so a short segment cannot turn every later offset into an out-of-bounds map. ShmSegmentWin32.cpp (CreateFileMappingW in Local\) is compile-guarded and untested - this project's Windows machine is not a correctness gate.
- The Android path is compile-verified, not just written: an arm64-v8a NDK build with the option ON links, ShmSegmentPosix.cpp.o carries an undefined ASharedMemory_create, and ShmSegmentWin32.cpp.o is empty there. That build is also what caught the missing <cstddef> in ShmSegment.h and Framing.h, where std::size_t / std::ptrdiff_t only resolved through a transitive include on the host sysroot.
- FdPassing.{h,cpp}: SCM_RIGHTS in the FIRST transport commit, as plan B section 8.1 demands, over a dedicated AF_UNIX SOCK_DGRAM socketpair rather than the control byte stream (message boundaries survive on every POSIX - SOCK_SEQPACKET does not exist on macOS - and ancillary data can never be split from its payload). The third inherited defect this replaces: Feat/CS-Delta-IPC deferred fd passing and hardcoded `out->fd = -1` in LocalSocketTransport.cpp:296, so on the only platform that matters its data plane could not move one byte between processes. MSG_CTRUNC, an unexpected descriptor count and a malformed sideband header all close every descriptor received before failing, and a too-small sideband buffer is refused before the recvmsg so a datagram is never half-consumed.
- InProcessTransport.{h,cpp}: two in-memory queues plus the condvar doorbell pair. It keeps the frame size cap so nothing that passes in inproc becomes illegal after the switch to spawn, hands descriptors over with dup() under the same ownership rule as SCM_RIGHTS, and lets a peer's queued messages be drained after Shutdown - usually the last one says why it is going away.
- Not done here on purpose: the MOBILEGL_IPC_* environment variables are parsed in ConfigLoader.cpp, which belongs to another P0 work package running in parallel; this commit only exposes the constants (kDefaultSpinUs, kMaxFramePayloadSize, FdPassing::kMaxSidebandBytes) so that plumbing has something to set.
|
||
|
|
a1e22c26ab |
[Build] (MG_Remote, Protocol): pin the flatbuffers submodule, add the control-plane schema and commit its generated header
- 3rdparty/flatbuffers submodule pinned to the latest release tag v25.12.19 (7e163021). The runtime is header-only, so only 3rdparty/flatbuffers/include is ever used and no library is linked; CMake never calls add_subdirectory on it and flatc is not in the build graph (plan B section 8.1, inheriting the earlier plan's section 7.1).
- MobileGL/MG_Remote/Protocol/protocol.fbs carries the CONTROL PLANE only: SegmentRef, Hello, Welcome, CapsSnapshot, DefaultFramebufferInfo, SurfaceOp, SurfaceReply, ResyncRequest, ResyncDone, AuxRequest, Fatal, LogLine, union CtrlMsg and the CtrlEnvelope root with a file_identifier. Hot-path records are FlatBuffers structs generated from MG_Pipe/PipeCalls.def in a later package and are deliberately absent here, so record numbering never churns.
- Two deviations from the earlier plan's section 7.1 sketch, both deliberate: (a) ProgramReflection is not a union member - plan B ships program artifacts inside the create_shader_state CSO blob (section 8.2), and union tags are wire values that may only ever be appended, so reserving a tag for a message that may never exist is worse than appending one later; (b) maxComputeWorkGroupCount/Size are vectors, not [int:3] - fixed-size arrays are legal only in FlatBuffers structs, never in tables.
- scripts/gen_protocol.py resolves flatc as MOBILEGL_FLATC_EXECUTABLE, otherwise builds the pinned flatc ONCE into <repo>/../flatc-build (override with MOBILEGL_FLATC_BUILD_DIR), outside the project build graph. A flatc found on PATH is deliberately refused and a version mismatch against the pinned runtime is a hard error: the generated header static_asserts FLATBUFFERS_VERSION, so a stray flatc either fails to compile or churns the committed file on every machine. The earlier branch did the opposite - Protocol/CMakeLists.txt:22-38 add_subdirectory'd the FlatBuffers tree with FLATBUFFERS_BUILD_FLATC=ON whenever MOBILEGL_FLATC_EXECUTABLE was unset, which is exactly the NDK trap it claimed to avoid (cross-compile an arm64 flatc, then run it on the host).
- protocol_generated.h is committed with the project source header prepended by the generator, so regeneration is byte-identical: verified by running gen_protocol.py twice and by perturbing the file and regenerating it back.
- Reuse from Feat/CS-Delta-IPC: MobileGL/Protocol/mg_protocol_base.h, kept as the shared C vocabulary (result codes, byte spans, shm region, id typedefs) and keeping the structSize-first versioning discipline that section 14.2 calls the answer to risk B-R10. Dropped from it: MobileGLObjectKind / MobileGLObjectScope / MobileGLObjectHandle - plan B never puts GL object identity on the wire (the frontend allocates {slot, generation} handles in MG_Pipe, section 4.2.1), so a second identity vocabulary would be a drift surface with no reader. Added MOBILEGL_ERR_BUFFER_TOO_SMALL as an append-only code for the receive contract.
|
||
|
|
bd2b4158e0 |
[Fix] (DirectVulkan, State): key the transform-feedback counter slots on the object's identity, not on its recycled GL name
- VulkanRenderer::CurrentXfbCounterSlot keyed m_xfbCounterSlotByObject on
GetBoundTransformFeedbackName(). glGenTransformFeedbacks hands a deleted name
straight back (IndexGenerator is LIFO) and nothing ever removed a map entry, so
a transform feedback object created on a recycled name was served the DEAD
object's counter group - and with it that group's m_xfbCountersValid and
m_xfbLastSeenGeneration entries, which are the resume/fresh decision for
vkCmdBeginTransformFeedbackEXT. This is D21 in plan B v2 4.7.3, the one entry
in that table whose today-key "guards nothing", and 10.4-5 asks for it to land
on dev on its own - hence this separate commit, kept in files no other commit
on this branch touches so the cherry-pick applies unaided.
- Frontend: TransformFeedbackObjectState gains a never-reused `lifetimeId`
through a default member initialiser, so every route into existence
(operator[] materialisation, `= {}` in GenTransformFeedbackNames and
CreateTransformFeedbackObject) mints a fresh one and a recycled name cannot
carry the dead object's id back. The allocator is the same shape as
BufferObject::AllocateLifetimeId (atomic, starts at 1 so a zeroed backend slot
is never a live object).
- The bound object's id is mirrored in m_boundTransformFeedbackLifetimeId,
refreshed by RestoreBoundTransformFeedbackState - which every bind, and the
revert that deleting the bound object performs, goes through - and seeded for
the default object by the GLContext constructor. GetBoundTransformFeedback
LifetimeId is therefore a const load. Reading it through operator[] instead
would have been an INSERT on the per-draw path, and UnorderedMap is
ska::flat_hash_map, whose rehash invalidates every reference into the
container, not just its iterators.
- Backend: the UnorderedMap is replaced by a fixed 16-entry owner table, which
fixes the second half of the same defect - the map was keyed on a value that
recycles yet was never pruned, so it grew for the life of the context. With
lifetime ids as keys a map would have grown without bound instead, so the
bounded table is required, not cosmetic.
- Slot exhaustion: past sixteen owners a group has to be taken over, and the
victim is chosen among owners with NO OPEN SPAN, which
GLContext::HasOpenTransformFeedbackSpan answers; an identity no live object
carries any more answers false, and that is what lets a dead owner's group
come back. Least-recently-used ALONE would have been exactly the wrong rule:
GL only permits another object to capture while this one is PAUSED, so the
paused span these groups exist to protect is by construction the least
recently used entry, and an LRU takeover would reset the one resume offset
that still matters. LRU is now only the tie-break among reclaimable groups.
Sixteen genuinely open spans at once is reported (MGLOG_E_ONCE) rather than
resolved silently, because whatever is taken then restarts at offset 0.
- Not done, and why: the natural place to hand a group back is
glEndTransformFeedback, but registering DirectVulkan's EndTransformFeedback
table entry would flip the test FixupGsStripCaptureOrder makes of that same
pointer (GL_Drawing.cpp:1255) to decide whether the backend already captured
in GL's vertex order, silently disabling the geometry-stage strip fixup for
DirectVulkan. Giving that discriminator a name of its own is a separate
change; until then the no-open-span rule is what keeps the table honest.
- CurrentXfbCounterSlot asserts the identity is never 0. Zero is the free-slot
sentinel, so an identity of 0 would match every free slot as "mine" without
ever claiming one - this bug reintroduced, with no symptom at the call site.
- TransformFeedbackLifetimeIdTest, in its own translation unit, pins the
frontend halves: an object created on a recycled name must not report the dead
object's id, the default object has an identity before anything binds it, and
a PAUSED span still reads as open while another object is bound and capturing
- which is the whole correctness argument for the eviction rule. The name
reuse is not simulated: the test asks the real generator and skips (loudly) if
it never recycled. Still untested: the >16-owners path itself, which needs a
backend scenario with seventeen capturing objects and there is none.
- Negative controls, each applied then reverted: making a non-bound object's
span read as closed reddens APausedSpanStaysOpenWhileAnotherObjectCaptures;
making a vanished identity read as open reddens the same case on its delete
assertion; dropping the constructor's seeding reddens
AnObjectAtARecycledNameCarriesAFreshLifetimeId.
- Tested: cmake --build build-linux -j 24 (clean, 166 targets); ctest -L unit
-j 12 -> 1386/1386 passed; ctest -L integration-gpu -> 866/866 passed
serially, and 866/866 on one of two -j 8 runs. The other -j 8 run failed
DirectGLES.PointSizeDemotionScenario.TheDemotionIsActuallyArmedWhenTheEnviron
mentPinsItOn, a member of the pre-existing parallel-ctest flake family: it
passes in isolation here, and the unmodified parent tree
(~/w7/p0-noop-wins-base) reproduces the same family under -j 8.
|
||
|
|
9c7339b214 |
[Feat] (State): give RenderbufferObject the never-reused lifetime id every other cache-keyable state object already has
- RenderbufferObject was the last state object a backend twin registry keys on that could only be identified by its heap address or its GL name - both of which recycle. BufferObject, VertexArrayObject and ProgramObject all carry a process-wide, never-reused id for exactly this; the renderbuffer's absence is named in plan B §10.4-5 as one of the two latent problems P0 closes. - Mirrors BufferObject.h:202-208 / BufferObject.cpp:19-24 verbatim in shape: a private static AllocateLifetimeId() over a namespace-scope std::atomic<Uint64> starting at 1 (so a zero-initialised memo slot can never carry a live object's id), a const member initialised from it at construction, and an inline const getter. The doc comment is the buffer one restated for the renderbuffer's own recycling sources. - Deliberately NO GetVersion(): plan B §11 P0 says the id only. A mutation counter would be a second, independent invalidation surface to keep correct, and nothing needs one yet - the renderbuffer's mutable content already reaches the backends through AllocateStorage / SetInternalFormat / SetSamples. - No caller yet, by design: the id exists so §4.7.3's D1 rekey (and the DirectGLES renderbuffer twin registry at Managers.h:1858) has something to key on. It is a pure addition - no existing field, signature or answer changes. - ObjectLifetimeIdTest gains the two cases the other two object types already have, so the renderbuffer is covered by the same allocator-reuse probe: an object rebuilt at a freed address must not answer to the dead one's id, and two live ones must differ. - Tested: cmake --build build-linux -j 24 (clean); ctest -R ObjectLifetimeId -> 6/6 passed (4 pre-existing + 2 new). |
||
|
|
50815a232e |
[Fix] (Backend, Getter): retire the two frontend queries that were never asked and strip the unreachable frontend arms from the third
- GLFunctionsTable had three entries that are frontend queries wearing a
backend interface (plan B v2 §2.1(a)): GetIntegeri_v, GetInteger64i_v and
GetProgramiv. Two of them have NO caller at all - `grep -o
'gBackendFunctionsTable\.GL\.[A-Za-z_0-9]*'` outside MG_Backend/ lists 69
distinct entries and neither GetInteger64i_v nor GetProgramiv is among them -
so both table slots, both backends' implementations and both registrations are
deleted here. This is plan B §11 P0's first "strictly no-op free win".
- Nothing was moved into MG_Impl, because MG_Impl already answers all of it.
glGetInteger64i_v is served by GL_Getter.cpp:1240-1314, which handles the
indexed buffer queries itself and derives every other pname from its own
GetIntegeri_v ("Handing the leftovers straight to the backend instead made
glGetInteger64i_v disagree with glGetIntegeri_v on the very same pname").
glGetProgramiv is served by GL_Program.cpp, whose GL_COMPUTE_WORK_GROUP_SIZE
arm (:928-946) reads ProgramObject::GetComputeLocalSize - a link artifact of
the program the APPLICATION wrote, which is the only program in the
application's namespace. §4.7.1 class B.
- GetIntegeri_v stays, but only for what a backend genuinely owns. Its pure
frontend arms were unreachable: GL_Getter::GetIntegeri_v answers
GL_SHADER_STORAGE_BUFFER_{BINDING,START,SIZE} through
TryDecodeIndexedBufferQuery (:991-1029) and the six GL_IMAGE_BINDING_* pnames
at :1115-1153, and returns before touching the table. That left 9 dead cases
in DirectGLES.cpp and 9 in DirectVulkan.cpp - the plan's "15" undercounts the
two files separately. What still arrives is GL_MAX_COMPUTE_WORK_GROUP_COUNT /
_SIZE (GL_Getter.cpp:1161-1177, and MG_Util/ShaderTranspiler/CompileEnv.cpp
:134-138 asks the table directly), so DirectVulkan keeps exactly those two and
DirectGLES becomes a plain driver passthrough.
- The dead arms were also WRONG, which is why deleting rather than reconciling
them is the strict no-op: they clamped a bound range's size to the buffer's
current storage, while the frontend reports the size glBindBufferRange was
asked for verbatim (GL 4.6 core tables 23.4/23.5 - the clamp answered 0 for
KHR-GL43.shader_storage_buffer_object.basic-binding's shape). Had a later
refactor made the table the answer, the regression would have been silent.
- ProgramResourceCache::computeWorkGroupSize and the spirv-reflect entry-point
loop that filled it go with DirectVulkan's GetProgramiv; nothing else read it.
- Three cases added to AdvertisedLimitsScenario pin what the frontend answers,
on both lanes: the indexed SSBO binding/start/size on the 32- and 64-bit
widths INCLUDING a shrink of the store underneath the binding (the arm that
actually separates verbatim from clamped), the six image-unit pnames on both
widths, and the compute local size plus the INVALID_OPERATION a program with
no compute stage must give.
- Both new gates were shown to go red for their reason: making
GL_COMPUTE_WORK_GROUP_SIZE answer a defaulted (1,1,1) fails
ComputeLocalSizeComesFromTheLinkedProgram on both lanes, and re-introducing
the deleted store clamp in GL_Getter fails
IndexedBufferBindingsAreReportedVerbatimOnBothWidths on both lanes.
- Tested: cmake --build build-linux -j 24 (clean); ctest -L unit -j 12 ->
1382/1382 passed; ctest -R AdvertisedLimits -> 18/18 passed (6 pre-existing +
3 new, x DirectGLES and DirectVulkan).
|
||
|
|
d380a01f32 |
[Fix] (Metrics, DirectGLES, DirectVulkan): stop the summary line printing window totals under a per-frame label, and wire the six staging paths the site inventory claimed were covered or absent
- A window with no Present divided by a faked 1 and printed the window TOTALS under
"bytes/f[...]": the *MultiDraw* slice (47 draws, no present) reported 1,404,550 bytes as a
PER-FRAME figure, a 47x overstatement of exactly the SEG_STAGE sizing input plan B section
8.2 / section 11 P0 asks this package to produce. FormatWindowLine now relabels the bracket
to "bytes[...]" and prints draws/f=n/a when the window holds no frame; acc/draw=n/a follows
the same rule, because "0.00" beside a non-zero acc= is the same lie. Pinned by
PipeStatsTest.SummaryLineSurvivesZeroFrames and the reworked SummaryLineSurvivesZeroDraws.
- Every per-frame and per-draw field now goes through one FormatFixed2 helper. draws/f read 1
for 26 draws over 14 frames (1.86) and buf read 97 for 1360 bytes (97.14): a systematic
downward truncation of up to a whole unit on the figures the package exists to produce.
Pinned by PipeStatsTest.PerFrameFieldsKeepTwoDecimals.
- FormatSummaryLine rewrote the window bases as a side effect of formatting, so any second
reader silently zeroed the next window. Split into a pure FormatWindowLine() and an explicit
AdvanceSummaryWindow(); EmitSummaryLine calls both. Pinned by
PipeStatsTest.FormattingTwiceDoesNotConsumeTheWindow.
- Init() called ResetForTesting(), against the header's own "not used by any shipping path".
Both now forward to an internal ResetCounters().
- TracyPlot published only the MISS half of each gate under the gate's unqualified name, so
the headline output channel of section 11 P0 carried no denominator. Two series per gate now
("...-hit" / "...-miss") from static literal arrays. The payload histogram stays unplotted
and says why: it is a run-total distribution over draws (section 4.5.7), not a per-frame
scalar, and it reaches the operator through the JSON dump.
- GetOrCreatePipeline's 15-call tally sat ABOVE the list-topology primitive-restart refusal,
so a declined draw added ten reads it never made - an OVER-count, which breaks the lower
bound contract every other tally keeps. Moved to immediately before the payload build, and
the enumeration reconciled with the constant: the excluded read is the sample-shading
capability, short-circuited by m_sampleRateShadingFeatureEnabled.
- Six real staging paths were uncounted while the inventory claimed coverage. The inventory
claim "DirectVulkan's own buffer staging ... has no second copy to count" was simply false.
Now wired: MultiDraw.cpp's UploadScratch/UploadScratchRing (indirect commands, the compute
tier's draw-info array, the rebased index stream - the class is passed in, so a new tier
cannot forget it); Managers.cpp's pool-recycle reseed and the VBO-backed Float64 narrowing;
VkBufferManager's eight host->device copies of buffer contents plus UploadTransient, the
single chokepoint for Magma's per-draw vertex/index/indirect staging; VkTextureManager's
packed staging slice, with the same box/rect SHAPE split Espryt already reported.
- New byte class stage-indirect-cmd, for draw PARAMETER bytes a backend synthesises and
stages. Kept out of stage-index-client because these are the population that becomes MGPipe
command-record payload (section 4.5.7), not resource bytes. A name addition, not a rename:
no recorded baseline is invalidated.
- stage-ubo-named moved past the zero-copy direct-bind decision in ResolveUniformBufferPayload
(a direct bind repacks nothing, so counting it there reported a copy that never happened),
and Magma's default-uniform-block image now feeds stage-ubo-global the way Espryt's does,
counted after the per-frame slice memo.
- The site inventory in PipeStats.cpp is rewritten to name every unwired path by file and
function. An inventory that overstates coverage is worse than a missing counter, because
the zero is then read as an answer.
- Evidence, lavapipe/llvmpipe, MOBILEGL_PIPE_STATS=1: the MultiDraw slice now prints
"window=0 draws/f=n/a bytes[buf=1404550 ...]"; the indirect tiers
(MOBILEGL_ESPRYT_MULTIDRAW_MODE=indirect|multiindirect) move icmd 0 -> 660; Magma's GuiBatch
line moves from buf=0 tex=0 ubog=0 to buf=685.71 tex=41.14 ubog=157.71 with tex[emit=9
box=9 rect=0 jobs=9]. Off-path A/B against the base tree with the env unset, 9 runs each of
a 40-scenario draw slice, sorted totals in ms: Espryt base 389..794 (median 399) vs branch
384..403 (median 394); Magma base 406..472 (median 410) vs branch 408..761 (median 414) -
the always-compiled guard is below this harness's noise on both backends.
- 1410 unit tests green (15 PipeStatsTest). integration-gpu 860/860, 860/860, 859/860; the one
failure is DirectVulkan.PointSizeDemotion...TheDemotionIsActuallyArmedWhenTheEnvironmentPins-
ItOn, a member of the load-dependent *IsActuallyArmed* flake family already present in the
untouched base tree, and it passes 8/8 standalone here. stdio gate and gen_pipe check green.
|
||
|
|
7566a0b002 |
[Feat] (DirectGLES, DirectVulkan): instrument the six memo gates, the staging byte paths and both Present hooks with the MGPipe counters
- The sites of plan B section 2.3.1, verified against dev@81b17c0b (the plan's own line numbers for DirectGLES drift by 4-9 lines; the DirectVulkan ones are exact): SyncRenderState is DirectGLES.cpp:1994 with the version read at :1998 and the early-out at :2007-2010 (plan says 2003 / 2007 / 2016-2018); SyncNeccessaryTextures at :1511 (plan :1520); CurrentUnitBindingsEpoch at :1412-1435 (plan :1418-1436); PrepareForDraw at :2907-2968 (plan :2916-2976); the global-UBO upload at :3355-3397 (plan :3369-3392); TrySetupDrawFastPath :5994, GetOrCreatePipeline :4948-4993, ApplyDynamicDrawStateTail :5871-5893 and UniformManager ResolveUniformBufferPayload :2022/:2052 all as cited. - Accessor counting is STATIC TALLIES at ten hot entry points, not a wrapper around the 293 pGLContext-> sites: each instrumented function adds the number of accessor calls its own body made on the path taken. Reads inside callees, and every conditional read (the sRGB capability in SyncRenderState, the XFB probe and the version-gated parameter fetch in TrySetupDrawFastPath, the cull-mode/logic-op/tessellation reads in the pipeline payload builder) are excluded, so the number is a consistent LOWER bound. The full inventory of what is and is not counted is the header comment of PipeStats.cpp. - The Magma fast-path gate is counted from SetupDraw, not from inside TrySetupDrawFastPath: that function has 27 decline returns and one success return, and counting at the caller is the only shape that cannot miss one. - The texture upload counts the SHAPE (union box vs N-rect list) separately from the bytes, because SSIM is blind to the shape and the +6 ms/frame Mali regression of section 7.3 was a shape regression, not a byte one. - Frame boundary: DirectGLES::Present after the ring upkeep, and DirectVulkan's backend Present rather than VulkanRenderer::Present - the latter has an early return for the no-usable-swapchain case, and a suspended frame is still a frame the counters close. - Measured on lavapipe/llvmpipe with MOBILEGL_PIPE_STATS=1, GuiBatchScenario, 14 frames and 26 draws: Espryt 20.65 accessor calls per draw (gates ers 22/18, etl 71/9, eub 71/9), Magma 15.54 (mfp 12/14, mpm 0/14, mdt 12/14). Both land inside the 10-25 band section 2.3.1 predicted and far below the 124/169 static counts, which is the correction that section was written to force. |
||
|
|
42e0f47ebb |
[Feat] (Metrics): land the MGPipe boundary counters - bytes, dynamic accessor calls and the six memo gates, off unless MOBILEGL_PIPE_STATS is set
- Plan B section 11 P0 asks for TracyPlot per-frame counters on both sides of the boundary, and section 2.3.1 adds the deliverable v1 did not have: DYNAMIC call counters. The static call-site counts everyone quotes (Espryt 124 / Magma 169) are not the per-draw cost, because every one of those paths is memo-gated; without a dynamic counter P2's verdict stays a guess. The tree had no per-frame byte or call measurement at all - MG_Util/Metrics is format arithmetic, and Tracy has zones but no plots. - Cost when off: g_pipeStatsEnabled is a plain global Bool latched once in Initialize() right after MG_ConfigLoader::Init(), and every counting site is `if (PipeStats::Enabled()) ...` - one load of a hot global and one never-taken branch. The counters are relaxed atomics because buffer and texture staging are reachable from more than one thread; the off path never touches them. - Eight byte classes (stage-buffer, stage-texture, stage-ubo-global, stage-ubo-named, stage-vertex-client, stage-index-client, persistent-map-push and the residual-value-block placeholder of section 6.3), six call classes and the six memo gates of section 2.3.1, each as a hit/miss pair. Names are minted now, including the two that stay 0 in P0, so no recorded baseline is invalidated by a later rename - which is why the name set is pinned by a test. - Reporting: TracyPlot per counter per frame when TRACY_ENABLE; with MOBILEGL_PIPE_STATS=1 one MGLOG_I summary line every 120 frames and at teardown, plus a JSON dump to MOBILEGL_PIPE_STATS_FILE when that is set. MGLOG_I against the usual "MGLOG_D for non-critical" rule on purpose: the line has to survive an INFO build - the only build a device runs - it is at most one line per 120 frames, and it exists only when the operator asked for it. - Summary lines report disjoint WINDOWS, not run totals: a run total over a workload that changes shape (load, then steady state) averages away the very number section 2.3.1 wants an absolute value for. - MOBILEGL_PIPE_STATS_FILE joins the six switches parsed in e48a3582; like them it needs no allow-list entry because InitializeAcceptedEnvVariables accepts every MOBILEGL_-prefixed variable by construction. |
||
|
|
9bbf71990c |
[Build] (CI): add the pipe-gen check, the stdio-instrumentation gate and the citation lint
- Three P0 gates from plan B section 11, in one job that needs no build: a broken build must not be able to hide a drifted interface. - pipe-gen-check regenerates G1-G7 and runs git diff --exit-code over MobileGL/MG_Pipe/generated. Since all seven generators read the same .def files, this is what makes drift between them impossible rather than merely unlikely. - The stdio gate refuses fprintf(stderr and printf( under MG_Backend and MG_State. Nothing there matches today, so it lands with NO whitelist - verified with a negative control that fprintf(stderr and std::printf both trip it while snprintf does not. Per-draw instrumentation has been committed by accident before, once inside a mutex critical section, and MGLOG_D is the channel these trees are allowed to use because it compiles out in INFO builds. - scripts/gen_pipe_dirty_surface.py reports the frontend mutation surface corollary 4 needs covered: 926 mutator calls under MG_Impl/GLImpl over 73 distinct mutators, of which only 92 sit in a function that also reaches the backend. The other 834 are published by the NEXT verb, which is precisely the population that needs an aggregate generation. Informational in P0; it becomes a gate in P1 when there is a mapping file to diff against. - scripts/check_doc_citations.py resolves every `path:line` citation against a git revision. It reproduces the failure that motivated it - the plan's first draft cited SamplerObject.h:468-492 in a 160-line file - and reports 84 unresolved citations out of 1028 in docs/Disaggregated today, which is why CI runs it warning-only until those documents settle. --strict exits 1, verified in both directions. |
||
|
|
2f8d0f0d51 |
[Feat] (Config): parse the six MOBILEGL_PIPE_* switches
- Appendix B of plan B. All six default to today's behaviour: PipePush 0 is "pull everything", verify and stats off, legacy memos ON so the first handle waves keep a real old-versus-new arm (B-R16), texel retain 0 because MipmapStorage already holds a complete CPU shadow so that cache buys latency and never correctness (section 7.5c), index mirror 64 MiB. - No allow-list edit is needed and the header says why: InitializeAcceptedEnvVariables accepts every MOBILEGL_ / LIBGL_ prefixed variable found in the environment, so a name with that prefix is visible to these queries by construction - the failure mode of a hand-maintained accepted list does not exist here. - PipeLegacyMemos defaults ON, so it is read as a tri-state (QueryEnvQuirkOverride != ForceOff) rather than as a plain truthy flag: unset must keep the memos and only an explicitly falsy value may drop them. Reading it with QueryEnvFlag would have inverted the default silently. - QueryEnvUint64 is added alongside QueryEnvUint32 for the subsystem bitmask, accepting a 0x prefix - a bitmask written in decimal is unreadable - and warning and falling back to the default on anything unparseable, exactly like its 32-bit sibling. |
||
|
|
3363258908 |
[Test] (MGPipe): pin the catalogue arithmetic, the wire opcodes and the comparator
- Thirteen cases that need no GL context and no driver, so the fact that PipeCalls.def and the seven generated files agree is checked on every unit run rather than at review time. - The catalogue count is one fact stated three times - the macro expansion, MGP_CALL_LIST_DOCUMENTED_COUNT and the two generated tables - and the per-class counts documented in the .def header are asserted individually, which is what caught kCtxState being 17 rather than 18 (set_texture_params carries kCtxObject, per the plan's own example in section 4.1). - An uninstalled pipe must be all-null: that is what "this subsystem has not been migrated, keep pulling" means (section 4.1), so it is asserted rather than assumed. - The comparator test pins the two properties the verify harness depends on: the FIRST differing field is named, and padding is not a field - two payloads that differ only in padding compare equal, while a nested array element does not. - The poison test pins the per-verb behaviour a bitmap cannot express: a field filled for verb N is stale at verb N+1. |
||
|
|
9c773182bb |
[Feat] (MGPipe): land the interface skeleton - the complete call catalogue, the payload PODs and the seven generators
- Plan B section 4 makes the frontend/backend boundary explicit and gives the backend its
own state machine. This is the P0 deliverable of section 11: the whole catalogue exists
from day one, placeholders included, because the wire opcode is a call's position in
PipeCalls.def and record numbering must never churn.
- MG_Pipe/PipeCalls.def is the single source of truth: 68 unique calls as
X(Name, Payload, Class, Flags). Its header reconciles that number with the plan's
headline counts (section 4.4, appendix A), which double count - "CSO 15" names
bind_sampler_states and set_sampler_views that the "set_* 17" list also names, "screen
14" tabulates the query family that section 4.3 assigns to the context, and the
"transfer 12" row enumerates 11 calls. Each reconciliation is written down next to the
count rather than resolved silently.
- MGPipeHandles.h: the 8-byte {slot, gen} pair, dense per-kind slots, the reserved null
and default-framebuffer handles, and the ShaderCso composite band (sections 4.2, 5.6.3).
The two generations are documented as strictly separate, with the interface rule that no
call may require the client to know MGGen.
- MGPipeTypes.h: every payload of section 4.5 as a flat POD with explicit padding, a
trivial-copyability assertion and an exact sizeof assertion, because the wire records are
memcpy'd and a field silently changing width is a protocol break no test would see.
MGPCaps embeds DynamicBackendParameters by inclusion so a caps field added there needs no
second edit here; its assertion is stated as a composition because that struct still
carries SizeT. ResidualValueBlock is pinned at MGL_RESIDUAL_BLOCK_SIZE 1248, the ratchet
that only ever goes down and reaches static_assert(... == 0) in P13 (section 6.3).
- MGPipeHostSpan.h keeps the one shape that changes with the transport isolated behind one
predictable branch, with the kFromServerIndexMirror sentinel D-B7 needs.
- MGPipeCallbacks.h names the reverse channel as ten callbacks plus the forward terminator
in the context table, replacing 95 poke sites across 17 methods (section 7.1).
- scripts/gen_pipe.py runs G1-G7 off those .def files. G1 asserts each table is EXACTLY its
call count of function pointers; G3 pads every wire record to the stream's 8-byte
granularity and checks size >= sizeof && size <= remaining && size % 8 == 0 before
dispatch, fatally; G4 compares field by field (padding excluded, floats by bits) because
a comparator with false positives is one nobody reads - DirectGLES.cpp says the same
thing about its own memcmp of RenderStateParameters; G5 turns the accessor list into
per-verb poison generations rather than a written-once bitmap, which is the only version
that can see a field left over from the previous draw (section 6.2.2); G6 joins the 477
read points of the vendored backend_read_inventory.md against Coverage.def and reports
0 UNMAPPED (299 to a call, 167 signatures that become handle parameters, 6 reverse
channel, 5 client-resolved); G7 pins the pipeline subset BY MEMBER NAME from what
VulkanRenderer::ComputePipelineStateHash hashes today, computing no offsets in python.
- The generated files are committed so the build never depends on python; CI regenerates
and diffs them.
|
||
|
|
8349babe90 |
[Docs] (Disaggregated): consolidate into a single MGPipe plan and drop the replica plan
- docs/Disaggregated/PLAN.md is now the one plan: the MGPipe design with the transport, control-plane, present, threading, EGL/process, monolith/build chapters inlined as real chapters (7-13) instead of references, sections renumbered 0-17 + appendices, every internal citation updated - the replica-GLContext plan and its review record are removed at the user's request; REVIEW.md is the MGPipe design-competition and adversarial-review record only, with the comparison verdicts dropped and the remaining finding text reworded to the new section numbers - day-43 GO/NO-GO now names its two outcomes (continue / shrink to headless tooling or re-evaluate) without any rollback path |
||
|
|
1794ac94b1 | [Docs] (Disaggregated): land plan B - MGPipe, a gallium-style explicit frontend/backend interface with a backend-owned state machine (client-allocated {slot,gen} handles, CSOs keyed on the pipeline subset with dynamic state pushed separately, set_* catalogue derived from the twins the two backends already keep, per-verb validate with aggregate generations, reverse channel as ten named callbacks plus a pull terminator, strangler migration behind PipeInputs with per-verb poison and a field-wise verify harness, three purity gates replacing byte identity), the item-by-item comparison against the replica plan A (memory, copies, drift surface, monolith benefit vs 4x effort and 7x later first frame) with a day-43 GO/NO-GO hedge, and the round-2 design-competition and adversarial-review record; mark plan A as superseded except for its inherited transport sections | ||
|
|
8b31de2f8d | [Docs] (Disaggregated): land the two-process split plan - server runs the unchanged backends against a replica GLContext driven by mutator replay, the client is the sole originator of every implicit-publish semantic (persistent-map push, MarkGpuWritten, texture dirty clear, XFB accounting, generated-mip allocation), SPSC shm ring with FlatBuffers structs on the hot path and tables on the control socket, two-way doorbells, present credit 1, spawn-only shipping build with an inproc CI variant, and a P0-P9 schedule gated on the existing unit/integration/retrace/CTS suites; plus the design-competition and adversarial-review record | ||
|
|
81b17c0b75 | [Test] (ShaderTranspiler, Integration): pin the five reworked shapes - the read-only capture stage through the real link, the seeded carrier's ESSL declaration, the metadata-driven block redeclaration and its decline, the control-stage-less evaluation decline, and a carrier clearing an i64vec4 | ||
|
|
d1edf765f5 | [Fix] (Link, Async): carry the resolved gl_PointSize capture request into the SPIR-V handoff and let a deferred verdict name its own severity - the demotion read the request off a reflection slice phase A never fills it into, so its forced carrier was dead code, and its decline reason replayed at a level no shipped build keeps | ||
|
|
97e07190ac | [Fix] (ShaderTranspiler): seed a forced point-size carrier nothing writes and decline a control stage whose live clip/cull distance would still print gl_PointSize - an ES front end deletes a never-written output, and SPIRV-Cross redeclares that block from member decorations rather than access | ||
|
|
a4dcdf989e | [Fix] (ShaderTranspiler): stop the point-size carrier landing on a location a live varying owns, and decline the evaluation stage a synthesized pass-through control stage cannot feed - an i64vec4 counted as one location, and a located input carrier trips both backends' pass-through guard | ||
|
|
1c113e4b26 | [Fix] (ShaderTranspiler): count a 64-bit integer vector as two locations in the XFB flattener's LocationSpan - it read 64-bitness off the element's float type alone, so an i64vec3/4 member packed the members after it onto locations it already owns | ||
|
|
bf9cfb3079 | [Test] (Integration): PointSizeDemotionScenario - the demoted value chain is client-invisible in both configurations, with pinned per-backend lanes and a log-latch arming guard | ||
|
|
e1818d497a | [Test] (ShaderTranspiler): pin the point-size demotion's shapes - capability stripped, carriers named and located past the program's varyings, byte-identical no-ops, whole-struct-copy decline, and the two new L1 key bits | ||
|
|
d7f66722d1 | [Fix] (ShaderTranspiler, Link, DirectGLES, DirectVulkan): demote tessellation/geometry gl_PointSize to an ordinary varying where the device cannot host the built-in - the value survives for gl_in reads and by-name capture, both backends' declines stay for shapes the pass refuses, and the verdict rides the L1 key | ||
|
|
92dc41ebf9 | [Test] (DirectVulkan, SelfTest): pin the probe's fence-timeout teardown against a fake driver, the never-worse arming refusal, and paused-span patch/instanced counting against an unpaused control | ||
|
|
feea131d8b | [Fix] (DirectVulkan, SelfTest): honour the primitives-generated probe's leak-on-timeout contract in both of its callers, count paused-span draws the frontend cannot price, refuse a substitute measured worse than the stream query, and derive the POST row's failure clause from the measurement | ||
|
|
19f4402fbf | [Test] (DirectVulkan): pin the primitives-generated probe's verdict and override mapping, and hold the reroute's GL answers and arming observable on the integration lane | ||
|
|
1350031368 | [Fix] (DirectVulkan): count GL_PRIMITIVES_GENERATED for transform-feedback-inactive draws - a bring-up probe measures the silent stream query and reroutes such draws through the dedicated primitives-generated query or a clipping-statistics pool, reported from the Vulkan POST | ||
|
|
0ee3384b22 | [Fix] (Espryt): land a SubData into an adopted store as a GPU-ordered copy - the in-place coherent write tore the frames still reading the old section bytes | ||
|
|
ba3f8d6774 | [Test] (Integration): pin the adopted mesh-arena store - cross-frame SubData visibility, readback identity, and the GPU-written readback | ||
|
|
3327784fd0 | [Fix] (Espryt): adopt mesh-arena-sized stores into coherent persistent maps at definition, and stop the flush tiers from re-synchronizing them | ||
|
|
ff426da3a9 | [Fix, Test] (MG_State, DirectVulkan): order host writes to an adopted store after recorded GPU work - a SubData issued after a dispatch landed in coherent memory before the deferred dispatch executed, so its increments overwrote the newer bytes; un-skip the DirectVulkan half of the SubData-after-dispatch scenario | ||
|
|
08419a1fe6 | [Chore] (Config): point the two stale comments at the renamed feature fields | ||
|
|
5d51372c44 | [Chore] (Config): triage backend-scoped env toggles under MOBILEGL_ESPRYT_ and MOBILEGL_MAGMA_ prefixes | ||
|
|
7fd4550968 | [Fix] (Espryt): copy before any FBO attach in the packed16 probe - attaching the array relayouts it to the plain order and was neutralizing the subject | ||
|
|
971537058e | [Fix] (Espryt): probe every allocation recipe of the packed16 shape - the Mali layout heuristic inverts between contexts, so one recipe cannot speak for the storage | ||
|
|
dd98c450ad | [Test] (SelfTest): model the params-before-upload escape so a params-first probe regression reads as a red test | ||
|
|
5dbbbbd7eb | [Fix] (Espryt): allocate the packed16 probe's textures uploads-first, the order a minted backend texture performs and the one the Mali layout choice keys on | ||
|
|
5d140a41ce | [Fix] (Espryt): re-arm the packed16 probe on the allocation-scoped mirror the device actually has and let any mirrored level trigger the widening |