Completes Campaign V slice V2 by moving both particle render paths -
billboard (particle.vert/.frag) and mesh-emitter (particle_mesh.vert/.frag) -
from raw 64-bit ARB_bindless_texture handles to binding=9 handle-table
indices, matching V2a's mesh path and V2b's terrain path.
Particles differ from both prior sub-slices in HOW the handle reaches the
shader:
- Billboard particles carry it as a per-INSTANCE vertex attribute (not an
SSBO batch or a per-draw uniform), because each particle can use a
different texture within one instanced draw. aTextureHandle (location 6,
uvec2) became aTextureIndex (uint); particle.vert looks it up via
ACDREAM_TEXTURE_HANDLE and reconstructs the SAME uvec2 into vTextureHandle
exactly as before, so particle.frag - including its zero-handle check for
the procedural circle fallback - needed no change at all. The per-instance
ABI struct BillboardGpuInstance shrank by 4 bytes (one uint slot instead of
two uint handle halves); ParticleBindlessInstanceTests updated for the new
68-byte layout and the vertex attribute declaration text.
- Mesh-emitter particles carry it as a per-draw uniform (uTextureHandle,
uvec2) exactly like terrain's pattern from V2b: one texture per draw call,
set right before it. Became uTextureIndex (uint) + the same
ACDREAM_TEXTURE_HANDLE lookup.
ParticleRenderer owns its own GlBindlessHandleTable and binding=9 SSBO,
independent of the other three renderers' tables, created eagerly in the
constructor alongside the other GL resources it already creates there. Unlike
the other three renderers, flushing/binding the table happens immediately
before EVERY individual draw call (four call sites: immediate billboard,
immediate mesh, and both halves of the deferred/prepared RetailAlphaQueue
path) rather than once per pipeline-state switch - a run of consecutive
mesh-particle sub-batches can register a new handle partway through (each
sub-batch has its own texture), and the table must be current for each one,
not just the first.
TextureCache's particle-texture cache (AcquireParticleTexture,
StandaloneBindlessTextureCache) needed no change: it only ever hands back a
raw ulong handle, and both ParticleGfxInfo.TextureHandle and
ParticleInstance.TextureHandle keep carrying that raw value - the table
lookup is added exactly where each path already converts its handle into
GPU-visible state (WriteBillboardGpuInstance and the two ProgramUniform
call sites).
Coverage caveat (flagged per the campaign doc's slice table): the offline
pixel gate's fixed outdoor view has no particles in frame, so it does not
exercise this slice - it only confirms nothing else regressed. This change
is correspondingly kept strictly mechanical (indirection only, no logic
change), but it still needs a user visual check with live particle emitters
before being trusted as pixel-identical.
Gate: dotnet build -c Release green, dotnet test tests/AcDream.App.Tests
-c Release green (3843 passed / 3 skipped on a clean run - one unrelated
pre-existing flaky allocation test, UiDatFontTests, failed once and passed
on immediate re-run in isolation and in the full suite, confirmed unrelated
to this change), and tools/run-offline-pixel-gate.ps1 passed against the
V2b commit's build with a 4.62e-05 differing-pixel fraction (a tripwire
only, per the coverage caveat above). No divergence-register row: this
introduces no retail behavior deviation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Moves the mesh/EnvCell draw path's per-batch texture representation from a
64-bit ARB_bindless_texture handle to a small integer table index, entirely
on the still-shipping GL backend, with zero pixel change. This is the CPU-side
half of the eventual Vulkan descriptor-array indexing model: a table index is
the backend-neutral form (Vulkan indexes a descriptor array with it directly),
while a raw bindless handle is GL-only. Landing the data-model change now, on
GL, under a strict self-differential pixel gate, keeps it separate from V4c's
much larger RHI-plumbing change (see docs/plans/2026-07-27-vulkan-campaign.md
section 5.2 for why the table cannot be device-owned yet).
Mechanism: mesh_modern.vert's BatchData struct carries `textureIndex` (a slot)
instead of `textureHandle` (uvec2); the vertex shader looks the slot up in a
new binding=9 storage buffer (GpuBindingModel.StorageTextureTable) and passes
the reconstructed uvec2 handle to the fragment shader exactly as before, so
mesh_modern.frag needed no change at all beyond the UBO-set macro below. The
16-byte std430 stride is unchanged (GpuBindingModel.GpuBatchDataStrideBytes);
textureLayer/flags keep their offsets, so every existing CPU writer's layout
is untouched.
The handle->slot table (GlBindlessHandleTable, new, pure C#) is owned
separately by WbDrawDispatcher and EnvCellRenderer rather than shared through
a single TextureCache-owned instance: EnvCellRenderer never had a TextureCache
dependency, and nothing requires index agreement between renderers since each
rebinds its own binding=9 buffer immediately before its own draw call. This
avoided threading a new constructor parameter through EnvCellRenderer (and its
six test call sites) for no behavioral benefit. TextureCache and
CompositeTextureArrayCache turned out to need no changes at all: they only
ever produce raw ulong handles, and that production path is unaffected -
the new indirection is entirely a WbDrawDispatcher/EnvCellRenderer-side
concern, added exactly where each already assembles its per-batch GPU struct
(ToInput, the copy-back loop, PrepareDeferredAlphaDraws for the
RetailAlphaQueue path, and EnvCellRenderer's ModernBatchData construction).
The table itself is a single non-ring buffer (unlike the per-frame
triple-buffered SSBOs) because a genuinely new handle is rare - new dat
surfaces/composite overrides, not every frame - so it flushes only when
GlBindlessHandleTable.Dirty is set, mirroring how the existing texture caches
already upload infrequently.
Shader-side, introduced Rendering/Shaders/common.glsl as the shared preamble
GL has no #include for: Shader.cs gained an `includeCommonPreamble` overload
that splices the file's text in after the leading #version/#extension block
(GLSL requires #version first). It declares the binding=9 table plus the
ACDREAM_TEXTURE_HANDLE(idx) lookup macro, and a scaffolding ACDREAM_UBO_SET
macro (a no-op under GL today, redefined to `set = 1,` when the Vulkan
toolchain compiles this same source at V6+, per the campaign doc's set-1 UBO
note) applied to both SceneLighting UBO declarations now so no later slice
needs to touch them again.
Tests: WbDrawDispatcherIndirectBuilderTests updated for the renamed
IndirectGroupInput/BatchDataPublic fields; new ModernBatchDataLayoutTests
(mirrors ClipFrameLayoutTests' role, but for EnvCellRenderer's GPU struct) and
GlBindlessHandleTableTests (pure-CPU allocator behavior, including the
zero-handle case, which is registered like any other handle rather than
special-cased, since that's what reproduces the pre-V2 sampling result
bit-for-bit).
Gate: dotnet build -c Release green, dotnet test
tests/AcDream.App.Tests -c Release green (3843 passed / 3 skipped, +9 over
the 3834/3 baseline), and tools/run-offline-pixel-gate.ps1 passed with a
2.84e-05 differing-pixel fraction against the parent commit - within the
documented ~33x same-commit noise margin. No divergence-register row: this
introduces no retail behavior deviation.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implements GlGpuDevice and the rest of AcDream.App.Rendering.Gpu.Gl,
filling the V0-pinned IGpuDevice contract on OpenGL 4.3. This is the
first of the port slices described in
docs/plans/2026-07-27-vulkan-campaign.md: every later renderer port
(V2 onward) needs a real, driver-proven GL implementation of the RHI
to port onto, and the GL backend is deliberately built to be
behaviour-preserving rather than optimal, because that is what turns
each subsequent slice's pixel gate into a strict identity check
instead of a moving target. The Vulkan backend (V5+) is where the
actual efficiency gains land.
GlGpuDevice is a fresh root, not derived from Chorizite's
BaseGraphicsDevice/OpenGLGraphicsDevice - shedding that inheritance is
one of the things this campaign explicitly does. It owns its own
BindlessSupport instance rather than sharing the legacy WB render
path's, which is what lets it be constructed the moment a GL context
and a GpuFrameFlightController exist, with no dependency on when
WorldRenderCompositionPhase happens to detect bindless support later
in startup. The ring buffer keeps a managed staging array plus a real
GL buffer per flight slot and flushes with one BufferSubData
immediately before each Draw/DrawIndexed/MultiDrawIndexedIndirect
(never at bind time, since a renderer may still write after binding);
V1 throws on an over-capacity ring request rather than growing it,
since nothing consumes the device yet and a silent grow would hide a
future renderer's real working set. The texture table is a bump/free-
list allocator over a managed uvec2 handle array, gated through the
frame-flight retirement queue so a released slot cannot be reused
while a submitted frame might still read it. Push constants are
applied by uniform name on the currently-bound program, cached per
program, and explicitly re-applied whenever BindPipeline switches
programs - GL uniforms are per-program state, so the "survives
pipeline changes within a pass" guarantee the interface documents (a
freebie on Vulkan's shared pipeline layout) has to be emulated here.
BindlessSupport gained one additive method,
GetResidentHandle(texture, sampler), calling the same
ArbBindlessTexture.GetTextureSamplerHandle entry point
ManagedGLTextureArray already uses through a different path. The
existing GetResidentHandle(texture) cannot express
IGpuDevice.RegisterTexture's documented pair semantics ("the same
texture registered with two samplers occupies two slots"), so this
was the minimal change needed rather than a workaround.
The pure bookkeeping - ring watermark/alignment arithmetic, the
texture-slot allocator, render-state diffing, the push-constant field-
to-uniform-name table, and GL format mapping - lives in small GL-free
classes so it is unit-testable without a live context, following the
same seam pattern GpuFrameFlightController already uses for its fence
API. GlGpuTimerPool follows suit with an injectable timer-query API.
The device is constructed in HostInputCameraCompositionPhase
immediately after the frame-flight controller (the same phase that
already builds GpuFrameFlightController), rather than in
WorldRenderCompositionPhase as first considered: GlGpuDevice's self-
contained bindless detection means it has no ordering dependency on
the legacy WB path's BindlessSupport, so it can be proven against the
real driver as early as possible while keeping the composition change
to one phase. Composition, publication, and shutdown wiring follow
the existing acquire/publish/fault-injection pattern exactly, and GPU
device disposal is scheduled through the frame-flight retirement queue
before that queue itself is torn down. Nothing consumes the device
yet - that starts at V4a - so this slice's pixel gate is trivially a
tripwire.
App tests: 3834 passed / 3 skipped (V0 baseline 3785 + 49 new: ring,
texture-slot, render-state, push-constant, format-mapping, enum-
mapping, and timer-pool tests, plus one new fault-injection point in
the existing composition theory).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Campaign V migrates the renderer from OpenGL 4.3+extensions to a single
Vulkan 1.3 backend on Windows x64 and Linux x64, then deletes the GL path.
Motivation is compatibility and efficiency, not rescue: mandatory
GL_ARB_bindless_texture is the exact floor that parked Slice L (Mesa
D3D12/llvmpipe lack it) while Vulkan descriptor indexing is core, and
per-frame data can be written straight into mapped memory rather than
copied through BufferSubData.
V0 pins the contract every later slice codes against. Nothing consumes it
yet, so this commit changes no runtime behavior.
The seam is a minimal Vulkan-shaped RHI implemented FIRST on GL. That
ordering is the point: the twelve renderers then port one at a time under a
strict pixel gate on the still-shipping backend, so a divergence is
attributed to one slice instead of surfacing at a big-bang integration.
Duplicating renderers per backend was rejected because WbDrawDispatcher is
4,449 lines holding only ~62 GL call sites — the API surface is small and
the retail-fidelity CPU logic is large, and forking the latter is how subtle
regressions enter.
Contract highlights:
- GpuBindingModel pins set/binding numbers dual-legal for GL and Vulkan
GLSL. Storage bindings 0-8 keep today's shader numbering; UBOs move to
their own set, which resolves the binding=1 collision GL only tolerates
because it keeps SSBO and UBO tables separate.
- GpuRingAllocation is a ref struct replacing every per-frame
BufferSubData; the compiler forbids outliving the owning frame.
- GpuTextureSlot replaces bindless handles. Unassigned is a loud
uint.MaxValue sentinel rather than a silent resolve to slot 0 — the
failure mode behind the magenta 1x1 UI placeholder bug. Renderers
needing a fallback take the device's really-registered default slot.
- Renderers always speak GL winding/viewport conventions; the Vulkan
backend compensates with a negative viewport height in exactly one
mapping function.
Verified while writing the plan: acdream's cameras already build
[0,1]-NDC projections (PortalProjection.cs:12-13), which is Vulkan's
convention. No projection rework is needed and depth precision improves,
at the cost of shifted z-fight patterns — the one pre-approved divergence
class, registered per instance at V7.
Gate: Release build green; App suite 3,785 passed / 3 skipped (3,763
baseline plus 22 new contract tests). Note for later slices, recorded in
the plan: run the suite in Release. LandblockBuildOriginTests'
far-strip test asserts behavior that LandblockStreamer.cs:505 deliberately
turns into a loud Debug.Assert in Debug builds, so a Debug run shows one
pre-existing failure that is not a regression.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Make every App composition phase borrow one GameRuntime, retire the duplicate view/event adapters, and dispose the root only after its graphical borrowers release. This preserves synchronous UI commands while giving shutdown one exact ownership ledger.
Co-authored-by: OpenAI Codex <codex@openai.com>
Move remote-motion construction, CreateObject vector initialization, final simulation-component retirement, and the combined J5 ownership ledger into Runtime. Delete App compatibility views and moved-state reconstruction while preserving the existing graphical projection and retail update order.
Move projectile component identity, prediction invalidation, spatial worksets, authoritative corrections, and the retail physics step into AcDream.Runtime. Keep App as the DAT-shape and presentation adapter so ACE outcomes and visible behavior remain unchanged.
Move the sole PhysicsEngine, production cache, collision admissions, canonical bodies and hosts, remote components, ordinary/remote worksets, simulation, cell commits, and shadow synchronization under RuntimeEntityObjectLifetime. Keep App as the prepared-asset, animation-input, and render-projection adapter while preserving the named-retail update and collision order.
Add exact-incarnation, object-clock, callback-reentrancy, GUID-reuse, two-runtime isolation, source ownership, collision publication, and graphical projection coverage. Release build and the complete 8,588-test solution pass.
Co-authored-by: Codex <noreply@openai.com>
Issue stable Runtime identities at canonical registration, publish entity and inventory commits through one generation-stamped synchronous stream, and make graphical adapters borrow the same direct views and events as a no-window host. Preserve exact projection teardown and retail mutation order while removing App-side event reconstruction.
Make the hard-recenter ordering fixture independent of the production two-millisecond frame budget so its injected-failure gate is deterministic.
Co-authored-by: Codex <codex@openai.com>
Introduce one presentation-free RuntimeEntityObjectLifetime for the exact entity directory and ClientObjectTable. Make GameWindow, graphical projections, retained UI, interaction, session routing, create/delete integration, and reset borrow that owner while preserving synchronous retail ordering, dormant retention, and retry semantics.
Co-authored-by: Codex <codex@openai.com>
Move the presentation-free inbound physics timestamp/snapshot authority and parent-relation state into AcDream.Runtime.Entities without changing their control flow. Move their dedicated tests with them and keep App consumers as borrowers during the staged J3 cutover.
Validated by 26 focused Runtime tests, 232 focused App tests, the Release solution build, and 8,429 complete Release tests with five existing skips.
Co-authored-by: Codex <codex@openai.com>
Move the canonical WorldSession generation, connect/enter/tick/stop transaction, inbound subscription owner, and retryable teardown acknowledgements into AcDream.Runtime. Keep App as a borrowing graphical host with a single inertable command projection and no mirrored session state.
Validated by 79 Runtime tests, 3,776 App tests with three existing skips, the Release solution build, and 8,428 complete Release tests with five existing skips.
Co-authored-by: Codex <codex@openai.com>
Establish the J1 presentation-independent contract with instance-scoped clocks and generations, immutable borrowed views, typed generation-gated commands, normalized ordered diagnostics, and teardown acknowledgements. Route graphical startup plus press-time selection, movement, and combat through focused App adapters over the exact existing owners without adding a queue or mirrored world.
Validated by the Release solution build, 13 Runtime tests, 3,838 App tests with three existing skips, and the complete 8,424-test Release suite with five existing skips.
Co-authored-by: Codex <codex@openai.com>
Eliminate boxed production surface-override enumeration, retain vital modifier projections until the enchantment registry mutates, and measure DAT font widths without allocating a captured delegate. Preserve exact hashes, spell stacking, and glyph advances with warmed zero-allocation tests.
Validated by the focused rendering, UI, and spell suites, a zero-error Release build, and 8,409 passing Release tests with five pre-existing skips.
Co-authored-by: Codex <codex@openai.com>
Model active-monitor refresh telemetry as optional so GLFW display replacement during console/RDP transitions cannot escape the native move callback and terminate the client. Preserve the existing software pacing fallback until a later monitor event succeeds.
Co-authored-by: OpenAI Codex <codex@openai.com>
Route production meshes and attached particles from the exact retained PView frame ranges, while keeping the former WorldEntity partition only for standalone tests and explicit diagnostic/oracle probes. Copy retained source/count facts before arena release so world diagnostics no longer require the old partition. Record the accepted G4 visual gate and open the exact G5 production matrix.
Co-authored-by: OpenAI Codex <codex@openai.com>
Derive attached particle owners from the exact retained PView route stream with a zero-allocation warm path. Correct G4 gate provenance, record the current production-only RDP sample, and keep G5 old-route deletion explicitly blocked until the reapplied retained cutover passes its connected visual gate.
Detach old-world spatial ownership atomically, prioritize destination retirement dependencies, and reveal the viewport at the retail transition edge. Give private paperdoll views independent mesh ownership and retain dormant ACE entities so portal revisits preserve server objects without extending active GPU lifetimes.
Reuse the PView frame input, publish mutation-invalidated landblock views, and avoid constructing optional shadow iterators while preserving title and lifecycle visibility facts.
Source composite warmup from the canonical published destination neighborhood, including quiesced static and live projections, instead of rescanning the retained Far-tier world after every membership edge.
This lifecycle correction applies to both draw paths, preserving ef1d263337 as the sole G4 visual rollback.
Co-authored-by: OpenAI Codex <codex@openai.com>
Scan published destination candidates under a separate bounded budget from expensive mesh and composite preparation so large retained worlds cannot exhaust the reveal window before candidate discovery completes.
The correction is valid on both the retained and pre-cutover draw paths, so the G4 visual rollback remains the single commit ef1d263337.
Co-authored-by: OpenAI Codex <codex@openai.com>
Make the incremental render scene the production entity source at the existing retail PView stages while retaining the accepted dispatcher upload and draw executor. Keep diagnostics consumer-gated, retain ordered indices across unchanged frames, refresh only dirty records, and preserve exact mesh-load, selection, alpha, lighting, and route lifecycle semantics.
Retain appearance classification per projection while refreshing transforms, clip slots, lights, selection lighting, and group instance payloads each frame. Incomplete resources and active animation remain on the live classification path, and only an exact full packed comparison acknowledges scene dirtiness.
The production dispatcher forces transparent deferral off when no top-level VAO was resolved, even if the frame alpha queue is active. Carry the same per-draw VAO sentinel into the compare-only packed classifier so its empty-submission state is exact rather than sampling only the later queue state.
Build a compare-only dispatcher classifier from RenderFrameView and compare complete opaque, alpha, selection, clip, light, texture, transform, and draw payloads against the accepted path. Preserve retail's stable equal-CYpt ordering with explicit draw-local submission ordinals so material-group history cannot affect alpha ties.
Capture immutable presentation payloads in the render scene, flatten unique entity and mesh-part records into the borrowed frame arena, and compare every routed dispatcher input tuple without changing the production draw source.
Build a same-frame candidate product from incremental render-scene indices and compare its exact PView routes against the accepted current path without changing the production draw source.
Introduce two reusable generation-stamped arenas whose views are token-validated across build, publish, borrow, release, and abort. This is a non-drawing G0 seam for the incremental scene and changes no production consumer.
Treat the equipped-child controller's composed-pose callback as the authoritative attached projection presence edge. Portal transit can withdraw presentation while retaining the accepted parent relation, so the first destination pose must register the child again.
Release gate: 3,734 App tests / 3 skips; 8,218 complete-solution tests / 5 skips.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Keep shared private entity viewports outside the normal-world picking product and make the injected spatial-pin retry test independent of the production frame clock.
Co-Authored-By: OpenAI Codex <codex@openai.com>
Construct Slice F's non-drawing scene only for lifecycle automation, drain accepted static and live deltas at the final update boundary, and compare exact current-path fingerprints at cadence and checkpoints. Publish bounded mismatch, journal, index, digest, and memory evidence without changing normal launches or draw submission.
Release: 8,211 passed, 5 skipped.
Maintain packed render worksets as lifecycle deltas arrive and synchronize active animated statics without scanning the resident world. This keeps Slice F non-drawing while preparing exact spatial and feature queries for shadow comparison.
Release: 8,206 passed, 5 skipped.