feat(render): Campaign V slice V6h — the Vulkan composition host
ACDREAM_RENDER_BACKEND=vulkan now runs the real GameWindow composition rather
than a second main(). All nine phases execute: DAT load, streaming, camera,
entity table, session, and the real retained UiHost drawing through the RHI.
No world renderers — they are raw GL until V4t and the world arm behind it.
The offline log is the client's own (acdream.pak opened, 6266 spells, Region
0x13000000, "loading world view centered on 0xA9B4FFFF", fourteen retail
LayoutDesc lines, streaming radii), and the captured frame is the retail
retained UI: vitals, combat/spell bar with DAT scarab icons, the nine-slot
toolbar, chat with tabs and Send, radar/compass with dat-font glyphs. Sampled
against the GL capture the widgets agree — chat interior RGBA (25,24,27,158)
vs (22,21,23,158), vitals bar (117,1,0) and toolbar slot (0,11,17) identical.
Three seams, as §5.5.9 specified:
1. Platform acquisition — already generic — publishes GameWindowGraphics
instead of a bare GL. Phases that still speak raw GL read Graphics.Gl and
take their Vulkan arm when it is null; each branch names the slice that
removes it.
2. VulkanHostInputCameraCompositionFactory is a new file and the whole of the
Phase-1 fork: four graphics members differ, input/camera/pointer delegate.
The default factory is chosen inside the phase from the platform result.
HostInputCameraResult gained backend-neutral Retirement and FrameSlots.
3. The frame root forks on one condition. The GL world-scene assembly is
unchanged, wrapped in `if (gl is not null)`; the Vulkan arm's graph is one
backbuffer clear pass computing the same RenderFrameFoundation from the same
clock and weather owners, then private presentation over it.
§5.5.9's three TextureCache couplings are unpicked: the constructor takes GL?
and rejects bindless without one, world entry points route through a Gl
property that throws naming V4t, and the (GlGpuTexture) VRAM-accounting cast
became a backend test. That cast's stated reason — DrawSprite's texture-unit
binding — was already stale, deleted at V6d.
VulkanBringUpHost is reduced to the capability-probe harness it is named for:
the instance/surface/device/swapchain sequence moved into VulkanGraphicsContext,
which the composition host and the harness now share. It is reached only with
ACDREAM_VULKAN_PROBE=1.
One latent Vulkan defect surfaced and is fixed here. The first composition-host
frame died with ErrorDeviceLost; validation named VUID-vkCmdDraw-None-08600 —
descriptor set 2 never bound. VulkanGpuPassEncoder bound sets 0/1/2 only as a
side effect of BindStorageBuffer/BindUniformBuffer, so a pass sampling the
texture table while binding no buffer — every retained-UI and debug-line pass —
drew with the table unbound. It survived V6c-V6g because the bring-up host
always drew VulkanRhiScene first and the UI pass inherited its binds; the
composition host has no 3-D scene. The fix is one line in the encoder's
constructor beside the viewport and scissor defaults, which exist for exactly
the same reason: a pass opens with complete binding state rather than depending
on what preceded it.
Gates: strict GL offline pixel gate against 46d893f7 measures 1.24e-05 (7 of
563,200 pixels), inside the documented 15-23 px / 4.1e-05 band, so GL behaviour
did not move. App tests 4,075/3 skips; complete Release suite 9,138/5 skips.
One full Vulkan run with VK_LAYER_KHRONOS_validation: zero errors, zero
warnings. Both Vulkan runs converged the ownership ledger — no [shutdown]
diagnostic on either stream. The reduced probe harness presented 34,811
validation-clean frames.
No divergence-register row: GL is the shipping backend and the pixel gate proves
it unmoved; the Vulkan arm is not a retail deviation but a backend under
construction.
Next is V4t, the texture stack, which the world arm cannot be written without.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
46d893f7c3
commit
b16f820643
29 changed files with 2292 additions and 997 deletions
|
|
@ -18,7 +18,7 @@ public sealed unsafe class TextureCache
|
|||
: Wb.IEntityTextureLifetime,
|
||||
IDisposable
|
||||
{
|
||||
private readonly GL _gl;
|
||||
private readonly GL? _gl;
|
||||
private readonly IGpuDevice _device;
|
||||
private readonly IDatReaderWriter _dats;
|
||||
private readonly string _diagnosticsDirectory;
|
||||
|
|
@ -105,7 +105,7 @@ public sealed unsafe class TextureCache
|
|||
// contract), and this convenience overload has no real caller today (both
|
||||
// production construction sites already target the internal overload
|
||||
// below) — kept internal rather than deleted to preserve its shape.
|
||||
internal TextureCache(GL gl, IGpuDevice device, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
|
||||
internal TextureCache(GL? gl, IGpuDevice device, IDatReaderWriter dats, Wb.BindlessSupport? bindless = null)
|
||||
: this(
|
||||
gl,
|
||||
device,
|
||||
|
|
@ -119,8 +119,19 @@ public sealed unsafe class TextureCache
|
|||
{
|
||||
}
|
||||
|
||||
/// <param name="gl">
|
||||
/// The GL context, or null on a backend that has none. Campaign V slice V6h:
|
||||
/// the UI path (<see cref="GetOrUploadRenderSurface"/>, <see cref="UploadRgba8"/>)
|
||||
/// is entirely <see cref="IGpuDevice"/>-driven and runs on either backend,
|
||||
/// while the world paths — the legacy <c>Texture2D</c> upload, particle
|
||||
/// arrays, and the composite/bindless caches — still speak raw GL and are
|
||||
/// unreachable without it. A null context therefore reaches exactly the same
|
||||
/// code a null <paramref name="bindless"/> already gated, and every world
|
||||
/// entry point throws with the slice that owns it named. Removed at V4t,
|
||||
/// which ports the world texture stack onto the RHI.
|
||||
/// </param>
|
||||
internal TextureCache(
|
||||
GL gl,
|
||||
GL? gl,
|
||||
IGpuDevice device,
|
||||
IDatReaderWriter dats,
|
||||
Wb.BindlessSupport? bindless,
|
||||
|
|
@ -130,6 +141,13 @@ public sealed unsafe class TextureCache
|
|||
{
|
||||
budgets ??= ResidencyBudgetOptions.Default;
|
||||
_gl = gl;
|
||||
if (gl is null && bindless is not null)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Bindless composite/particle texture caches require a GL context.",
|
||||
nameof(bindless));
|
||||
}
|
||||
|
||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
||||
_dats = dats;
|
||||
_bindless = bindless;
|
||||
|
|
@ -144,7 +162,7 @@ public sealed unsafe class TextureCache
|
|||
try
|
||||
{
|
||||
composite = new CompositeTextureArrayCache(
|
||||
gl,
|
||||
gl!,
|
||||
bindless,
|
||||
retirementQueue,
|
||||
budgets.CompositeUnownedBytes,
|
||||
|
|
@ -170,6 +188,17 @@ public sealed unsafe class TextureCache
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The GL context the world texture paths need. Campaign V slice V6h: a
|
||||
/// Vulkan-composed cache serves the UI path through <see cref="IGpuDevice"/>
|
||||
/// alone and never reaches here, so a failure names the slice that owns the
|
||||
/// port rather than dereferencing null.
|
||||
/// </summary>
|
||||
private GL Gl => _gl ?? throw new InvalidOperationException(
|
||||
"This TextureCache owns no GL context: the world texture paths " +
|
||||
"(Texture2D upload, particle arrays, composite/bindless caches) are " +
|
||||
"unavailable until Campaign V slice V4t ports them onto the RHI.");
|
||||
|
||||
internal void RegisterResidencySources(ResidencyManager manager)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(manager);
|
||||
|
|
@ -319,7 +348,7 @@ public sealed unsafe class TextureCache
|
|||
try
|
||||
{
|
||||
texture.Upload(0, 0, decoded.Rgba8);
|
||||
uint glName = ((GlGpuTexture)texture).GlName;
|
||||
uint glName = UploadAccountingName(texture);
|
||||
TrackUploadedTexture(glName, decoded.Width, decoded.Height);
|
||||
|
||||
IGpuSampler sampler = _device.CreateSampler(nearest ? UiNearestRepeat : GpuSamplerDescription.WorldRepeat);
|
||||
|
|
@ -333,6 +362,24 @@ public sealed unsafe class TextureCache
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The identity a UI upload is accounted under. On GL it is the texture's
|
||||
/// own GL name — unchanged, so the VRAM ledger and the
|
||||
/// <c>ACDREAM_DUMP_SURFACES</c> histogram key off exactly what they always
|
||||
/// did. On any other backend there is no such name, so a descending
|
||||
/// synthetic counter supplies one; it starts at <c>uint.MaxValue</c> because
|
||||
/// GL hands out small ascending names and the two spaces share the
|
||||
/// <c>_uploadMetadata</c> dictionary. The value is a dictionary key and a
|
||||
/// dedup token only — Campaign V slice V6d removed the last draw-time
|
||||
/// consumer of a raw GL name, so nothing binds it.
|
||||
/// </summary>
|
||||
private uint UploadAccountingName(IGpuTexture texture) =>
|
||||
texture is GlGpuTexture glTexture
|
||||
? glTexture.GlName
|
||||
: _nextSyntheticUploadName--;
|
||||
|
||||
private uint _nextSyntheticUploadName = uint.MaxValue;
|
||||
|
||||
/// <summary>
|
||||
/// Point sampling with REPEAT addressing — pixel-exact retail UI art that is
|
||||
/// still tiled by nine-slice chrome and meter tracks. Neither stock preset
|
||||
|
|
@ -413,7 +460,7 @@ public sealed unsafe class TextureCache
|
|||
{
|
||||
handle = _bindless!.GetResidentHandle(name);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
Gl,
|
||||
$"making particle surface 0x{surfaceId:X8} resident");
|
||||
var resource = new StandaloneBindlessTextureResource
|
||||
{
|
||||
|
|
@ -441,7 +488,7 @@ public sealed unsafe class TextureCache
|
|||
{
|
||||
_bindless!.MakeNonResident(handle);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
Gl,
|
||||
"rolling back particle texture residency");
|
||||
residencyReleased = true;
|
||||
});
|
||||
|
|
@ -636,7 +683,7 @@ public sealed unsafe class TextureCache
|
|||
{
|
||||
owner._bindless!.MakeNonResident(resource.Handle);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
owner._gl,
|
||||
owner.Gl,
|
||||
$"releasing particle texture handle {resource.Handle}");
|
||||
}
|
||||
|
||||
|
|
@ -916,15 +963,15 @@ public sealed unsafe class TextureCache
|
|||
|
||||
private uint UploadRgba8(DecodedTexture decoded, bool nearest = false)
|
||||
{
|
||||
uint tex = _gl.GenTexture();
|
||||
uint tex = Gl.GenTexture();
|
||||
if (tex == 0)
|
||||
throw new InvalidOperationException("OpenGL did not create a 2D texture.");
|
||||
try
|
||||
{
|
||||
_gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
Gl.BindTexture(TextureTarget.Texture2D, tex);
|
||||
|
||||
fixed (byte* p = decoded.Rgba8)
|
||||
_gl.TexImage2D(
|
||||
Gl.TexImage2D(
|
||||
TextureTarget.Texture2D,
|
||||
0,
|
||||
InternalFormat.Rgba8,
|
||||
|
|
@ -938,12 +985,12 @@ public sealed unsafe class TextureCache
|
|||
// Point (nearest) sampling for pixel-exact UI text — bilinear softens the dat
|
||||
// font's small glyphs. Other surfaces use bilinear.
|
||||
int filter = nearest ? (int)TextureMinFilter.Nearest : (int)TextureMinFilter.Linear;
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, filter);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, filter);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
|
||||
_gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
|
||||
Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, filter);
|
||||
Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, filter);
|
||||
Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
|
||||
Gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
Gl,
|
||||
$"uploading 2D RGBA8 texture {decoded.Width}x{decoded.Height}");
|
||||
|
||||
TrackUploadedTexture(tex, decoded.Width, decoded.Height);
|
||||
|
|
@ -951,12 +998,12 @@ public sealed unsafe class TextureCache
|
|||
}
|
||||
catch
|
||||
{
|
||||
_gl.DeleteTexture(tex);
|
||||
Gl.DeleteTexture(tex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
Gl.BindTexture(TextureTarget.Texture2D, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -967,15 +1014,15 @@ public sealed unsafe class TextureCache
|
|||
/// </summary>
|
||||
private uint UploadRgba8AsLayer1Array(DecodedTexture decoded)
|
||||
{
|
||||
uint tex = _gl.GenTexture();
|
||||
uint tex = Gl.GenTexture();
|
||||
if (tex == 0)
|
||||
throw new InvalidOperationException("OpenGL did not create a one-layer texture array.");
|
||||
try
|
||||
{
|
||||
_gl.BindTexture(TextureTarget.Texture2DArray, tex);
|
||||
Gl.BindTexture(TextureTarget.Texture2DArray, tex);
|
||||
|
||||
fixed (byte* p = decoded.Rgba8)
|
||||
_gl.TexImage3D(
|
||||
Gl.TexImage3D(
|
||||
TextureTarget.Texture2DArray,
|
||||
0,
|
||||
InternalFormat.Rgba8,
|
||||
|
|
@ -987,12 +1034,12 @@ public sealed unsafe class TextureCache
|
|||
PixelType.UnsignedByte,
|
||||
p);
|
||||
|
||||
_gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
_gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
|
||||
_gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
|
||||
Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
|
||||
Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
|
||||
Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapS, (int)TextureWrapMode.Repeat);
|
||||
Gl.TexParameter(TextureTarget.Texture2DArray, TextureParameterName.TextureWrapT, (int)TextureWrapMode.Repeat);
|
||||
Wb.GLHelpers.ThrowOnResourceError(
|
||||
_gl,
|
||||
Gl,
|
||||
$"uploading one-layer RGBA8 array {decoded.Width}x{decoded.Height}");
|
||||
|
||||
TrackUploadedTexture(tex, decoded.Width, decoded.Height);
|
||||
|
|
@ -1000,12 +1047,12 @@ public sealed unsafe class TextureCache
|
|||
}
|
||||
catch
|
||||
{
|
||||
_gl.DeleteTexture(tex);
|
||||
Gl.DeleteTexture(tex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
Gl.BindTexture(TextureTarget.Texture2DArray, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1019,8 +1066,8 @@ public sealed unsafe class TextureCache
|
|||
|
||||
private void DeleteUploadedTexture(uint name)
|
||||
{
|
||||
_gl.DeleteTexture(name);
|
||||
Wb.GLHelpers.ThrowOnResourceError(_gl, $"deleting uploaded texture {name}");
|
||||
Gl.DeleteTexture(name);
|
||||
Wb.GLHelpers.ThrowOnResourceError(Gl, $"deleting uploaded texture {name}");
|
||||
UntrackUploadedTexture(name);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue