acdream/tests/AcDream.Core.Tests
Erik e946b46f75 feat(render): Campaign V slice V4b - move the mesh arena onto IGpuBuffer
The shared vertex/index arena is the largest single GPU allocation acdream
makes (384 MiB + 128 MiB) and the one the Vulkan backend has the most specific
plan for (campaign doc section 4.3). This slice swaps the resource handle type
underneath it and changes nothing else: the reclaimable-range allocator, the
growth quanta, the budgeted incremental grow-and-copy, the retirement-ledger
gating, the abort ticket, the LRU that drives eviction, and the 896 MiB
dual-generation physical ceiling are all untouched. That is deliberate - those
are the semantics section 4.3 says the Vulkan arena must mirror exactly, so
preserving them is the point of the slice rather than an incidental constraint.

What moved:

- GlobalMeshBuffer's two GL buffer objects became IGpuBuffer, allocated through
  IGpuDevice.CreateBuffer with DeviceLocal residency and Vertex-or-Index plus
  both transfer usages (the arena is simultaneously a draw source and both ends
  of its own migration, which is exactly why GpuBufferUsage is a flags enum).
- UploadMesh's two hand-rolled BufferSubData sites became IGpuBuffer.Upload.
  The old code staged indices through GL_COPY_WRITE_BUFFER specifically so an
  upload could not mutate whichever VAO a preceding render pass left bound;
  Upload stages through a neutral binding point of the backend's choosing, so
  that property now comes for free instead of by hand.
- AdvanceMigration's CopyBufferSubData became IGpuBuffer.CopyTo - a device-side
  copy, which the Vulkan backend will record as vkCmdCopyBuffer. The live
  prefix still never round-trips through system memory.
- BeginMigration/CommitMigration/AbortMigration/Dispose now carry IGpuBuffer in
  the migration record and the abort ticket instead of raw uint names, so the
  ticket's identity check is a resource identity rather than a number that goes
  stale the moment the buffer is deleted.

What deliberately did not move. A VAO has no RHI verb - Vulkan bakes vertex
input into the pipeline - and WbDrawDispatcher, EnvCellRenderer and
ParticleRenderer still bind VAO/VBO/IBO with raw GL until V4c hands them the
pass encoder. So GlobalMeshBuffer keeps its GL handle for the vertex array and
its attribute layout, and VBO/IBO became computed properties that publish the
backing GL name of the buffer the arena now owns as an IGpuBuffer. One private
RequireGlBuffer helper is the single place that reaches through the interface,
and it disappears with those consumers. ObjectMeshManager therefore needed no
upload-path change at all - it reads those same three properties.

Two decisions worth recording.

First, arena deletes do not route through IGpuBuffer.Dispose. The arena already
gates every delete behind its own GpuRetirementLedger and decrements its
physical-capacity accounting in the same retirement stage; Dispose would defer
the physical free through the device queue a second time, so the accounting
would run ahead of real GPU residency and could admit a migration that breaches
the 896 MiB ceiling. GlGpuBuffer gains DeleteRetired for callers that have
already proved flight safety, and GlobalMeshBuffer composes it into a release
whose four stages match TrackedGlResource.CreateRetryableBufferDeletion exactly
- precondition, mutation-with-validation, byte accounting, resource-count
accounting - so a driver failure re-issues only the delete and never
double-counts.

Second, two corrections in the GL backend, both required to keep this port
behaviour-preserving rather than merely compiling. GlGpuBuffer's glBufferData
usage hint now follows residency (DeviceLocal -> StaticDraw), which is what the
arena has always requested; the host-writable rings and texture table keep
DynamicDraw and are unaffected. And a failed allocation now releases the GL
name it had already created - GL_OUT_OF_MEMORY is a real outcome for a 384 MiB
growth destination, and the previous code leaked the name on that path.

