diff --git a/src/AcDream.App/Rendering/BitmapFont.cs b/src/AcDream.App/Rendering/BitmapFont.cs
index 87067356..3f123b3d 100644
--- a/src/AcDream.App/Rendering/BitmapFont.cs
+++ b/src/AcDream.App/Rendering/BitmapFont.cs
@@ -1,8 +1,6 @@
using System;
using System.IO;
using AcDream.App.Rendering.Gpu;
-using AcDream.App.Rendering.Gpu.Gl;
-using Silk.NET.OpenGL;
using StbTrueTypeSharp;
namespace AcDream.App.Rendering;
@@ -14,11 +12,13 @@ namespace AcDream.App.Rendering;
///
/// Campaign V slice V4a: the atlas is created and uploaded through
/// instead of raw GL, and registered
-/// into the device's texture table. stays a raw GL
-/// name — extracted from the created — because its
-/// only consumer ('s classic sprite/font texture-unit
-/// binding path; see that class's remarks) is not itself slot-based this
-/// slice.
+/// into the device's texture table.
+///
+/// Campaign V slice V6d: is that registration's
+/// rather than the raw GL name it used to
+/// be. Its only consumer is , which now samples the
+/// table instead of binding a texture unit, so the GL name has no remaining
+/// reader — and on Vulkan there is no GL name to have.
///
/// Only printable ASCII (32..127) is supported for the debug overlay.
///
@@ -49,6 +49,12 @@ public sealed unsafe class BitmapFont : IDisposable
private readonly int _numChars;
private readonly IGpuTexture _texture;
+ ///
+ /// The atlas's — a one-based index into
+ /// the device's global texture table, not a GL texture name. The name is
+ /// unchanged so that the public shape of this class did not move for a
+ /// change of currency its only consumer makes invisible.
+ ///
public uint TextureId { get; }
public float PixelHeight { get; }
public float LineHeight { get; }
@@ -118,34 +124,22 @@ public sealed unsafe class BitmapFont : IDisposable
Height: AtlasHeight,
LayerCount: 1,
MipLevelCount: 1));
+ GpuTextureSlot slot;
try
{
fixed (byte* ptr = pixels)
texture.Upload(0, 0, new ReadOnlySpan(ptr, AtlasWidth * AtlasHeight));
- uint glNameForWrapFixup = ((GlGpuTexture)texture).GlName;
-
- // The classic texture-unit binding path TextRenderer's font draws
- // use (see its class remarks) samples this texture object
- // directly rather than through a bound GL sampler object, so wrap
- // addressing must live on the texture itself — GlGpuTexture's
- // constructor only sets the filter, not wrap mode (GL 4.3
- // defaults textures to GL_REPEAT). Retail's dat font glyph UVs
- // never leave their own tight atlas sub-rect, but pin ClampToEdge
- // explicitly to preserve the exact sampling behaviour the raw-GL
- // constructor had. MUST happen BEFORE RegisterTexture below:
- // ARB_bindless_texture forbids glTexParameter on a texture once
- // its bindless handle has been made resident (GL_INVALID_OPERATION).
- if (device is GlGpuDevice glDeviceForWrapFixup)
- {
- GL gl = glDeviceForWrapFixup.Gl;
- gl.BindTexture(TextureTarget.Texture2D, glNameForWrapFixup);
- gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)TextureWrapMode.ClampToEdge);
- gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)TextureWrapMode.ClampToEdge);
- gl.BindTexture(TextureTarget.Texture2D, 0);
- }
+ // Clamped, so a glyph's edge texel cannot bleed in from the opposite
+ // side of the atlas. Retail's dat font glyph UVs never leave their
+ // own tight sub-rect, but the sampler states it rather than relying
+ // on that. Slice V6d: this sampler is now the only thing that
+ // decides how the atlas is filtered — the raw glTexParameter pass
+ // that used to sit here existed solely because the classic
+ // texture-unit path sampled the texture object directly, and it is
+ // gone with that path.
IGpuSampler sampler = device.CreateSampler(GpuSamplerDescription.WorldClamp);
- device.RegisterTexture(texture, sampler);
+ slot = device.RegisterTexture(texture, sampler);
}
catch
{
@@ -154,7 +148,7 @@ public sealed unsafe class BitmapFont : IDisposable
}
_texture = texture;
- TextureId = ((GlGpuTexture)texture).GlName;
+ TextureId = UiTextureTableHandle.FromSlot(slot);
}
public bool TryGetGlyph(char c, out Glyph g)
diff --git a/src/AcDream.App/Rendering/DebugLineRenderer.cs b/src/AcDream.App/Rendering/DebugLineRenderer.cs
index 2206d609..1bce691c 100644
--- a/src/AcDream.App/Rendering/DebugLineRenderer.cs
+++ b/src/AcDream.App/Rendering/DebugLineRenderer.cs
@@ -1,8 +1,6 @@
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.Rendering.Gpu;
-using AcDream.App.Rendering.Gpu.Gl;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
@@ -16,17 +14,22 @@ namespace AcDream.App.Rendering;
/// (
/// topology, depth disabled to match this renderer's "visible through
/// geometry" intent), and each Flush's vertex data comes from a per-frame
-/// ring allocation instead of the old single respecialized VBO. The shader's
-/// uView/uProjection pair does not fit the shared
-/// GpuPushConstants block (one combined view-projection matrix), and
-/// has no verb for arbitrary named uniforms, so
-/// they are set directly against the pipeline's compiled program — the same
-/// mechanical translation uses for its own
-/// shader-local uniforms.
+/// ring allocation instead of the old single respecialized VBO.
+///
+/// Campaign V slice V6d removed the last GL dependency. The shader's
+/// separate uView/uProjection pair was set directly against the
+/// compiled GL program because the pinned block
+/// carries one combined matrix and there is no verb for arbitrary named
+/// uniforms. That was never portable — Vulkan has no default uniform block at
+/// all — so the shader converged on uViewProjection and
+/// multiplies the two matrices on the CPU. The product
+/// therefore rounds once per frame instead of once per vertex; these lines are
+/// diagnostic-only geometry that draws when collision wireframes are switched
+/// on, so nothing user-visible depends on those bits.
///
/// Vertex format is (vec3 pos, vec3 color) = 24 bytes per vertex.
///
-public sealed unsafe class DebugLineRenderer : IDisposable
+public sealed class DebugLineRenderer : IDisposable
{
private const int FloatsPerVertex = 6;
private const int VertexStrideBytes = FloatsPerVertex * sizeof(float);
@@ -38,11 +41,8 @@ public sealed unsafe class DebugLineRenderer : IDisposable
new GpuVertexAttribute(1, GpuVertexFormat.Float3, 12),
]);
- private readonly GL _gl;
private readonly ICurrentGpuFrameSource _frameSource;
private readonly IGpuPipeline _pipeline;
- private readonly int _uViewLocation;
- private readonly int _uProjectionLocation;
private readonly List _buffer = new(4096);
private int _vertexCount;
@@ -55,13 +55,6 @@ public sealed unsafe class DebugLineRenderer : IDisposable
ArgumentNullException.ThrowIfNull(device);
_frameSource = frameSource ?? throw new ArgumentNullException(nameof(frameSource));
ArgumentException.ThrowIfNullOrWhiteSpace(shaderDir);
- if (device is not GlGpuDevice glDevice)
- {
- throw new NotSupportedException(
- "DebugLineRenderer's uView/uProjection uniforms (see the class remarks) are set " +
- "directly against the GL program; it needs GlGpuDevice's raw GL handle.");
- }
- _gl = glDevice.Gl;
_pipeline = device.CreatePipeline(new GpuPipelineDescription
{
@@ -80,10 +73,6 @@ public sealed unsafe class DebugLineRenderer : IDisposable
ColorWrite = true,
SampleCount = 1,
});
-
- uint program = ((GlGpuPipeline)_pipeline).GlProgram;
- _uViewLocation = _gl.GetUniformLocation(program, "uView");
- _uProjectionLocation = _gl.GetUniformLocation(program, "uProjection");
}
/// Clear accumulated lines. Call at the start of each frame.
@@ -186,8 +175,14 @@ public sealed unsafe class DebugLineRenderer : IDisposable
SampleCount = 1,
});
encoder.BindPipeline(_pipeline);
- SetMatrix(_uViewLocation, view);
- SetMatrix(_uProjectionLocation, projection);
+
+ // The GLSL used to evaluate uProjection * uView per vertex. System.Numerics
+ // is row-vector convention and GL/Vulkan read the 16 floats as column-major,
+ // which transposes; so the CPU-side equivalent of that product is
+ // view * projection, in that order.
+ GpuPushConstants constants = GpuPushConstants.Default;
+ constants.ViewProjection = view * projection;
+ encoder.SetPushConstants(constants);
int byteCount = _buffer.Count * sizeof(float);
GpuRingAllocation allocation = frame.AllocateRing(byteCount, GpuRingUsage.Vertex);
@@ -196,13 +191,6 @@ public sealed unsafe class DebugLineRenderer : IDisposable
encoder.Draw((uint)_vertexCount, 1, 0, 0);
}
- private void SetMatrix(int location, Matrix4x4 m)
- {
- if (location < 0)
- return;
- _gl.UniformMatrix4(location, 1, false, (float*)&m);
- }
-
public void Dispose()
{
_pipeline.Dispose();
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlAmbientCapabilityState.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlAmbientCapabilityState.cs
new file mode 100644
index 00000000..fcd6b70f
--- /dev/null
+++ b/src/AcDream.App/Rendering/Gpu/Gl/GlAmbientCapabilityState.cs
@@ -0,0 +1,226 @@
+using Silk.NET.OpenGL;
+
+namespace AcDream.App.Rendering.Gpu.Gl;
+
+///
+/// The narrow slice of GL that reads and
+/// writes. It exists so the save/restore transaction can be exercised without a
+/// GL context: the property that matters is "every value this pass can change
+/// is put back, including when a draw throws", and that is a property of the
+/// bookkeeping, not of the driver.
+///
+/// Slice V6d generalized this from ITextRenderGlStateApi, which
+/// TextRenderer owned privately and which restored a strict subset of the
+/// same values. That renderer no longer touches GL at all, and the guarantee now
+/// lives in one place for every RHI pass.
+///
+internal interface IGlAmbientStateApi
+{
+ bool IsEnabled(EnableCap capability);
+
+ int GetInteger(GetPName parameter);
+
+ bool GetBoolean(GetPName parameter);
+
+ void SetCapability(EnableCap capability, bool enabled);
+
+ void DepthMask(bool enabled);
+
+ void DepthFunc(DepthFunction function);
+
+ void BlendFuncSeparate(
+ BlendingFactor sourceRgb,
+ BlendingFactor destinationRgb,
+ BlendingFactor sourceAlpha,
+ BlendingFactor destinationAlpha);
+
+ void CullFace(TriangleFace face);
+
+ void FrontFace(FrontFaceDirection direction);
+
+ void UseProgram(uint program);
+
+ void BindVertexArray(uint vertexArray);
+
+ void BindBuffer(BufferTargetARB target, uint buffer);
+
+ void ActiveTexture(TextureUnit unit);
+
+ void BindTexture(TextureTarget target, uint texture);
+}
+
+internal sealed class SilkGlAmbientStateApi : IGlAmbientStateApi
+{
+ private readonly GL _gl;
+
+ public SilkGlAmbientStateApi(GL gl) => _gl = gl ?? throw new ArgumentNullException(nameof(gl));
+
+ public bool IsEnabled(EnableCap capability) => _gl.IsEnabled(capability);
+
+ public int GetInteger(GetPName parameter)
+ {
+ _gl.GetInteger(parameter, out int value);
+ return value;
+ }
+
+ public bool GetBoolean(GetPName parameter) => _gl.GetBoolean(parameter);
+
+ public void SetCapability(EnableCap capability, bool enabled)
+ {
+ if (enabled)
+ _gl.Enable(capability);
+ else
+ _gl.Disable(capability);
+ }
+
+ public void DepthMask(bool enabled) => _gl.DepthMask(enabled);
+
+ public void DepthFunc(DepthFunction function) => _gl.DepthFunc(function);
+
+ public void BlendFuncSeparate(
+ BlendingFactor sourceRgb,
+ BlendingFactor destinationRgb,
+ BlendingFactor sourceAlpha,
+ BlendingFactor destinationAlpha) =>
+ _gl.BlendFuncSeparate(sourceRgb, destinationRgb, sourceAlpha, destinationAlpha);
+
+ public void CullFace(TriangleFace face) => _gl.CullFace(face);
+
+ public void FrontFace(FrontFaceDirection direction) => _gl.FrontFace(direction);
+
+ public void UseProgram(uint program) => _gl.UseProgram(program);
+
+ public void BindVertexArray(uint vertexArray) => _gl.BindVertexArray(vertexArray);
+
+ public void BindBuffer(BufferTargetARB target, uint buffer) => _gl.BindBuffer(target, buffer);
+
+ public void ActiveTexture(TextureUnit unit) => _gl.ActiveTexture(unit);
+
+ public void BindTexture(TextureTarget target, uint texture) => _gl.BindTexture(target, texture);
+}
+
+///
+/// Every ambient GL capability/binding a bind (or a
+/// dynamic setter, or the pass's own sample count) can change, captured by raw
+/// query and restored by raw call.
+///
+/// Transitional for as long as raw-GL renderers coexist with RHI-ported ones:
+/// each raw-GL renderer assumes whatever state the previous one left behind is
+/// still there, so a pass that changes state and does not put it back is
+/// invisible until the world silhouette changes. That is precisely how the first
+/// V4a attempt lost multisampling (plan §7.1 rule 1). Deleted at V4h.
+///
+internal readonly struct GlAmbientCapabilityState
+{
+ private readonly int _program;
+ private readonly int _vertexArray;
+ private readonly int _arrayBuffer;
+ private readonly int _activeTexture;
+ private readonly int _texture0Binding2D;
+ private readonly bool _depthTest;
+ private readonly bool _depthWrite;
+ private readonly int _depthFunc;
+ private readonly bool _blend;
+ private readonly int _blendSourceRgb;
+ private readonly int _blendDestinationRgb;
+ private readonly int _blendSourceAlpha;
+ private readonly int _blendDestinationAlpha;
+ private readonly bool _cullFace;
+ private readonly int _cullFaceMode;
+ private readonly int _frontFace;
+ private readonly bool _alphaToCoverage;
+ private readonly bool _multisample;
+
+ private GlAmbientCapabilityState(
+ int program, int vertexArray, int arrayBuffer, int activeTexture, int texture0Binding2D,
+ bool depthTest, bool depthWrite, int depthFunc,
+ bool blend, int blendSourceRgb, int blendDestinationRgb, int blendSourceAlpha, int blendDestinationAlpha,
+ bool cullFace, int cullFaceMode, int frontFace,
+ bool alphaToCoverage, bool multisample)
+ {
+ _program = program;
+ _vertexArray = vertexArray;
+ _arrayBuffer = arrayBuffer;
+ _activeTexture = activeTexture;
+ _texture0Binding2D = texture0Binding2D;
+ _depthTest = depthTest;
+ _depthWrite = depthWrite;
+ _depthFunc = depthFunc;
+ _blend = blend;
+ _blendSourceRgb = blendSourceRgb;
+ _blendDestinationRgb = blendDestinationRgb;
+ _blendSourceAlpha = blendSourceAlpha;
+ _blendDestinationAlpha = blendDestinationAlpha;
+ _cullFace = cullFace;
+ _cullFaceMode = cullFaceMode;
+ _frontFace = frontFace;
+ _alphaToCoverage = alphaToCoverage;
+ _multisample = multisample;
+ }
+
+ internal static GlAmbientCapabilityState Capture(IGlAmbientStateApi gl)
+ {
+ ArgumentNullException.ThrowIfNull(gl);
+ int program = gl.GetInteger(GetPName.CurrentProgram);
+ int vertexArray = gl.GetInteger(GetPName.VertexArrayBinding);
+ int arrayBuffer = gl.GetInteger(GetPName.ArrayBufferBinding);
+ int activeTexture = gl.GetInteger(GetPName.ActiveTexture);
+
+ int texture0Binding2D;
+ try
+ {
+ gl.ActiveTexture(TextureUnit.Texture0);
+ texture0Binding2D = gl.GetInteger(GetPName.TextureBinding2D);
+ }
+ finally
+ {
+ gl.ActiveTexture((TextureUnit)activeTexture);
+ }
+
+ return new GlAmbientCapabilityState(
+ program, vertexArray, arrayBuffer, activeTexture, texture0Binding2D,
+ gl.IsEnabled(EnableCap.DepthTest),
+ gl.GetBoolean(GetPName.DepthWritemask),
+ gl.GetInteger(GetPName.DepthFunc),
+ gl.IsEnabled(EnableCap.Blend),
+ gl.GetInteger(GetPName.BlendSrcRgb),
+ gl.GetInteger(GetPName.BlendDstRgb),
+ gl.GetInteger(GetPName.BlendSrcAlpha),
+ gl.GetInteger(GetPName.BlendDstAlpha),
+ gl.IsEnabled(EnableCap.CullFace),
+ gl.GetInteger(GetPName.CullFaceMode),
+ gl.GetInteger(GetPName.FrontFace),
+ gl.IsEnabled(EnableCap.SampleAlphaToCoverage),
+ gl.IsEnabled(EnableCap.Multisample));
+ }
+
+ internal void Restore(IGlAmbientStateApi gl)
+ {
+ ArgumentNullException.ThrowIfNull(gl);
+ gl.UseProgram((uint)_program);
+ gl.BindVertexArray((uint)_vertexArray);
+ gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBuffer);
+
+ gl.ActiveTexture(TextureUnit.Texture0);
+ gl.BindTexture(TextureTarget.Texture2D, (uint)_texture0Binding2D);
+ gl.ActiveTexture((TextureUnit)_activeTexture);
+
+ gl.SetCapability(EnableCap.DepthTest, _depthTest);
+ gl.DepthMask(_depthWrite);
+ gl.DepthFunc((DepthFunction)_depthFunc);
+
+ gl.SetCapability(EnableCap.Blend, _blend);
+ gl.BlendFuncSeparate(
+ (BlendingFactor)_blendSourceRgb,
+ (BlendingFactor)_blendDestinationRgb,
+ (BlendingFactor)_blendSourceAlpha,
+ (BlendingFactor)_blendDestinationAlpha);
+
+ gl.SetCapability(EnableCap.CullFace, _cullFace);
+ gl.CullFace((TriangleFace)_cullFaceMode);
+ gl.FrontFace((FrontFaceDirection)_frontFace);
+
+ gl.SetCapability(EnableCap.SampleAlphaToCoverage, _alphaToCoverage);
+ gl.SetCapability(EnableCap.Multisample, _multisample);
+ }
+}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs
index 3935e742..6213d73e 100644
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs
+++ b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuDevice.cs
@@ -168,6 +168,18 @@ internal sealed class GlGpuDevice : IGpuDevice
internal GlGpuPushConstantBinder PushConstants => _pushConstants;
internal GlGpuTimerPool TimerPool => _timerPool;
+ ///
+ /// GL name of the buffer emulating the global texture table
+ /// (). Slice V6d:
+ /// binds it on every pipeline
+ /// bind, which is the GL analogue of the Vulkan backend binding descriptor
+ /// set 2 on every draw. Without it an RHI shader that samples the table
+ /// would read whichever raw-GL renderer's private handle table was left at
+ /// binding 9 — a different slot numbering entirely, which is the loudest
+ /// possible way to sample the wrong texture.
+ ///
+ internal uint TextureTableGlName => _textureTableBuffer.GlName;
+
internal GlRenderStateSnapshot CurrentRenderState { get; private set; }
public IGpuBuffer CreateBuffer(in GpuBufferDescription description)
@@ -199,8 +211,15 @@ internal sealed class GlGpuDevice : IGpuDevice
ArgumentNullException.ThrowIfNull(description);
string vertexPath = Path.Combine(_shadersDirectory, $"{description.Shaders.Name}.vert");
string fragmentPath = Path.Combine(_shadersDirectory, $"{description.Shaders.Name}.frag");
- string vertexSource = File.ReadAllText(vertexPath);
- string fragmentSource = File.ReadAllText(fragmentPath);
+ // Campaign V slice V6d: every RHI pipeline gets the shared preamble,
+ // unconditionally. The Vulkan backend injects its own preamble into
+ // every shader it compiles, so making the GL side selective would mean
+ // one source file compiling against two different sets of definitions
+ // depending on which pipeline happened to ask for it. A shader that
+ // reads nothing from the preamble simply carries an unused declaration.
+ string common = File.ReadAllText(Path.Combine(_shadersDirectory, "common.glsl"));
+ string vertexSource = Shader.InjectPreamble(File.ReadAllText(vertexPath), common);
+ string fragmentSource = Shader.InjectPreamble(File.ReadAllText(fragmentPath), common);
return new GlGpuPipeline(_gl, Retirement, description, vertexSource, fragmentSource);
}
diff --git a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs
index 09e33cd1..cd9fe32a 100644
--- a/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs
+++ b/src/AcDream.App/Rendering/Gpu/Gl/GlGpuPassEncoder.cs
@@ -17,6 +17,7 @@ internal sealed class GlGpuPassEncoder : IGpuPassEncoder
private readonly GlGpuDevice _device;
private readonly GlGpuFrame _frame;
private readonly GL _gl;
+ private readonly IGlAmbientStateApi _ambientApi;
private readonly GlAmbientCapabilityState _ambientOnEntry;
private bool _closed;
@@ -44,7 +45,21 @@ internal sealed class GlGpuPassEncoder : IGpuPassEncoder
// on. Capturing here and restoring on Dispose keeps the GL backend's
// behaviour-preserving property true at this seam. Deleted at V4h
// once nothing raw-GL remains.
- _ambientOnEntry = GlAmbientCapabilityState.Capture(_gl);
+ _ambientApi = new SilkGlAmbientStateApi(_gl);
+ _ambientOnEntry = GlAmbientCapabilityState.Capture(_ambientApi);
+
+ // Campaign V slice V6d. GL_MULTISAMPLE is the one piece of pass state
+ // with no representation in GpuPipelineDescription, and the pass's own
+ // SampleCount is the contract's answer for it: a single-sampled pass
+ // does not multisample. Until now the retained UI asserted that with a
+ // raw glDisable of its own — exactly the kind of state a
+ // backend-neutral renderer cannot own. Quality settings enable
+ // GL_MULTISAMPLE once per frame for the world, and if it leaks into the
+ // UI pass every glyph's soft alpha edge becomes dithered coverage
+ // instead of a clean alpha blend (the "fuzzy text" artifact). The
+ // ambient capture above puts it back on Dispose, so the raw-GL world
+ // renderers that follow are unaffected.
+ _ambientApi.SetCapability(EnableCap.Multisample, pass.SampleCount > 1);
}
public GpuPassDescription Pass { get; }
@@ -72,6 +87,20 @@ internal sealed class GlGpuPassEncoder : IGpuPassEncoder
_gl.BindVertexArray(p.GlVertexArray);
GLHelpers.ThrowOnResourceError(_gl, $"bind pipeline '{description.Name}' VAO");
+ // Campaign V slice V6d: the device's texture table is bound with the
+ // pipeline, the GL analogue of the Vulkan backend binding descriptor
+ // set 2 on every draw. It has to happen here rather than once per frame
+ // because every raw-GL world renderer binds its OWN private handle
+ // table at this same binding before its own draws, with its own slot
+ // numbering; an RHI shader that read that instead would sample a
+ // plausible but entirely unrelated texture. Removed at V4h with the
+ // per-renderer tables.
+ _gl.BindBufferBase(
+ GLEnum.ShaderStorageBuffer,
+ GpuBindingModel.StorageTextureTable,
+ _device.TextureTableGlName);
+ GLHelpers.ThrowOnResourceError(_gl, $"bind pipeline '{description.Name}' texture table");
+
// Push constants "survive pipeline changes within a pass" per the
// IGpuPassEncoder contract. GL uniforms are per-program state, so the
// GL backend must explicitly re-apply the last value to the newly
@@ -247,7 +276,7 @@ internal sealed class GlGpuPassEncoder : IGpuPassEncoder
// opened (see the constructor's comment) so a still-raw-GL renderer
// running immediately after this pass sees exactly what it would have
// seen had this pass never bound a pipeline.
- _ambientOnEntry.Restore(_gl);
+ _ambientOnEntry.Restore(_ambientApi);
_frame.ClosePass(this);
}
@@ -269,125 +298,3 @@ internal sealed class GlGpuPassEncoder : IGpuPassEncoder
}
}
-///
-/// Every ambient GL capability/binding a bind (or
-/// a dynamic setter) can change, captured by raw query and restored by raw
-/// call — the same field list TextRenderGlStateScope already restores
-/// around TextRenderer.Flush, generalized to every
-/// so a renderer with no scope of its own
-/// (DebugLineRenderer today) does not leak state forward into the next
-/// raw-GL renderer either. Transitional for as long as raw-GL renderers
-/// coexist with RHI-ported ones; deleted at V4h.
-///
-internal readonly struct GlAmbientCapabilityState
-{
- private readonly int _program;
- private readonly int _vertexArray;
- private readonly int _arrayBuffer;
- private readonly int _activeTexture;
- private readonly int _texture0Binding2D;
- private readonly bool _depthTest;
- private readonly bool _depthWrite;
- private readonly int _depthFunc;
- private readonly bool _blend;
- private readonly int _blendSourceRgb;
- private readonly int _blendDestinationRgb;
- private readonly int _blendSourceAlpha;
- private readonly int _blendDestinationAlpha;
- private readonly bool _cullFace;
- private readonly int _cullFaceMode;
- private readonly int _frontFace;
- private readonly bool _alphaToCoverage;
- private readonly bool _multisample;
-
- private GlAmbientCapabilityState(
- int program, int vertexArray, int arrayBuffer, int activeTexture, int texture0Binding2D,
- bool depthTest, bool depthWrite, int depthFunc,
- bool blend, int blendSourceRgb, int blendDestinationRgb, int blendSourceAlpha, int blendDestinationAlpha,
- bool cullFace, int cullFaceMode, int frontFace,
- bool alphaToCoverage, bool multisample)
- {
- _program = program;
- _vertexArray = vertexArray;
- _arrayBuffer = arrayBuffer;
- _activeTexture = activeTexture;
- _texture0Binding2D = texture0Binding2D;
- _depthTest = depthTest;
- _depthWrite = depthWrite;
- _depthFunc = depthFunc;
- _blend = blend;
- _blendSourceRgb = blendSourceRgb;
- _blendDestinationRgb = blendDestinationRgb;
- _blendSourceAlpha = blendSourceAlpha;
- _blendDestinationAlpha = blendDestinationAlpha;
- _cullFace = cullFace;
- _cullFaceMode = cullFaceMode;
- _frontFace = frontFace;
- _alphaToCoverage = alphaToCoverage;
- _multisample = multisample;
- }
-
- internal static GlAmbientCapabilityState Capture(GL gl)
- {
- gl.GetInteger(GetPName.CurrentProgram, out int program);
- gl.GetInteger(GetPName.VertexArrayBinding, out int vertexArray);
- gl.GetInteger(GetPName.ArrayBufferBinding, out int arrayBuffer);
- gl.GetInteger(GetPName.ActiveTexture, out int activeTexture);
- int texture0Binding2D;
- gl.ActiveTexture(TextureUnit.Texture0);
- gl.GetInteger(GetPName.TextureBinding2D, out texture0Binding2D);
- gl.ActiveTexture((TextureUnit)activeTexture);
-
- gl.GetInteger(GetPName.DepthFunc, out int depthFunc);
- gl.GetInteger(GetPName.BlendSrcRgb, out int blendSourceRgb);
- gl.GetInteger(GetPName.BlendDstRgb, out int blendDestinationRgb);
- gl.GetInteger(GetPName.BlendSrcAlpha, out int blendSourceAlpha);
- gl.GetInteger(GetPName.BlendDstAlpha, out int blendDestinationAlpha);
- gl.GetInteger(GetPName.CullFaceMode, out int cullFaceMode);
- gl.GetInteger(GetPName.FrontFace, out int frontFace);
-
- return new GlAmbientCapabilityState(
- program, vertexArray, arrayBuffer, activeTexture, texture0Binding2D,
- gl.IsEnabled(EnableCap.DepthTest), gl.GetBoolean(GetPName.DepthWritemask), depthFunc,
- gl.IsEnabled(EnableCap.Blend), blendSourceRgb, blendDestinationRgb, blendSourceAlpha, blendDestinationAlpha,
- gl.IsEnabled(EnableCap.CullFace), cullFaceMode, frontFace,
- gl.IsEnabled(EnableCap.SampleAlphaToCoverage), gl.IsEnabled(EnableCap.Multisample));
- }
-
- internal void Restore(GL gl)
- {
- gl.UseProgram((uint)_program);
- gl.BindVertexArray((uint)_vertexArray);
- gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBuffer);
-
- gl.ActiveTexture(TextureUnit.Texture0);
- gl.BindTexture(TextureTarget.Texture2D, (uint)_texture0Binding2D);
- gl.ActiveTexture((TextureUnit)_activeTexture);
-
- SetCapability(gl, EnableCap.DepthTest, _depthTest);
- gl.DepthMask(_depthWrite);
- gl.DepthFunc((DepthFunction)_depthFunc);
-
- SetCapability(gl, EnableCap.Blend, _blend);
- gl.BlendFuncSeparate(
- (BlendingFactor)_blendSourceRgb,
- (BlendingFactor)_blendDestinationRgb,
- (BlendingFactor)_blendSourceAlpha,
- (BlendingFactor)_blendDestinationAlpha);
-
- SetCapability(gl, EnableCap.CullFace, _cullFace);
- gl.CullFace((TriangleFace)_cullFaceMode);
- gl.FrontFace((FrontFaceDirection)_frontFace);
-
- SetCapability(gl, EnableCap.SampleAlphaToCoverage, _alphaToCoverage);
- SetCapability(gl, EnableCap.Multisample, _multisample);
- }
-
- private static void SetCapability(GL gl, EnableCap capability, bool enabled)
- {
- if (enabled)
- gl.Enable(capability);
- else
- gl.Disable(capability);
- }
-}
diff --git a/src/AcDream.App/Rendering/Gpu/Vk/VulkanGpuTexture.cs b/src/AcDream.App/Rendering/Gpu/Vk/VulkanGpuTexture.cs
index 8b88c48d..19f6b0a0 100644
--- a/src/AcDream.App/Rendering/Gpu/Vk/VulkanGpuTexture.cs
+++ b/src/AcDream.App/Rendering/Gpu/Vk/VulkanGpuTexture.cs
@@ -111,7 +111,13 @@ internal sealed unsafe class VulkanGpuTexture : IGpuTexture
{
SType = StructureType.ImageViewCreateInfo,
Image = image,
- ViewType = VulkanTextureFormatMapping.ViewTypeOf(description.Kind),
+ // Slice V6d: a texture that is not an attachment exists to be
+ // sampled, and every sampled view has to be layered because the
+ // global table's descriptor type is sampler2DArray. Attachments
+ // keep the literal view type of their kind.
+ ViewType = renderTarget
+ ? VulkanTextureFormatMapping.ViewTypeOf(description.Kind)
+ : VulkanTextureFormatMapping.SampledViewTypeOf(description.Kind),
Format = VkFormat,
SubresourceRange = new ImageSubresourceRange
{
diff --git a/src/AcDream.App/Rendering/Gpu/Vk/VulkanTextureFormatMapping.cs b/src/AcDream.App/Rendering/Gpu/Vk/VulkanTextureFormatMapping.cs
index 0b31dca9..3d84dda6 100644
--- a/src/AcDream.App/Rendering/Gpu/Vk/VulkanTextureFormatMapping.cs
+++ b/src/AcDream.App/Rendering/Gpu/Vk/VulkanTextureFormatMapping.cs
@@ -126,6 +126,32 @@ internal static class VulkanTextureFormatMapping
_ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unknown texture kind."),
};
+ ///
+ /// The view type for a texture that will be REGISTERED INTO THE GLOBAL
+ /// TABLE, which is always layered.
+ ///
+ /// The table is a single descriptor array and a descriptor array has
+ /// one element type: sampler2DArray (plan §4.4). A view whose type is
+ /// cannot legally back such a descriptor,
+ /// so a plain 2-D texture is viewed as a one-layer array instead. The image
+ /// itself is unchanged — arrayLayers is already 1 — and a one-layer
+ /// array view over it costs nothing.
+ ///
+ /// This is what lets a renderer create
+ /// and have it work on both backends: GL reconstructs a sampler2D
+ /// from the entry's bindless handle, Vulkan reads layer 0 of the array. The
+ /// ACDREAM_SAMPLE_2D macro is the shader-side half of the same
+ /// arrangement.
+ ///
+ /// Attachment views keep : an attachment is not
+ /// a table entry, and its view type is answerable from the pass alone.
+ ///
+ internal static ImageViewType SampledViewTypeOf(GpuTextureKind kind) => kind switch
+ {
+ GpuTextureKind.Texture2D or GpuTextureKind.Texture2DArray => ImageViewType.Type2DArray,
+ _ => throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unknown texture kind."),
+ };
+
internal static Filter FilterOf(GpuFilter filter) => filter switch
{
GpuFilter.Nearest => Filter.Nearest,
diff --git a/src/AcDream.App/Rendering/Shader.cs b/src/AcDream.App/Rendering/Shader.cs
index b74960ae..2d3be01c 100644
--- a/src/AcDream.App/Rendering/Shader.cs
+++ b/src/AcDream.App/Rendering/Shader.cs
@@ -51,8 +51,13 @@ public sealed class Shader : IDisposable
/// #version to be the very first statement in the source, so the
/// preamble cannot simply be prepended — it has to land after that block,
/// before the first real declaration.
+ ///
+ /// Internal rather than private since slice V6d: GlGpuDevice.CreatePipeline
+ /// loads RHI shader pairs itself and has to splice the same preamble the
+ /// same way. Two implementations of "where does the preamble go" is exactly
+ /// the kind of drift that shows up as one shader silently missing a binding.
///
- private static string InjectPreamble(string source, string preamble)
+ internal static string InjectPreamble(string source, string preamble)
{
int insertAt = 0;
int lineStart = 0;
diff --git a/src/AcDream.App/Rendering/Shaders/common.glsl b/src/AcDream.App/Rendering/Shaders/common.glsl
index f0234359..21c0873a 100644
--- a/src/AcDream.App/Rendering/Shaders/common.glsl
+++ b/src/AcDream.App/Rendering/Shaders/common.glsl
@@ -39,3 +39,18 @@ layout(std430, binding = 9) readonly buffer TextureTableBuf {
// existing call site already follows that exact pattern and a function cannot
// return an opaque sampler type built from a runtime value in GLSL.
#define ACDREAM_TEXTURE_HANDLE(idx) gTextureTable[idx]
+
+// Campaign V slice V6d: samples a table slot that holds a plain 2-D texture.
+//
+// The two backends disagree about what a 2-D table entry IS, and this macro is
+// the one place that difference lives. Under GL a bindless handle carries its
+// own texture type, so a GL_TEXTURE_2D entry is reconstructed as a sampler2D
+// and read with a 2-component UV. Under Vulkan the table is one descriptor
+// array whose element type is fixed at sampler2DArray, so the same entry is a
+// one-layer array read at layer 0 (see tools/ShaderCompiler/VulkanGlslPreamble.cs).
+//
+// That asymmetry is deliberate and is what keeps the retained UI's textures
+// exactly as they are on GL — including the paperdoll/appraisal FBO colour
+// texture, which is an externally-owned GL_TEXTURE_2D registered by the §7.1
+// transitional seam and cannot be made an array before V4g moves its renderer.
+#define ACDREAM_SAMPLE_2D(idx, uv) texture(sampler2D(gTextureTable[idx]), uv)
diff --git a/src/AcDream.App/Rendering/Shaders/debug_line.vert b/src/AcDream.App/Rendering/Shaders/debug_line.vert
index f6340133..145bcd18 100644
--- a/src/AcDream.App/Rendering/Shaders/debug_line.vert
+++ b/src/AcDream.App/Rendering/Shaders/debug_line.vert
@@ -2,12 +2,21 @@
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aColor;
-uniform mat4 uView;
-uniform mat4 uProjection;
+// Campaign V slice V6d: the separate uView/uProjection pair converged into the
+// pinned GpuPushConstants block's single uViewProjection. Vulkan has no default
+// uniform block, so a loose uniform cannot exist there at all — a shader either
+// reads a field of the shared push-constant block or it cannot be expressed.
+//
+// The product now happens once on the CPU (DebugLineRenderer.Flush) instead of
+// once per vertex, so the rounding differs in the last bits. That is why this
+// convergence is measured on its own: these lines are diagnostic-only geometry,
+// drawn only when collision wireframes are switched on, so the offline gate
+// reads nothing but its own ambient noise.
+uniform mat4 uViewProjection;
out vec3 vColor;
void main() {
vColor = aColor;
- gl_Position = uProjection * uView * vec4(aPos, 1.0);
+ gl_Position = uViewProjection * vec4(aPos, 1.0);
}
diff --git a/src/AcDream.App/Rendering/Shaders/spv/debug_line.frag.spv b/src/AcDream.App/Rendering/Shaders/spv/debug_line.frag.spv
new file mode 100644
index 00000000..7d27c512
Binary files /dev/null and b/src/AcDream.App/Rendering/Shaders/spv/debug_line.frag.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/spv/debug_line.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/debug_line.vert.spv
new file mode 100644
index 00000000..82c2f533
Binary files /dev/null and b/src/AcDream.App/Rendering/Shaders/spv/debug_line.vert.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json
index 0da87a4d..a57f56cb 100644
--- a/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json
+++ b/src/AcDream.App/Rendering/Shaders/spv/shaders.manifest.json
@@ -3,13 +3,12 @@
"shaders": [
{
"name": "debug_line",
- "vulkanReady": false,
+ "vulkanReady": true,
"stages": [
{
"stage": "vert",
- "sourceSha256": "e6a535ed722a034482cfe09e15ac2308ecde2bb54bc7d303cb5874b5b347eb61",
- "compiled": false,
- "message": "debug_line.vert:62: error: \u0027uProjection\u0027 : undeclared identifier"
+ "sourceSha256": "069ef7c89eea80c68e28f44b9e065c5cd3cc9220e8ba63261d345377025c2ea1",
+ "compiled": true
},
{
"stage": "frag",
@@ -26,13 +25,13 @@
"stage": "vert",
"sourceSha256": "c35f767ab07fa9df805f9e77f4851f517c153dd2ef2efa6d49d0c24b688e4f56",
"compiled": false,
- "message": "mesh.vert:70: error: \u0027uModel\u0027 : undeclared identifier"
+ "message": "mesh.vert:71: error: \u0027uModel\u0027 : undeclared identifier"
},
{
"stage": "frag",
"sourceSha256": "4d6478543a9a903a3453581fa847e096aaecf01f38ebb2921572663bad8e24ea",
"compiled": false,
- "message": "mesh.frag:157: error: \u0027uDiffuse\u0027 : undeclared identifier"
+ "message": "mesh.frag:158: error: \u0027uDiffuse\u0027 : undeclared identifier"
}
]
},
@@ -44,13 +43,13 @@
"stage": "vert",
"sourceSha256": "1ec2f4af83e73102d87997244a35b69ad5e9ece4b1ad78e2b5ece4d58fab5530",
"compiled": false,
- "message": "mesh_modern.vert:379: error: \u0027assign\u0027 : cannot convert from \u0027 global highp uint\u0027 to \u0027layout( location=4) flat out highp 2-component vector of uint\u0027"
+ "message": "mesh_modern.vert:380: error: \u0027assign\u0027 : cannot convert from \u0027 global highp uint\u0027 to \u0027layout( location=4) flat out highp 2-component vector of uint\u0027"
},
{
"stage": "frag",
"sourceSha256": "3aea96cba6c2afc49caae7545f9e42603b6b17afa50b3254beca60f95af5d2f3",
"compiled": false,
- "message": "mesh_modern.frag:105: error: \u0027sampler2DArray\u0027 : sampler-constructor requires the extension GL_ARB_bindless_texture enabled"
+ "message": "mesh_modern.frag:106: error: \u0027sampler2DArray\u0027 : sampler-constructor requires the extension GL_ARB_bindless_texture enabled"
}
]
},
@@ -62,13 +61,13 @@
"stage": "vert",
"sourceSha256": "6a6ebeaacba95e5e4e8a308ed7c4cd805b80f305650c1e9e03e2bdfc6c18f5e7",
"compiled": false,
- "message": "particle.vert:82: error: \u0027assign\u0027 : cannot convert from \u0027layout( location=6) in highp uint\u0027 to \u0027layout( location=2) flat out highp 2-component vector of uint\u0027"
+ "message": "particle.vert:83: error: \u0027assign\u0027 : cannot convert from \u0027layout( location=6) in highp uint\u0027 to \u0027layout( location=2) flat out highp 2-component vector of uint\u0027"
},
{
"stage": "frag",
"sourceSha256": "3924ecbabf051349bc6a13e6cff370725a0832f8baf515decdb7e0394304006d",
"compiled": false,
- "message": "particle.frag:59: error: \u0027sampler2DArray\u0027 : sampler-constructor requires the extension GL_ARB_bindless_texture enabled"
+ "message": "particle.frag:60: error: \u0027sampler2DArray\u0027 : sampler-constructor requires the extension GL_ARB_bindless_texture enabled"
}
]
},
@@ -85,7 +84,7 @@
"stage": "frag",
"sourceSha256": "0da368243e967388990f4f4b90e2304044af6187de45f70499a3e4ece8dfd5a8",
"compiled": false,
- "message": "particle_mesh.frag:61: error: \u0027uTextureIndex\u0027 : undeclared identifier"
+ "message": "particle_mesh.frag:62: error: \u0027uTextureIndex\u0027 : undeclared identifier"
}
]
},
@@ -97,13 +96,13 @@
"stage": "vert",
"sourceSha256": "d338e9b03686b7baf79d5121c5c8d0f24037979cc58f203957d7bd97b02b1cc2",
"compiled": false,
- "message": "sky.vert:150: error: \u0027uUvScroll\u0027 : undeclared identifier"
+ "message": "sky.vert:151: error: \u0027uUvScroll\u0027 : undeclared identifier"
},
{
"stage": "frag",
"sourceSha256": "8084af39f65ae399c73e3ca864376ef20ba8a1c495ee4774be6a82af3872c51c",
"compiled": false,
- "message": "sky.frag:75: error: \u0027uDiffuse\u0027 : undeclared identifier"
+ "message": "sky.frag:76: error: \u0027uDiffuse\u0027 : undeclared identifier"
}
]
},
@@ -115,31 +114,29 @@
"stage": "vert",
"sourceSha256": "4de580ce11b8d755d3558dc49bf7ebccec54d307595d91c38b5c5d552d645c7e",
"compiled": false,
- "message": "terrain_modern.vert:218: error: \u0027uProjection\u0027 : undeclared identifier"
+ "message": "terrain_modern.vert:219: error: \u0027uProjection\u0027 : undeclared identifier"
},
{
"stage": "frag",
"sourceSha256": "6003b81df6da6cbea7f00310bd956348bc7b2525345dd490b0b6a3b6428340d9",
"compiled": false,
- "message": "terrain_modern.frag:107: error: \u0027uTexTiling\u0027 : undeclared identifier"
+ "message": "terrain_modern.frag:108: error: \u0027uTexTiling\u0027 : undeclared identifier"
}
]
},
{
"name": "ui_text",
- "vulkanReady": false,
+ "vulkanReady": true,
"stages": [
{
"stage": "vert",
- "sourceSha256": "6c4b0cb8b05da648a5e335db6747cb239dd1fbf95333b658557f52b39eadf4a3",
- "compiled": false,
- "message": "ui_text.vert:64: error: \u0027uScreenSize\u0027 : undeclared identifier"
+ "sourceSha256": "4ddc52f18ea953b33e9263dc2703397f63f4b104bc45d67edc06531145bd9d53",
+ "compiled": true
},
{
"stage": "frag",
- "sourceSha256": "7287a9f19530979b00de01a8f6c3865905ae3f72e5f219ce228b41915c58d60d",
- "compiled": false,
- "message": "ui_text.frag:57: error: \u0027uUseTexture\u0027 : undeclared identifier"
+ "sourceSha256": "43cb53aa8c933f7800142b72ea81bafc923fd7039fd460022d55543081d0aee5",
+ "compiled": true
}
]
},
diff --git a/src/AcDream.App/Rendering/Shaders/spv/ui_text.frag.spv b/src/AcDream.App/Rendering/Shaders/spv/ui_text.frag.spv
new file mode 100644
index 00000000..9c632c3e
Binary files /dev/null and b/src/AcDream.App/Rendering/Shaders/spv/ui_text.frag.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/spv/ui_text.vert.spv b/src/AcDream.App/Rendering/Shaders/spv/ui_text.vert.spv
new file mode 100644
index 00000000..5d5b87a4
Binary files /dev/null and b/src/AcDream.App/Rendering/Shaders/spv/ui_text.vert.spv differ
diff --git a/src/AcDream.App/Rendering/Shaders/ui_text.frag b/src/AcDream.App/Rendering/Shaders/ui_text.frag
index 75c9cd3d..f0454e34 100644
--- a/src/AcDream.App/Rendering/Shaders/ui_text.frag
+++ b/src/AcDream.App/Rendering/Shaders/ui_text.frag
@@ -1,19 +1,45 @@
#version 430 core
+#extension GL_ARB_bindless_texture : require
in vec2 vUv;
in vec4 vColor;
out vec4 FragColor;
-uniform sampler2D uTex;
-uniform int uUseTexture;
+// Campaign V slice V6d: the retained UI samples the device's global texture
+// table instead of whatever happened to be bound to texture unit 0. The old
+// `uniform sampler2D uTex` plus `uniform int uUseTexture` pair could not exist
+// under Vulkan — there is no default uniform block, and an opaque sampler
+// cannot be a push constant — so both are expressed with the pinned block's two
+// texture-table slots. There is no third field and none was needed: which slots
+// are ASSIGNED is itself the mode.
+//
+// uTextureIndexB assigned -> single-channel coverage source (a font atlas):
+// red is the glyph's alpha, and the colour's RGB
+// is NOT multiplied by it.
+// uTextureIndexA assigned -> RGBA colour source (dat chrome, icons, sprites),
+// modulated by the vertex colour/tint.
+// neither assigned -> a flat quad in the vertex colour. Untextured
+// rects and DrawFill take this path; the old
+// shader's uUseTexture==0 branch is unchanged.
+//
+// Exactly one of the two is ever assigned at a time, so the branch is uniform
+// across a draw.
+uniform uint uTextureIndexA;
+uniform uint uTextureIndexB;
+
+// GpuTextureSlot.Unassigned. It is a loud sentinel precisely so it can be
+// tested for rather than silently resolving to slot 0 — the failure mode that
+// produced the magenta 1x1 UI placeholder. Both branches below are guarded, so
+// it never reaches a sampler.
+const uint kUnassignedTextureSlot = 0xFFFFFFFFu;
void main() {
- if (uUseTexture == 1) {
+ if (uTextureIndexB != kUnassignedTextureSlot) {
// Font atlas is a single-channel R8 texture; red = coverage alpha.
- float coverage = texture(uTex, vUv).r;
+ float coverage = ACDREAM_SAMPLE_2D(uTextureIndexB, vUv).r;
FragColor = vec4(vColor.rgb, vColor.a * coverage);
- } else if (uUseTexture == 2) {
+ } else if (uTextureIndexA != kUnassignedTextureSlot) {
// RGBA dat sprite (decoded to RGBA8); modulate by tint/alpha.
- FragColor = texture(uTex, vUv) * vColor;
+ FragColor = ACDREAM_SAMPLE_2D(uTextureIndexA, vUv) * vColor;
} else {
FragColor = vColor;
}
diff --git a/src/AcDream.App/Rendering/Shaders/ui_text.vert b/src/AcDream.App/Rendering/Shaders/ui_text.vert
index 0cc6c932..30d0cf71 100644
--- a/src/AcDream.App/Rendering/Shaders/ui_text.vert
+++ b/src/AcDream.App/Rendering/Shaders/ui_text.vert
@@ -3,16 +3,28 @@ layout(location = 0) in vec2 aPos; // screen pixels, origin top-left
layout(location = 1) in vec2 aUv;
layout(location = 2) in vec4 aColor;
-uniform vec2 uScreenSize;
+// Campaign V slice V6d: uScreenSize moved into the pinned GpuPushConstants
+// block's two spare scalars — uParamA is the framebuffer width in pixels,
+// uParamB its height. Vulkan has no default uniform block, so a loose
+// `uniform vec2` cannot exist there; the block's fields are the only uniforms
+// a shader can read, and the two unclaimed scalars are exactly the right shape.
+//
+// The arithmetic below is byte-for-byte what it was: the same two divisions and
+// the same NDC mapping, reading two floats instead of one vec2.
+uniform float uParamA;
+uniform float uParamB;
out vec2 vUv;
out vec4 vColor;
void main() {
// Convert pixel coords (origin top-left, +Y down) to NDC (origin center, +Y up).
+ // This is GL-convention NDC on both backends: the Vulkan backend renders with
+ // a negative viewport height, so it consumes the same +Y-up clip space and no
+ // renderer performs a flip of its own.
vec2 ndc = vec2(
- aPos.x / uScreenSize.x * 2.0 - 1.0,
- 1.0 - aPos.y / uScreenSize.y * 2.0);
+ aPos.x / uParamA * 2.0 - 1.0,
+ 1.0 - aPos.y / uParamB * 2.0);
gl_Position = vec4(ndc, 0.0, 1.0);
vUv = aUv;
vColor = aColor;
diff --git a/src/AcDream.App/Rendering/TextRenderGlStateScope.cs b/src/AcDream.App/Rendering/TextRenderGlStateScope.cs
deleted file mode 100644
index b65123b7..00000000
--- a/src/AcDream.App/Rendering/TextRenderGlStateScope.cs
+++ /dev/null
@@ -1,156 +0,0 @@
-using Silk.NET.OpenGL;
-
-namespace AcDream.App.Rendering;
-
-internal interface ITextRenderGlStateApi
-{
- bool IsEnabled(EnableCap capability);
-
- int GetInteger(GetPName parameter);
-
- bool GetBoolean(GetPName parameter);
-
- void SetCapability(EnableCap capability, bool enabled);
-
- void DepthMask(bool enabled);
-
- void BlendFuncSeparate(
- BlendingFactor sourceRgb,
- BlendingFactor destinationRgb,
- BlendingFactor sourceAlpha,
- BlendingFactor destinationAlpha);
-
- void UseProgram(uint program);
-
- void BindVertexArray(uint vertexArray);
-
- void BindBuffer(BufferTargetARB target, uint buffer);
-
- void ActiveTexture(TextureUnit unit);
-
- void BindTexture(TextureTarget target, uint texture);
-}
-
-internal sealed class SilkTextRenderGlStateApi : ITextRenderGlStateApi
-{
- private readonly GL _gl;
-
- public SilkTextRenderGlStateApi(GL gl) =>
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
-
- public bool IsEnabled(EnableCap capability) => _gl.IsEnabled(capability);
-
- public int GetInteger(GetPName parameter) => _gl.GetInteger(parameter);
-
- public bool GetBoolean(GetPName parameter) => _gl.GetBoolean(parameter);
-
- public void SetCapability(EnableCap capability, bool enabled)
- {
- if (enabled)
- _gl.Enable(capability);
- else
- _gl.Disable(capability);
- }
-
- public void DepthMask(bool enabled) => _gl.DepthMask(enabled);
-
- public void BlendFuncSeparate(
- BlendingFactor sourceRgb,
- BlendingFactor destinationRgb,
- BlendingFactor sourceAlpha,
- BlendingFactor destinationAlpha) =>
- _gl.BlendFuncSeparate(
- sourceRgb,
- destinationRgb,
- sourceAlpha,
- destinationAlpha);
-
- public void UseProgram(uint program) => _gl.UseProgram(program);
-
- public void BindVertexArray(uint vertexArray) => _gl.BindVertexArray(vertexArray);
-
- public void BindBuffer(BufferTargetARB target, uint buffer) =>
- _gl.BindBuffer(target, buffer);
-
- public void ActiveTexture(TextureUnit unit) => _gl.ActiveTexture(unit);
-
- public void BindTexture(TextureTarget target, uint texture) =>
- _gl.BindTexture(target, texture);
-}
-
-///
-/// Exact, focused state transaction for . It
-/// captures every GL value that Flush or DrawLayer mutates, while avoiding the
-/// dozens of unrelated synchronous reads made by the broad diagnostic scope.
-///
-internal readonly struct TextRenderGlStateScope : IDisposable
-{
- private readonly ITextRenderGlStateApi _gl;
- private readonly bool _depthTest;
- private readonly bool _blend;
- private readonly bool _cullFace;
- private readonly bool _alphaToCoverage;
- private readonly bool _multisample;
- private readonly bool _depthWrite;
- private readonly int _blendSourceRgb;
- private readonly int _blendDestinationRgb;
- private readonly int _blendSourceAlpha;
- private readonly int _blendDestinationAlpha;
- private readonly int _program;
- private readonly int _vertexArray;
- private readonly int _arrayBuffer;
- private readonly int _activeTexture;
- private readonly int _texture0Binding2D;
-
- public TextRenderGlStateScope(ITextRenderGlStateApi gl)
- {
- _gl = gl ?? throw new ArgumentNullException(nameof(gl));
- _depthTest = gl.IsEnabled(EnableCap.DepthTest);
- _blend = gl.IsEnabled(EnableCap.Blend);
- _cullFace = gl.IsEnabled(EnableCap.CullFace);
- _alphaToCoverage = gl.IsEnabled(EnableCap.SampleAlphaToCoverage);
- _multisample = gl.IsEnabled(EnableCap.Multisample);
- _depthWrite = gl.GetBoolean(GetPName.DepthWritemask);
- _blendSourceRgb = gl.GetInteger(GetPName.BlendSrcRgb);
- _blendDestinationRgb = gl.GetInteger(GetPName.BlendDstRgb);
- _blendSourceAlpha = gl.GetInteger(GetPName.BlendSrcAlpha);
- _blendDestinationAlpha = gl.GetInteger(GetPName.BlendDstAlpha);
- _program = gl.GetInteger(GetPName.CurrentProgram);
- _vertexArray = gl.GetInteger(GetPName.VertexArrayBinding);
- _arrayBuffer = gl.GetInteger(GetPName.ArrayBufferBinding);
- _activeTexture = gl.GetInteger(GetPName.ActiveTexture);
-
- try
- {
- gl.ActiveTexture(TextureUnit.Texture0);
- _texture0Binding2D = gl.GetInteger(GetPName.TextureBinding2D);
- }
- finally
- {
- gl.ActiveTexture((TextureUnit)_activeTexture);
- }
- }
-
- public void Dispose()
- {
- _gl.UseProgram((uint)_program);
- _gl.BindVertexArray((uint)_vertexArray);
- _gl.BindBuffer(BufferTargetARB.ArrayBuffer, (uint)_arrayBuffer);
-
- _gl.ActiveTexture(TextureUnit.Texture0);
- _gl.BindTexture(TextureTarget.Texture2D, (uint)_texture0Binding2D);
- _gl.ActiveTexture((TextureUnit)_activeTexture);
-
- _gl.DepthMask(_depthWrite);
- _gl.BlendFuncSeparate(
- (BlendingFactor)_blendSourceRgb,
- (BlendingFactor)_blendDestinationRgb,
- (BlendingFactor)_blendSourceAlpha,
- (BlendingFactor)_blendDestinationAlpha);
- _gl.SetCapability(EnableCap.DepthTest, _depthTest);
- _gl.SetCapability(EnableCap.Blend, _blend);
- _gl.SetCapability(EnableCap.CullFace, _cullFace);
- _gl.SetCapability(EnableCap.SampleAlphaToCoverage, _alphaToCoverage);
- _gl.SetCapability(EnableCap.Multisample, _multisample);
- }
-}
diff --git a/src/AcDream.App/Rendering/TextRenderer.cs b/src/AcDream.App/Rendering/TextRenderer.cs
index 20e329d4..406ca5f3 100644
--- a/src/AcDream.App/Rendering/TextRenderer.cs
+++ b/src/AcDream.App/Rendering/TextRenderer.cs
@@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.Numerics;
using System.Runtime.InteropServices;
using AcDream.App.Rendering.Gpu;
-using AcDream.App.Rendering.Gpu.Gl;
-using Silk.NET.OpenGL;
namespace AcDream.App.Rendering;
@@ -16,32 +14,38 @@ namespace AcDream.App.Rendering;
///
/// Campaign V slice V4a: the ui_text shader compiles through
/// (one ,
-/// replacing the old hand-rolled Shader class), its three
+/// replacing the old hand-rolled Shader class) and its three
/// fence-buffered per-flight VBOs are gone in favour of a per-
-/// ring allocation per draw bucket, and the 1×1 white fill texture is created
-/// and uploaded through and registered
-/// into the device's texture table.
+/// ring allocation per draw bucket.
///
-/// has no verb for classic texture-unit binding
-/// (every acdream RHI pass samples through the bindless texture table) but
-/// receives an ARBITRARY externally-owned raw GL
-/// texture name from dozens of UI call sites that are not part of this slice
-/// (icons, dat chrome, composited item art) — converting that whole surface
-/// to slot-based sampling is out of scope here. So sprite/font texture
-/// binding stays classic (glActiveTexture/glBindTexture,
-/// ui_text.frag's uTex sampler unchanged) issued directly against
-/// the GL handle this class keeps for that reason, while the shader program
-/// itself, its blend/depth/cull description, and every per-frame vertex
-/// upload now go through the RHI. This mirrors the same GL-only escape hatch
-/// the campaign's V2 note already documents for the interim bindless handle
-/// table, and is retired only when a later slice moves ALL of TextRenderer's
-/// texture consumers onto registered slots.
+/// Campaign V slice V6d finished the job: this class no longer touches
+/// GL at all, and is the first production renderer that draws on either
+/// backend. Three things had to change for that.
+///
+/// Textures. V4a kept a classic glActiveTexture/glBindTexture
+/// path because receives an arbitrary texture from
+/// dozens of widget call sites. Those textures are all registered into the
+/// device's global table by TextureCache already — the classic path was
+/// only ever consuming the raw GL name that registration also produced. They
+/// now travel as values instead, and the
+/// shader samples the table.
+///
+/// Loose uniforms. uScreenSize and uUseTexture moved
+/// into the pinned block. Screen size is the
+/// block's two spare scalars; the sampling mode is derived from which of the
+/// two texture-table slots is assigned, so no new field was needed. See
+/// ui_text.frag for the three cases.
+///
+/// GL capability state. The pass no longer disables multisampling
+/// by hand — GlGpuPassEncoder derives that from the pass's SampleCount
+/// and restores it on close, which is where pass state belongs and which the
+/// Vulkan backend gets from the pass description for free.
///
/// Uses per-bucket ring allocations flushed in up to three draw calls per
/// layer, to avoid a per-vertex "use texture" flag. Rects are drawn first so
/// text sits on top of background panels.
///
-public sealed unsafe class TextRenderer : IDisposable
+public sealed class TextRenderer : IDisposable
{
private const int FloatsPerVertex = 8; // pos(2) + uv(2) + color(4)
private const int VertexStrideBytes = FloatsPerVertex * sizeof(float);
@@ -54,16 +58,8 @@ public sealed unsafe class TextRenderer : IDisposable
new GpuVertexAttribute(2, GpuVertexFormat.Float4, 16),
]);
- private readonly IGpuDevice _device;
- private readonly GlGpuDevice _glDevice;
private readonly ICurrentGpuFrameSource _frameSource;
- private readonly GL _gl;
- private readonly ITextRenderGlStateApi _glState;
private readonly IGpuPipeline _pipeline;
- private readonly IGpuTexture _whiteTexture;
- private readonly uint _whiteTex; // 1×1 white, for solid fills routed through the sprite bucket
- private readonly int _uScreenSizeLocation;
- private readonly int _uUseTextureLocation;
private sealed class SpriteSeg { public uint Texture; public readonly List Verts = new(256); }
@@ -112,82 +108,28 @@ public sealed unsafe class TextRenderer : IDisposable
// change of their own.
internal TextRenderer(IGpuDevice device, ICurrentGpuFrameSource frameSource, string shaderDir)
{
- _device = device ?? throw new ArgumentNullException(nameof(device));
+ ArgumentNullException.ThrowIfNull(device);
_frameSource = frameSource ?? throw new ArgumentNullException(nameof(frameSource));
- if (device is not GlGpuDevice glDevice)
- {
- throw new NotSupportedException(
- "TextRenderer's classic sprite/font texture-unit binding path (see the class " +
- "remarks) is GL-only; it needs the raw GL handle GlGpuDevice exposes. Other " +
- "backends are out of scope until a later slice removes that classic path.");
- }
- _glDevice = glDevice;
- _gl = glDevice.Gl;
- _glState = new SilkTextRenderGlStateApi(_gl);
ArgumentException.ThrowIfNullOrWhiteSpace(shaderDir);
- IGpuPipeline? pipeline = null;
- IGpuTexture? whiteTexture = null;
- try
+ _pipeline = device.CreatePipeline(new GpuPipelineDescription
{
- pipeline = device.CreatePipeline(new GpuPipelineDescription
- {
- Name = "ui-text",
- Shaders = new GpuShaderSet("ui_text"),
- VertexLayout = SpriteVertexLayout,
- Topology = GpuPrimitiveTopology.TriangleList,
- Blend = GpuBlendMode.StraightAlpha,
- // The retained UI is a self-contained 2-D pass — depth is
- // irrelevant and the world pass's alpha-to-coverage state must
- // not leak in (feedback_render_self_contained_gl_state). Flush
- // below still asserts this by hand via raw GL calls (and
- // GL_MULTISAMPLE, which has no representation here), matching
- // what TextRenderGlStateScope has always restored on exit.
- Depth = GpuDepthState.Disabled,
- Cull = GpuCullMode.None,
- AlphaToCoverage = false,
- ColorWrite = true,
- SampleCount = 1,
- });
-
- // 1×1 white texture so DrawFill can route solid-colour quads through the SPRITE
- // bucket (the shader multiplies texel×color → white×color = color). Lets a panel
- // background draw UNDER its text in painter order, which DrawRect's separate
- // bucket cannot (it always composites after all sprites).
- whiteTexture = device.CreateTexture(new GpuTextureDescription(
- "ui-text-white",
- GpuTextureKind.Texture2D,
- GpuTextureFormat.Rgba8Unorm,
- Width: 1,
- Height: 1,
- LayerCount: 1,
- MipLevelCount: 1));
- whiteTexture.Upload(0, 0, [255, 255, 255, 255]);
- IGpuSampler whiteSampler = device.CreateSampler(GpuSamplerDescription.UiNearest);
- device.RegisterTexture(whiteTexture, whiteSampler);
- }
- catch
- {
- whiteTexture?.Dispose();
- pipeline?.Dispose();
- throw;
- }
-
- _pipeline = pipeline;
- _whiteTexture = whiteTexture;
- _whiteTex = ((GlGpuTexture)whiteTexture).GlName;
-
- uint program = ((GlGpuPipeline)_pipeline).GlProgram;
- _uScreenSizeLocation = _gl.GetUniformLocation(program, "uScreenSize");
- _uUseTextureLocation = _gl.GetUniformLocation(program, "uUseTexture");
- // uTex (the sampler unit) never changes — bind it once rather than on every Flush.
- int texLocation = _gl.GetUniformLocation(program, "uTex");
- if (texLocation >= 0)
- {
- _gl.UseProgram(program);
- _gl.Uniform1(texLocation, 0);
- _gl.UseProgram(0);
- }
+ Name = "ui-text",
+ Shaders = new GpuShaderSet("ui_text"),
+ VertexLayout = SpriteVertexLayout,
+ Topology = GpuPrimitiveTopology.TriangleList,
+ Blend = GpuBlendMode.StraightAlpha,
+ // The retained UI is a self-contained 2-D pass — depth is
+ // irrelevant and the world pass's alpha-to-coverage state must not
+ // leak in (feedback_render_self_contained_gl_state). Multisampling
+ // is the pass's business rather than the pipeline's and comes from
+ // the SampleCount below; see GlGpuPassEncoder's constructor.
+ Depth = GpuDepthState.Disabled,
+ Cull = GpuCullMode.None,
+ AlphaToCoverage = false,
+ ColorWrite = true,
+ SampleCount = 1,
+ });
}
/// Begin a HUD pass. Call once per frame before any Draw* calls.
@@ -218,9 +160,15 @@ public sealed unsafe class TextRenderer : IDisposable
/// when active), so it composites in painter order with sprites + dat-font text. Use
/// this — not — for a panel BACKGROUND that text draws on top of:
/// DrawRect's bucket always flushes after all sprites, so a rect background would cover
- /// the text instead.
+ /// the text instead.
+ ///
+ /// Slice V6d: this used to route through a 1×1 white texture, relying on
+ /// white × colour = colour. The shader now has an untextured branch that produces
+ /// the same value directly (multiplying by exactly 1.0 changes no bits), so the
+ /// white texture is gone and the fill is a sprite segment with no texture.
+ ///
public void DrawFill(float x, float y, float w, float h, Vector4 color)
- => DrawSprite(_whiteTex, x, y, w, h, 0f, 0f, 1f, 1f, color);
+ => DrawSprite(UiTextureTableHandle.None, x, y, w, h, 0f, 0f, 1f, 1f, color);
/// Draw a 1-pixel-thick outline rect.
public void DrawRectOutline(float x, float y, float w, float h, Vector4 color, float thickness = 1f)
@@ -318,8 +266,15 @@ public sealed unsafe class TextRenderer : IDisposable
///
/// Draw a textured sprite quad in screen pixel space with an explicit
- /// source-UV rectangle (for 9-slice / atlas sub-regions). Batched per
- /// GL texture handle; flushed with uUseTexture=2 (RGBA modulate).
+ /// source-UV rectangle (for 9-slice / atlas sub-regions).
+ ///
+ /// is a — a
+ /// one-based index into the device's global texture table, which is what
+ /// TextureCache now hands out in place of the raw GL name it used to.
+ /// Segments batch per handle and draw in submission order.
+ /// draws the tint alone; every
+ /// widget guards against passing it, and uses it
+ /// deliberately.
///
public void DrawSprite(uint texture, float x, float y, float w, float h,
float u0, float v0, float u1, float v1, Vector4 tint)
@@ -331,14 +286,18 @@ public sealed unsafe class TextRenderer : IDisposable
}
///
- /// Resolves a produced by the paperdoll/appraisal
+ /// Encodes a produced by the paperdoll/appraisal
/// viewport transitional seam (GlGpuDevice.RegisterExternalColorTexture,
- /// campaign doc §7.1) back to the raw GL texture name
- /// needs. Returns 0 (no texture) for an unassigned slot. GL-only; removed
- /// at V4g alongside the seam it resolves.
+ /// campaign doc §7.1) as the handle takes. Returns
+ /// for an unassigned slot.
+ ///
+ /// Slice V6d: this used to resolve the slot back to a raw GL texture
+ /// name for the classic binding path. Now that the UI samples the table, the
+ /// externally-owned texture needs no special treatment at draw time at all —
+ /// registration already put it in the table, and this is a plain encode.
///
- internal uint ResolveExternalTextureSlot(GpuTextureSlot slot) =>
- _glDevice.TryResolveExternalColorTexture(slot, out uint name) ? name : 0;
+ internal static uint ResolveExternalTextureSlot(GpuTextureSlot slot) =>
+ UiTextureTableHandle.FromSlot(slot);
/// Pick the sprite segment for : extend the current
/// same-texture run, else reuse a pooled segment, else allocate. Submission order is
@@ -401,10 +360,11 @@ public sealed unsafe class TextRenderer : IDisposable
// Retained UI is a private render pass: an upload or draw failure must
// not leak its depth/cull/blend/MSAA state into a later recoverable
- // frame. The focused scope restores from Flush's generated finally,
- // including when either DrawLayer call throws.
- using var stateScope = new TextRenderGlStateScope(_glState);
-
+ // frame. Slice V6d: the encoder's own `using` is that guarantee — the
+ // GL backend captures every ambient capability a pipeline bind can
+ // change when the pass opens and restores it on close, including when
+ // either DrawLayer call throws. That replaced this renderer's private
+ // GL state scope, which restored a strict subset of the same values.
using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription
{
Name = "ui-text",
@@ -422,21 +382,6 @@ public sealed unsafe class TextRenderer : IDisposable
SampleCount = 1,
});
encoder.BindPipeline(_pipeline);
- _gl.Uniform2(_uScreenSizeLocation, _screenSize.X, _screenSize.Y);
-
- // Establish the self-contained UI pass state.
- // The world pass leaves alpha-to-coverage + multisample enabled (WbDrawDispatcher,
- // QualitySettings MSAA). If they bleed into the UI pass, each glyph's soft alpha
- // EDGE is converted to dithered MSAA coverage instead of a clean alpha blend —
- // the "text not sharp / fuzzy" artifact. The UI composites with straight alpha
- // blending and must own this state (feedback_render_self_contained_gl_state).
- _gl.Disable(EnableCap.SampleAlphaToCoverage);
- _gl.Disable(EnableCap.Multisample);
- _gl.Disable(EnableCap.DepthTest);
- _gl.Disable(EnableCap.CullFace);
- _gl.DepthMask(false);
- _gl.Enable(EnableCap.Blend);
- _gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
// LAYERED compositing for the UI (background → fill → text):
// 1. RGBA dat sprites — window chrome / panel backgrounds (behind)
@@ -459,16 +404,14 @@ public sealed unsafe class TextRenderer : IDisposable
List textBuf, int textVerts, BitmapFont? font,
IGpuFrame frame, IGpuPassEncoder encoder)
{
- // 1. RGBA dat sprites — one draw call per distinct GL texture.
+ // 1. RGBA dat sprites — one draw call per distinct texture-table slot.
if (segUsed > 0)
{
- SetUseTexture(2);
- _gl.ActiveTexture(TextureUnit.Texture0);
for (int i = 0; i < segUsed; i++)
{
var seg = spriteSegs[i];
if (seg.Verts.Count == 0) continue;
- _gl.BindTexture(TextureTarget.Texture2D, seg.Texture);
+ SetTextures(encoder, colorHandle: seg.Texture, coverageHandle: UiTextureTableHandle.None);
DrawRing(frame, encoder, seg.Verts);
}
}
@@ -476,24 +419,33 @@ public sealed unsafe class TextRenderer : IDisposable
// 2. Untextured rects — widget fills on top of the chrome.
if (rectVerts > 0)
{
- SetUseTexture(0);
+ SetTextures(encoder, UiTextureTableHandle.None, UiTextureTableHandle.None);
DrawRing(frame, encoder, rectBuf);
}
- // 3. Textured debug-font text glyphs on top.
+ // 3. Textured debug-font text glyphs on top. The atlas is single-channel
+ // coverage, which is the coverage slot rather than the colour one.
if (textVerts > 0 && font is not null)
{
- SetUseTexture(1);
- _gl.ActiveTexture(TextureUnit.Texture0);
- _gl.BindTexture(TextureTarget.Texture2D, font.TextureId);
+ SetTextures(encoder, UiTextureTableHandle.None, coverageHandle: font.TextureId);
DrawRing(frame, encoder, textBuf);
}
}
- private void SetUseTexture(int mode)
+ ///
+ /// Writes the shared push-constant block for one draw bucket: the screen
+ /// size the vertex stage maps pixels to NDC with, and the two texture-table
+ /// slots whose assignment selects the fragment stage's sampling mode.
+ /// At most one of the two handles is ever a real texture.
+ ///
+ private void SetTextures(IGpuPassEncoder encoder, uint colorHandle, uint coverageHandle)
{
- if (_uUseTextureLocation >= 0)
- _gl.Uniform1(_uUseTextureLocation, mode);
+ GpuPushConstants constants = GpuPushConstants.Default;
+ constants.ParamA = _screenSize.X;
+ constants.ParamB = _screenSize.Y;
+ constants.TextureIndexA = UiTextureTableHandle.ToSlot(colorHandle).Index;
+ constants.TextureIndexB = UiTextureTableHandle.ToSlot(coverageHandle).Index;
+ encoder.SetPushConstants(constants);
}
///
@@ -513,9 +465,5 @@ public sealed unsafe class TextRenderer : IDisposable
encoder.Draw((uint)(buf.Count / FloatsPerVertex), 1, 0, 0);
}
- public void Dispose()
- {
- _whiteTexture.Dispose();
- _pipeline.Dispose();
- }
+ public void Dispose() => _pipeline.Dispose();
}
diff --git a/src/AcDream.App/Rendering/TextureCache.cs b/src/AcDream.App/Rendering/TextureCache.cs
index 48da8081..e6c86be0 100644
--- a/src/AcDream.App/Rendering/TextureCache.cs
+++ b/src/AcDream.App/Rendering/TextureCache.cs
@@ -250,13 +250,20 @@ public sealed unsafe class TextureCache
/// DefaultPaletteId (same starting palette
/// uses); non-paletted formats have DefaultPaletteId==0 → palette null. Returns
/// a 1x1 magenta handle on miss.
+ ///
+ /// Campaign V slice V6d: the returned value is a
+ /// — a one-based index into the device's
+ /// global texture table — not a raw GL texture name. Every caller passes it
+ /// straight to , which samples the
+ /// table; nothing reads it as a GL name, and on Vulkan there is no GL name.
+ /// Zero still means "no texture", which is what every widget guards on.
///
public uint GetOrUploadRenderSurface(uint renderSurfaceId, out int width, out int height, bool nearest = false)
{
if (_renderSurfaceGpuTextures.TryGetValue(renderSurfaceId, out GpuUiTextureEntry existing))
{
width = existing.Width; height = existing.Height;
- return existing.GlName;
+ return UiTextureTableHandle.FromSlot(existing.Slot);
}
DecodedTexture decoded;
@@ -280,7 +287,7 @@ public sealed unsafe class TextureCache
GpuUiTextureEntry entry = UploadUiTexture(decoded, nearest, $"ui-rendersurface-0x{renderSurfaceId:X8}");
_renderSurfaceGpuTextures[renderSurfaceId] = entry;
width = decoded.Width; height = decoded.Height;
- return entry.GlName;
+ return UiTextureTableHandle.FromSlot(entry.Slot);
}
///
@@ -288,13 +295,16 @@ public sealed unsafe class TextureCache
/// decoded UI sprite/atlas and registers it into the device's global
/// texture table. Every UI-path texture uses REPEAT addressing (existing
/// behaviour — panel fills and tiled chrome sample UVs greater than 1) and
- /// a single mip level (UI sprites never mip). The classic texture-unit
- /// binding path uses for these
- /// (see that class's remarks) samples the texture object directly rather
- /// than through a bound sampler object, so filtering must live on the
- /// texture itself — GlGpuTexture's constructor always sets Linear,
- /// which is wrong for -requested (pixel-exact)
- /// sprites, so it is overridden here exactly as the old raw-GL path did.
+ /// a single mip level (UI sprites never mip).
+ ///
+ /// Campaign V slice V6d: the sampler is now what filtering actually
+ /// comes from. Before this slice the draw bound the texture object directly,
+ /// so filtering lived on the texture and was
+ /// applied with a raw glTexParameter before the bindless handle was
+ /// made resident. Sampling through the table means a bound sampler object
+ /// overrides those parameters, so a nearest-requested sprite has to be
+ /// registered with a nearest SAMPLER or every retail icon and dat-font
+ /// glyph would silently become bilinear.
///
private GpuUiTextureEntry UploadUiTexture(DecodedTexture decoded, bool nearest, string debugName)
{
@@ -312,19 +322,7 @@ public sealed unsafe class TextureCache
uint glName = ((GlGpuTexture)texture).GlName;
TrackUploadedTexture(glName, decoded.Width, decoded.Height);
- // MUST happen BEFORE RegisterTexture below: ARB_bindless_texture
- // forbids glTexParameter on a texture once its bindless handle has
- // been made resident (GL_INVALID_OPERATION). See BitmapFont's
- // constructor for the same fix and full explanation.
- if (nearest)
- {
- _gl.BindTexture(TextureTarget.Texture2D, glName);
- _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Nearest);
- _gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Nearest);
- _gl.BindTexture(TextureTarget.Texture2D, 0);
- }
-
- IGpuSampler sampler = _device.CreateSampler(GpuSamplerDescription.WorldRepeat);
+ IGpuSampler sampler = _device.CreateSampler(nearest ? UiNearestRepeat : GpuSamplerDescription.WorldRepeat);
GpuTextureSlot slot = _device.RegisterTexture(texture, sampler);
return new GpuUiTextureEntry(texture, slot, glName, decoded.Width, decoded.Height);
}
@@ -335,6 +333,19 @@ public sealed unsafe class TextureCache
}
}
+ ///
+ /// Point sampling with REPEAT addressing — pixel-exact retail UI art that is
+ /// still tiled by nine-slice chrome and meter tracks. Neither stock preset
+ /// fits: UiNearest clamps, WorldRepeat filters.
+ ///
+ private static readonly GpuSamplerDescription UiNearestRepeat = new(
+ GpuFilter.Nearest,
+ GpuFilter.Nearest,
+ GpuMipFilter.None,
+ GpuAddressMode.Repeat,
+ GpuAddressMode.Repeat,
+ MaxAnisotropy: 1f);
+
///
/// Alpha-channel histogram for one decoded texture. Used to diagnose
/// "why are clouds not transparent" — if cloud textures come out with
@@ -887,15 +898,20 @@ public sealed unsafe class TextureCache
/// Uploads a raw RGBA8 byte array as a Texture2D. Used by
/// to upload CPU-composited icon layers.
- /// The returned handle is tracked in and deleted by
- /// . Callers must NOT also store the handle in any of the
- /// keyed caches — that would cause a double-delete on Dispose.
+ /// The texture is tracked in and deleted by
+ /// . Callers must NOT also store the returned handle in any
+ /// of the keyed caches — that would cause a double-delete on Dispose.
+ ///
+ /// Campaign V slice V6d: returns a
+ /// rather than a GL texture name, for the reason given on
+ /// .
+ ///
public uint UploadRgba8(byte[] rgba, int width, int height, bool nearest = false)
{
GpuUiTextureEntry entry = UploadUiTexture(
new DecodedTexture(rgba, width, height), nearest, "ui-adhoc-rgba8");
_adhocGpuTextures.Add(entry);
- return entry.GlName;
+ return UiTextureTableHandle.FromSlot(entry.Slot);
}
private uint UploadRgba8(DecodedTexture decoded, bool nearest = false)
diff --git a/src/AcDream.App/Rendering/UiTextureTableHandle.cs b/src/AcDream.App/Rendering/UiTextureTableHandle.cs
new file mode 100644
index 00000000..edc71abf
--- /dev/null
+++ b/src/AcDream.App/Rendering/UiTextureTableHandle.cs
@@ -0,0 +1,50 @@
+using AcDream.App.Rendering.Gpu;
+
+namespace AcDream.App.Rendering;
+
+///
+/// Campaign V slice V6d: the encoding the retained UI passes textures around
+/// with — a ONE-BASED index into the device's global texture table, where 0
+/// means "no texture".
+///
+/// Why an encoding rather than the slot itself. Until this slice
+/// the UI's currency was a raw GL texture name: TextureCache handed one
+/// out, sixty-odd widget call sites carried it, and TextRenderer.DrawSprite
+/// bound it to texture unit 0. That name means nothing on Vulkan, so the
+/// currency has to become a . But
+/// is internal to the pinned RHI contract while
+/// UiRenderContext.DrawSprite, TextureCache.GetOrUploadRenderSurface
+/// and a dozen widget properties are public, so the slot cannot itself travel
+/// through those signatures without either publishing a contract type or
+/// converting the whole retained-UI surface to internal. Both were out of scope
+/// for this slice, and the second is explicitly forbidden by the campaign's
+/// rule against visibility sweeps.
+///
+/// Why one-based. The old currency already had the property that
+/// zero means nothing — GL texture name 0 is "no texture" — and every widget in
+/// the tree guards on it (if (tex == 0) return;). Slot 0 is a perfectly
+/// valid table index, so handing out raw slot indices would turn every one of
+/// those guards into a silent false negative. Shifting by one preserves the
+/// guard exactly, needs no call-site change, and keeps the sentinel loud rather
+/// than aliasing onto a real texture.
+///
+/// Retired when the retained UI's public surface can name a
+/// directly.
+///
+internal static class UiTextureTableHandle
+{
+ /// No texture. What every widget's tex == 0 guard tests for.
+ public const uint None = 0;
+
+ /// Encodes a registered slot. An unassigned slot encodes to .
+ public static uint FromSlot(GpuTextureSlot slot) =>
+ slot.IsAssigned ? slot.Index + 1 : None;
+
+ ///
+ /// Decodes a handle. decodes to
+ /// , which the retained UI's shader
+ /// reads as "draw the vertex colour" rather than sampling anything.
+ ///
+ public static GpuTextureSlot ToSlot(uint handle) =>
+ handle == None ? GpuTextureSlot.Unassigned : new GpuTextureSlot(handle - 1);
+}
diff --git a/src/AcDream.App/UI/UiViewport.cs b/src/AcDream.App/UI/UiViewport.cs
index 3f479906..90ec21ab 100644
--- a/src/AcDream.App/UI/UiViewport.cs
+++ b/src/AcDream.App/UI/UiViewport.cs
@@ -50,7 +50,7 @@ public sealed class UiViewport : UiElement
protected override void OnDraw(UiRenderContext ctx)
{
if (!Visible || !TextureSlot.IsAssigned) return;
- uint textureHandle = ctx.TextRenderer.ResolveExternalTextureSlot(TextureSlot);
+ uint textureHandle = AcDream.App.Rendering.TextRenderer.ResolveExternalTextureSlot(TextureSlot);
if (textureHandle == 0) return;
// Local origin is already at this widget's Left/Top (PushTransform applied by DrawSelfAndChildren).
// V is FLIPPED (v0=1, v1=0): the resolved texture is an off-screen FBO color texture, whose origin is
diff --git a/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs b/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs
index 4a7f532b..51f506af 100644
--- a/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/GlTextureOwnershipTests.cs
@@ -108,10 +108,10 @@ public sealed class GlTextureOwnershipTests
// from raw GlResourceCommand calls to IGpuDevice.CreatePipeline/
// CreateTexture, whose own checked-commit construction
// (GlResourceCommand.CreateName / ShaderProgramConstruction.Build,
- // predating this slice) is what those two now delegate to. What
- // TextRenderer's OWN constructor still owns is the ordered
- // pipeline-then-texture sequence and disposing whichever of the two
- // it already created if the other throws.
+ // predating this slice) is what those now delegate to. Slice V6d then
+ // removed the white fill texture, so TextRenderer's constructor owns a
+ // single resource and holds no GL name of any kind — every checked
+ // commit boundary it depends on lives behind the RHI.
string text = File.ReadAllText(Path.Combine(
root, "src", "AcDream.App", "Rendering", "TextRenderer.cs"));
string bindless = File.ReadAllText(Path.Combine(
@@ -124,14 +124,9 @@ public sealed class GlTextureOwnershipTests
Assert.Contains("GlResourceCommand.DeleteProgram", shaderPrograms, StringComparison.Ordinal);
Assert.Contains("_anisotropyBindingMutation.Execute", terrain, StringComparison.Ordinal);
Assert.Contains("GlResourceCommand.DeleteTexture", terrain, StringComparison.Ordinal);
- AssertAppearsInOrder(
- text,
- "pipeline = device.CreatePipeline(",
- "whiteTexture = device.CreateTexture(",
- "catch",
- "whiteTexture?.Dispose();",
- "pipeline?.Dispose();",
- "throw;");
+ Assert.Contains("_pipeline = device.CreatePipeline(", text, StringComparison.Ordinal);
+ Assert.DoesNotContain("GlResourceCommand", text, StringComparison.Ordinal);
+ Assert.DoesNotContain("GlName", text, StringComparison.Ordinal);
Assert.Contains("make bindless handle", bindless, StringComparison.Ordinal);
Assert.Contains("GlResourceCommand.Execute", bindless, StringComparison.Ordinal);
}
diff --git a/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs b/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs
index e5fbdd2c..dfe2fa26 100644
--- a/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/ResourceCleanupGroupTests.cs
@@ -131,18 +131,20 @@ public sealed class ResourceCleanupGroupTests
}
///
- /// Campaign V slice V4a: TextRenderer's constructor no longer owns raw
- /// VAO/VBO/texture GL names through a ResourceCleanupGroup ledger — it
- /// creates exactly two device-owned resources (a pipeline, then the white
- /// fill texture) and, since a later failure could otherwise orphan an
- /// already-created pipeline, disposes whichever of the two it already
- /// created if the other throws. This replaces the pre-V4a assertion of
- /// the same name, which pinned the old multi-resource raw-GL shape
- /// (Shader + three flight-indexed VAO/VBO pairs + a hand-rolled white
- /// texture) that no longer exists.
+ /// Campaign V slice V4a moved TextRenderer's constructor off raw
+ /// VAO/VBO/texture GL names and onto two device-owned resources — a
+ /// pipeline and a 1x1 white fill texture — with a catch that disposed
+ /// whichever already existed when the other threw.
+ ///
+ /// Slice V6d removed the white texture: the shader gained an untextured
+ /// branch that produces what white-times-colour produced, so the fill needs
+ /// no texture at all. That leaves exactly ONE owned resource, which is a
+ /// stronger property than correct rollback — with nothing to orphan there is
+ /// no partial-construction window to get wrong. This test pins that, so
+ /// re-growing a second resource without re-growing the rollback fails here.
///
[Fact]
- public void TextRendererDisposesWhicheverConstructorResourceAlreadyExistsOnFailure()
+ public void TextRendererConstructorOwnsExactlyOneDeviceResource()
{
string source = File.ReadAllText(Path.Combine(
FindRepoRoot(),
@@ -151,20 +153,27 @@ public sealed class ResourceCleanupGroupTests
"Rendering",
"TextRenderer.cs"));
- AssertAppearsInOrder(
- source,
- "IGpuPipeline? pipeline = null;",
- "IGpuTexture? whiteTexture = null;",
- "try",
- "pipeline = device.CreatePipeline(",
- "whiteTexture = device.CreateTexture(",
- "device.RegisterTexture(whiteTexture, whiteSampler);",
- "catch",
- "whiteTexture?.Dispose();",
- "pipeline?.Dispose();",
- "throw;",
- "_pipeline = pipeline;",
- "_whiteTexture = whiteTexture;");
+ Assert.Equal(1, CountOccurrences(source, "device.CreatePipeline("));
+ Assert.Equal(0, CountOccurrences(source, "device.CreateTexture("));
+ Assert.Equal(0, CountOccurrences(source, "device.CreateBuffer("));
+ Assert.Equal(0, CountOccurrences(source, "device.CreateSampler("));
+ Assert.Equal(0, CountOccurrences(source, "device.RegisterTexture("));
+
+ // And the one resource is released.
+ Assert.Contains("public void Dispose() => _pipeline.Dispose();", source, StringComparison.Ordinal);
+ }
+
+ private static int CountOccurrences(string source, string needle)
+ {
+ int count = 0;
+ for (int i = source.IndexOf(needle, StringComparison.Ordinal);
+ i >= 0;
+ i = source.IndexOf(needle, i + needle.Length, StringComparison.Ordinal))
+ {
+ count++;
+ }
+
+ return count;
}
[Fact]
diff --git a/tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs b/tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs
index 77154717..c4fc61cf 100644
--- a/tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs
+++ b/tests/AcDream.App.Tests/Rendering/TextRendererFailureSafetyTests.cs
@@ -1,13 +1,22 @@
using System.Reflection;
using AcDream.App.Rendering;
+using AcDream.App.Rendering.Gpu.Gl;
using Silk.NET.OpenGL;
namespace AcDream.App.Tests.Rendering;
+///
+/// The retained UI's whole draw is one RHI pass, and a failure anywhere inside
+/// it must not leave GL state behind for the raw-GL world renderers that run in
+/// the next frame. Until Campaign V slice V6d, TextRenderer owned a private
+/// state scope for that; V6d made the renderer backend-neutral and moved the
+/// guarantee onto , which every RHI pass
+/// gets. These tests follow it there.
+///
public sealed class TextRendererFailureSafetyTests
{
[Fact]
- public void Flush_CompilesCompleteGlStateScopeAsFinallyAroundBothDrawLayers()
+ public void Flush_CompilesThePassEncoderAsFinallyAroundBothDrawLayers()
{
MethodInfo flush = typeof(TextRenderer).GetMethod(nameof(TextRenderer.Flush))!;
MethodBody body = flush.GetMethodBody()!;
@@ -18,27 +27,36 @@ public sealed class TextRendererFailureSafetyTests
"Rendering",
"TextRenderer.cs"));
+ // The encoder's `using` is the finally: disposing it closes the pass,
+ // which is what restores the ambient capability state the pass changed.
Assert.Contains(
body.ExceptionHandlingClauses,
clause => clause.Flags == ExceptionHandlingClauseOptions.Finally);
AssertAppearsInOrder(
source,
- "using var stateScope = new TextRenderGlStateScope(_glState);",
- "_gl.Disable(EnableCap.Multisample);",
+ "using IGpuPassEncoder encoder = frame.BeginPass(new GpuPassDescription",
+ "encoder.BindPipeline(_pipeline);",
"DrawLayer(_spriteSegs,",
"DrawLayer(_overlaySpriteSegs,");
+
+ // And no raw GL of its own is left: the renderer draws on both backends.
+ Assert.DoesNotContain("Silk.NET.OpenGL", source, StringComparison.Ordinal);
+ Assert.DoesNotContain("_gl.", source, StringComparison.Ordinal);
}
[Fact]
- public void FailedDraw_RestoresEveryGlValueMutatedByTheTextPass()
+ public void FailedDraw_RestoresEveryGlValueMutatedByThePass()
{
var gl = new RecordingGlState
{
DepthWrite = false,
+ DepthFuncValue = DepthFunction.Greater,
BlendSourceRgb = BlendingFactor.DstAlpha,
BlendDestinationRgb = BlendingFactor.OneMinusDstAlpha,
BlendSourceAlpha = BlendingFactor.One,
BlendDestinationAlpha = BlendingFactor.Zero,
+ CullFaceMode = TriangleFace.Front,
+ FrontFaceDirection = FrontFaceDirection.CW,
Program = 17,
VertexArray = 23,
ArrayBuffer = 31,
@@ -48,31 +66,44 @@ public sealed class TextRendererFailureSafetyTests
gl.SetCapability(EnableCap.Blend, enabled: false);
gl.SetCapability(EnableCap.CullFace, enabled: true);
gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: true);
- gl.SetCapability(EnableCap.Multisample, enabled: false);
+ // Enabled on entry — the world's MSAA. The UI pass turns it off and the
+ // restore has to put it back, which is the exact dimension the V4a
+ // revert lost and which nothing covered before this test.
+ gl.SetCapability(EnableCap.Multisample, enabled: true);
gl.TextureBindings[TextureUnit.Texture0] = 41;
gl.TextureBindings[TextureUnit.Texture2] = 43;
StateSnapshot expected = gl.Capture();
Action failedDraw = () =>
{
- using var stateScope = new TextRenderGlStateScope(gl);
- gl.SetCapability(EnableCap.DepthTest, enabled: false);
- gl.SetCapability(EnableCap.Blend, enabled: true);
- gl.SetCapability(EnableCap.CullFace, enabled: false);
- gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: false);
- gl.SetCapability(EnableCap.Multisample, enabled: true);
- gl.DepthMask(true);
- gl.BlendFuncSeparate(
- BlendingFactor.SrcAlpha,
- BlendingFactor.OneMinusSrcAlpha,
- BlendingFactor.SrcAlpha,
- BlendingFactor.OneMinusSrcAlpha);
- gl.UseProgram(101);
- gl.BindVertexArray(103);
- gl.BindBuffer(BufferTargetARB.ArrayBuffer, 107);
- gl.ActiveTexture(TextureUnit.Texture0);
- gl.BindTexture(TextureTarget.Texture2D, 109);
- throw new InvalidOperationException("draw upload");
+ GlAmbientCapabilityState ambient = GlAmbientCapabilityState.Capture(gl);
+ try
+ {
+ gl.SetCapability(EnableCap.DepthTest, enabled: false);
+ gl.SetCapability(EnableCap.Blend, enabled: true);
+ gl.SetCapability(EnableCap.CullFace, enabled: false);
+ gl.SetCapability(EnableCap.SampleAlphaToCoverage, enabled: false);
+ gl.SetCapability(EnableCap.Multisample, enabled: false);
+ gl.DepthMask(true);
+ gl.DepthFunc(DepthFunction.Lequal);
+ gl.BlendFuncSeparate(
+ BlendingFactor.SrcAlpha,
+ BlendingFactor.OneMinusSrcAlpha,
+ BlendingFactor.SrcAlpha,
+ BlendingFactor.OneMinusSrcAlpha);
+ gl.CullFace(TriangleFace.Back);
+ gl.FrontFace(FrontFaceDirection.Ccw);
+ gl.UseProgram(101);
+ gl.BindVertexArray(103);
+ gl.BindBuffer(BufferTargetARB.ArrayBuffer, 107);
+ gl.ActiveTexture(TextureUnit.Texture0);
+ gl.BindTexture(TextureTarget.Texture2D, 109);
+ throw new InvalidOperationException("draw upload");
+ }
+ finally
+ {
+ ambient.Restore(gl);
+ }
};
Assert.Throws(failedDraw);
@@ -87,10 +118,13 @@ public sealed class TextRendererFailureSafetyTests
bool AlphaToCoverage,
bool Multisample,
bool DepthWrite,
+ DepthFunction DepthFunc,
BlendingFactor BlendSourceRgb,
BlendingFactor BlendDestinationRgb,
BlendingFactor BlendSourceAlpha,
BlendingFactor BlendDestinationAlpha,
+ TriangleFace CullFaceMode,
+ FrontFaceDirection FrontFaceDirection,
uint Program,
uint VertexArray,
uint ArrayBuffer,
@@ -98,15 +132,18 @@ public sealed class TextRendererFailureSafetyTests
uint Texture0,
uint Texture2);
- private sealed class RecordingGlState : ITextRenderGlStateApi
+ private sealed class RecordingGlState : IGlAmbientStateApi
{
private readonly Dictionary _capabilities = [];
public bool DepthWrite { get; set; }
+ public DepthFunction DepthFuncValue { get; set; }
public BlendingFactor BlendSourceRgb { get; set; }
public BlendingFactor BlendDestinationRgb { get; set; }
public BlendingFactor BlendSourceAlpha { get; set; }
public BlendingFactor BlendDestinationAlpha { get; set; }
+ public TriangleFace CullFaceMode { get; set; }
+ public FrontFaceDirection FrontFaceDirection { get; set; }
public uint Program { get; set; }
public uint VertexArray { get; set; }
public uint ArrayBuffer { get; set; }
@@ -120,10 +157,13 @@ public sealed class TextRendererFailureSafetyTests
IsEnabled(EnableCap.SampleAlphaToCoverage),
IsEnabled(EnableCap.Multisample),
DepthWrite,
+ DepthFuncValue,
BlendSourceRgb,
BlendDestinationRgb,
BlendSourceAlpha,
BlendDestinationAlpha,
+ CullFaceMode,
+ FrontFaceDirection,
Program,
VertexArray,
ArrayBuffer,
@@ -140,6 +180,9 @@ public sealed class TextRendererFailureSafetyTests
GetPName.BlendDstRgb => (int)BlendDestinationRgb,
GetPName.BlendSrcAlpha => (int)BlendSourceAlpha,
GetPName.BlendDstAlpha => (int)BlendDestinationAlpha,
+ GetPName.DepthFunc => (int)DepthFuncValue,
+ GetPName.CullFaceMode => (int)CullFaceMode,
+ GetPName.FrontFace => (int)FrontFaceDirection,
GetPName.CurrentProgram => (int)Program,
GetPName.VertexArrayBinding => (int)VertexArray,
GetPName.ArrayBufferBinding => (int)ArrayBuffer,
@@ -159,6 +202,8 @@ public sealed class TextRendererFailureSafetyTests
public void DepthMask(bool enabled) => DepthWrite = enabled;
+ public void DepthFunc(DepthFunction function) => DepthFuncValue = function;
+
public void BlendFuncSeparate(
BlendingFactor sourceRgb,
BlendingFactor destinationRgb,
@@ -171,6 +216,10 @@ public sealed class TextRendererFailureSafetyTests
BlendDestinationAlpha = destinationAlpha;
}
+ public void CullFace(TriangleFace face) => CullFaceMode = face;
+
+ public void FrontFace(FrontFaceDirection direction) => FrontFaceDirection = direction;
+
public void UseProgram(uint program) => Program = program;
public void BindVertexArray(uint vertexArray) => VertexArray = vertexArray;
diff --git a/tools/ShaderCompiler/VulkanGlslPreamble.cs b/tools/ShaderCompiler/VulkanGlslPreamble.cs
index c611e40b..08f9fc9b 100644
--- a/tools/ShaderCompiler/VulkanGlslPreamble.cs
+++ b/tools/ShaderCompiler/VulkanGlslPreamble.cs
@@ -94,6 +94,13 @@ internal static class VulkanGlslPreamble
text.AppendLine("#undef ACDREAM_TEXTURE_HANDLE");
text.AppendLine("#define ACDREAM_TEXTURE_HANDLE(idx) (idx)");
text.AppendLine("#define ACDREAM_TEXTURE(idx) uTextures[nonuniformEXT(uint(idx))]");
+ // Slice V6d: the 2-D read. GL reconstructs a sampler2D from the entry's
+ // bindless handle; Vulkan has one descriptor array whose element type is
+ // fixed at sampler2DArray, so a 2-D entry is a one-layer array read at
+ // layer 0. See common.glsl for the GL half and for why the UI's
+ // textures stay plain GL_TEXTURE_2D objects.
+ text.AppendLine(
+ "#define ACDREAM_SAMPLE_2D(idx, uv) texture(ACDREAM_TEXTURE(idx), vec3((uv), 0.0))");
text.AppendLine();
text.AppendLine("// §3.4 push constants: one shared 96-byte block, so switching pipelines");
text.AppendLine("// mid-pass invalidates neither descriptors nor constants.");