Plumbing: the device reaches the arena through WbMeshAdapter and
ObjectMeshManager. Their constructors became internal because IGpuDevice is an
internal type by the pinned contract, matching what V4a did for BitmapFont,
DebugLineRenderer and TextRenderer; both classes stay public and every caller
already lives inside AcDream.App or its InternalsVisibleTo test assemblies. The
unused public GlobalMeshBuffer(GL) convenience constructor is gone - it could
not supply a device and had no callers.

Gates. Release build green with TreatWarningsAsErrors. App tests 3,843 passed /
3 skipped, exactly the slice baseline; complete Release suite 8,906 passed / 5
skipped. Offline pixel gate against 79ee2361: 25 differing pixels of 563,200
(fraction 4.44e-05), against a same-commit control captured immediately
afterwards of 24 - the change is indistinguishable from capture noise and sits
40x under the 0.001 threshold. An earlier gate run was discarded rather than
interpreted: its client log showed real ScrollUp/ScrollDown input reaching the
offline window, which zoomed the camera, and a camera-motion difference is not
a rendering result.

No divergence-register row: this slice changes no retail-facing behaviour.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 20:02:50 +02:00
..
Allegiance feat(allegiance): Phase H.2 AllegianceRequests + AllegianceTree model 2026-04-18 17:17:45 +02:00
Audio feat(diagnostics): complete residency pressure ledger 2026-07-24 16:40:17 +02:00
Chat refactor(runtime): own communication and social state 2026-07-26 07:50:16 +02:00
Combat refactor(input): own gameplay action routing 2026-07-22 12:43:05 +02:00
Conformance refactor(pipeline): MP1a cleanup - narrow public surface, csproj parity, move residue 2026-07-05 20:52:01 +02:00
Fixtures fix(#185): landblock collision part-id uint32 overflow dropped stair steps 2026-07-08 10:07:50 +02:00
Input refactor(runtime): own per-session physics simulation 2026-07-26 13:39:57 +02:00
Items fix(interaction): restore retail loot placement and world-drop projection 2026-07-27 00:03:15 +02:00
Lighting perf(lighting): bound global light selection 2026-07-25 05:40:32 +02:00
Meshing feat(vfx): bind effects to live animated poses 2026-07-14 10:56:01 +02:00
Physics fix(headless): complete connected movement gate 2026-07-27 10:26:44 +02:00
Player refactor(net): converge live session state reset 2026-07-21 12:00:48 +02:00
Plugins fix(rendering): bound portal resource lifetime 2026-07-18 21:35:16 +02:00
Rendering feat(render): Campaign V slice V4b - move the mesh arena onto IGpuBuffer 2026-07-27 20:02:50 +02:00
Selection refactor(net): converge live session state reset 2026-07-21 12:00:48 +02:00
Social refactor(net): converge live session state reset 2026-07-21 12:00:48 +02:00
Spells feat(headless): share immutable gameplay content 2026-07-27 09:00:48 +02:00
Streaming fix(streaming): preserve portal destination ownership 2026-07-25 08:35:12 +02:00
Terrain fix #108-residual (root cause): terrain drew DOUBLE-SIDED - port retail landPolysDraw eye-side gate as terrain backface cull 2026-06-12 22:05:31 +02:00
Textures fix(textures): DecodeSolidColor null-safe against null ColorValue 2026-06-14 14:36:07 +02:00
Ui feat(chat): port retail client command families 2026-07-13 14:50:15 +02:00
Vfx fix(vfx): classify hardwareless particle emitters once 2026-07-27 00:03:44 +02:00
World refactor(runtime): own world environment state 2026-07-26 16:45:04 +02:00
AcDream.Core.Tests.csproj test: fix PhysicsResolveCapture/PhysicsDiagnostics static-leak isolation 2026-06-02 15:20:24 +02:00
GlobalUsings.cs refactor(runtime): own local movement and outbound cadence 2026-07-26 12:33:53 +02:00
SmokeTest.cs chore: phase 1 — add Core, Abstractions, App, Tests projects 2026-04-10 09:22:33 +02:00
xunit.runner.json test: fix PhysicsResolveCapture/PhysicsDiagnostics static-leak isolation 2026-06-02 15:20:24 +02:00