feat(render): V6e — move the world mesh's texture lookup to where Vulkan can express it
Campaign V slice V6e, first of three. mesh_modern is the shader every world
static, every piece of scenery and every EnvCell surface draws through, and it
was one of the four production pairs the SPIR-V toolchain still refused.
The blocker was a varying. Since V2 the vertex stage looked a batch's table slot
up in the binding=9 handle table and forwarded the resulting 64-bit
GL_ARB_bindless_texture handle to the fragment stage as a `flat uvec2`. That
works on GL because a bindless handle is just a number a shader may carry
anywhere. It cannot work on Vulkan at all: the equivalent object is a descriptor
in set 2, and a descriptor is not a value a stage can hand to another stage. So
what travels between the stages is now the SLOT — a `flat uint` — and the
fragment stage does the lookup at the point of sampling.
That relocation needs one shared idea, because the two backends disagree about
what the lookup IS. `ACDREAM_SAMPLE_ARRAY(slot, uvw)` asks the dialect-neutral
question — "sample table slot N" — and expands to
`texture(sampler2DArray(gTextureTable[slot]), uvw)` under GL and to
`texture(uTextures[nonuniformEXT(slot)], uvw)` under Vulkan. It is deliberately
a SAMPLING macro rather than a sampler-returning one: `nonuniformEXT` belongs on
the indexing expression itself, and binding the result to a local
`sampler2DArray` first is exactly where an implementation is free to drop it.
That is the same shape V6d already used for the retained UI's 2-D reads, and it
now covers the array reads the world path needs.
`ACDREAM_TEXTURE_NONE` lands alongside it, unused here and used by the next
commit. GL can ask "does this slot hold a texture" of the payload, because an
unregistered slot holds the null handle; Vulkan cannot, because set 2 is opaque
and reading an unwritten element of a partially-bound array is undefined rather
than zero. The sentinel moves that answer into the index, where both dialects
test it identically.
On GL nothing about the sampled result changes — the same slot resolves to the
same handle to the same texel. The SSBO read simply happens one stage later,
and `flat` keeps it one scalar load per primitive rather than per fragment.
Also: RenderBootstrap has been loading mesh_modern without common.glsl since V2,
which cannot have linked — `ACDREAM_UBO_SET` sits inside a layout qualifier
there. The UI Studio path is the only caller. One argument, same pair, same way
WorldRenderComposition has always loaded it.
Gates: Release build clean; App tests 4,057 passed / 3 skipped (baseline);
offline pixel gate against 95f8c25f differing fraction 3.37e-05 (~19 px of
563,200), inside the documented 15–23 px same-commit noise band and ~30x under
the 0.001 threshold. mesh_modern is the shader that gate covers most heavily,
so this is the strongest automated evidence any V6e commit gets.
Manifest: 4/9 pairs compile (debug_line, mesh_modern, ui_text, vk_probe).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
95f8c25f31
commit
935f4dc3d9
8 changed files with 80 additions and 28 deletions
|
|
@ -174,9 +174,16 @@ public static class RenderBootstrap
|
||||||
var lightingUbo = new SceneLightingUboBinding(gl);
|
var lightingUbo = new SceneLightingUboBinding(gl);
|
||||||
|
|
||||||
// --- Mesh shader (GameWindow ~1769-1771) ---
|
// --- Mesh shader (GameWindow ~1769-1771) ---
|
||||||
|
// Campaign V slice V6e: mesh_modern has needed common.glsl since V2 —
|
||||||
|
// ACDREAM_UBO_SET appears in a layout qualifier and ACDREAM_SAMPLE_ARRAY
|
||||||
|
// at the sample site, and without the preamble both are undeclared
|
||||||
|
// identifiers, so this program has failed to link on the Studio path
|
||||||
|
// since that slice. The world composition (WorldRenderComposition) has
|
||||||
|
// always passed true; this is the same pair loaded the same way.
|
||||||
var meshShader = new Shader(gl,
|
var meshShader = new Shader(gl,
|
||||||
Path.Combine(shaderDir, "mesh_modern.vert"),
|
Path.Combine(shaderDir, "mesh_modern.vert"),
|
||||||
Path.Combine(shaderDir, "mesh_modern.frag"));
|
Path.Combine(shaderDir, "mesh_modern.frag"),
|
||||||
|
includeCommonPreamble: true);
|
||||||
|
|
||||||
// --- TextureCache (GameWindow ~1774) ---
|
// --- TextureCache (GameWindow ~1774) ---
|
||||||
var frameFlights = new GpuFrameFlightController(gl);
|
var frameFlights = new GpuFrameFlightController(gl);
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,36 @@ layout(std430, binding = 9) readonly buffer TextureTableBuf {
|
||||||
// explicit rather than folded into one sampler-returning macro) because every
|
// explicit rather than folded into one sampler-returning macro) because every
|
||||||
// existing call site already follows that exact pattern and a function cannot
|
// existing call site already follows that exact pattern and a function cannot
|
||||||
// return an opaque sampler type built from a runtime value in GLSL.
|
// return an opaque sampler type built from a runtime value in GLSL.
|
||||||
|
//
|
||||||
|
// Campaign V slice V6e: this macro is GL-shaped — under Vulkan it degenerates
|
||||||
|
// to the index itself, because there is no handle to look up. Every NEW call
|
||||||
|
// site should use ACDREAM_SAMPLE_ARRAY / ACDREAM_SAMPLE_2D below, which ask the
|
||||||
|
// dialect-neutral question ("sample table slot N") instead of the GL-only one
|
||||||
|
// ("what handle does slot N hold"). The remaining direct users are the shaders
|
||||||
|
// whose port slice has not run yet.
|
||||||
#define ACDREAM_TEXTURE_HANDLE(idx) gTextureTable[idx]
|
#define ACDREAM_TEXTURE_HANDLE(idx) gTextureTable[idx]
|
||||||
|
|
||||||
|
// Campaign V slice V6e: samples a table slot that holds a 2-D ARRAY texture —
|
||||||
|
// the world/particle/terrain case, as opposed to the retained UI's plain 2-D
|
||||||
|
// entries that ACDREAM_SAMPLE_2D below covers.
|
||||||
|
//
|
||||||
|
// Expressed as a SAMPLING macro rather than a sampler-returning one on purpose.
|
||||||
|
// Under Vulkan the expansion carries a `nonuniformEXT` qualifier, and that
|
||||||
|
// qualifier belongs on the indexing expression at the point of use; binding the
|
||||||
|
// result to a local `sampler2DArray` variable first is where a driver is free to
|
||||||
|
// lose it. Both dialects therefore read the texture in one expression.
|
||||||
|
#define ACDREAM_SAMPLE_ARRAY(idx, uvw) texture(sampler2DArray(gTextureTable[idx]), uvw)
|
||||||
|
|
||||||
|
// Campaign V slice V6e: the reserved "this draw has no texture" slot index.
|
||||||
|
//
|
||||||
|
// GL could ask the question directly — an unregistered slot holds the handle 0,
|
||||||
|
// so `gTextureTable[idx] == uvec2(0)` answered it. Vulkan cannot: set 2 is an
|
||||||
|
// opaque descriptor array with nothing to compare, and reading an unwritten
|
||||||
|
// element of a partially-bound array is undefined rather than zero. So the
|
||||||
|
// answer moves to the index itself, which both dialects can test identically,
|
||||||
|
// and the CPU writes this value instead of registering a null handle.
|
||||||
|
#define ACDREAM_TEXTURE_NONE 0xFFFFFFFFu
|
||||||
|
|
||||||
// Campaign V slice V6d: samples a table slot that holds a plain 2-D texture.
|
// 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 two backends disagree about what a 2-D table entry IS, and this macro is
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,10 @@ in vec3 vNormal;
|
||||||
in vec2 vTexCoord;
|
in vec2 vTexCoord;
|
||||||
in vec3 vWorldPos;
|
in vec3 vWorldPos;
|
||||||
in vec3 vLit; // A7: per-vertex Gouraud lighting (ambient + capped lights), from mesh_modern.vert
|
in vec3 vLit; // A7: per-vertex Gouraud lighting (ambient + capped lights), from mesh_modern.vert
|
||||||
in flat uvec2 vTextureHandle;
|
// Campaign V slice V6e: the table slot, not the bindless handle — see
|
||||||
|
// mesh_modern.vert. The lookup moved here because a Vulkan varying cannot
|
||||||
|
// carry a descriptor.
|
||||||
|
in flat uint vTextureIndex;
|
||||||
in flat uint vTextureLayer;
|
in flat uint vTextureLayer;
|
||||||
in flat float vOpacityMultiplier; // #188
|
in flat float vOpacityMultiplier; // #188
|
||||||
in flat vec2 vSelectionLighting; // x=luminosity, y=diffuse
|
in flat vec2 vSelectionLighting; // x=luminosity, y=diffuse
|
||||||
|
|
@ -55,8 +58,7 @@ vec3 applyFog(vec3 lit, vec3 worldPos) {
|
||||||
out vec4 FragColor;
|
out vec4 FragColor;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
sampler2DArray tex = sampler2DArray(vTextureHandle);
|
vec4 color = ACDREAM_SAMPLE_ARRAY(vTextureIndex, vec3(vTexCoord, float(vTextureLayer)));
|
||||||
vec4 color = texture(tex, vec3(vTexCoord, float(vTextureLayer)));
|
|
||||||
|
|
||||||
// Two-pass alpha-test (N.5 Decision 2).
|
// Two-pass alpha-test (N.5 Decision 2).
|
||||||
// A.5 T20: opaque pass writes alpha as-sampled so GL_SAMPLE_ALPHA_TO_COVERAGE
|
// A.5 T20: opaque pass writes alpha as-sampled so GL_SAMPLE_ALPHA_TO_COVERAGE
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,8 @@ struct InstanceData {
|
||||||
// Campaign V slice V2 (2026-07-27): textureHandle (uvec2, a 64-bit
|
// Campaign V slice V2 (2026-07-27): textureHandle (uvec2, a 64-bit
|
||||||
// GL_ARB_bindless_texture handle) became textureIndex (uint) plus an explicit
|
// GL_ARB_bindless_texture handle) became textureIndex (uint) plus an explicit
|
||||||
// pad word. textureIndex is a slot into the binding=9 handle table
|
// pad word. textureIndex is a slot into the binding=9 handle table
|
||||||
// (ACDREAM_TEXTURE_HANDLE, common.glsl) that main() below looks up once per
|
// (common.glsl) which main() below forwards to the fragment stage, where slice
|
||||||
// vertex to reconstruct the exact same uvec2 handle main() used to receive
|
// V6e moved the lookup so the same source compiles for Vulkan. The pad word keeps
|
||||||
// directly — one indirection, identical value. The pad word keeps
|
|
||||||
// textureLayer/flags at their original std430 offsets (8/12), so the struct
|
// textureLayer/flags at their original std430 offsets (8/12), so the struct
|
||||||
// is still 16 bytes and every existing CPU writer's layout is unchanged
|
// is still 16 bytes and every existing CPU writer's layout is unchanged
|
||||||
// (GpuBindingModel.GpuBatchDataStrideBytes).
|
// (GpuBindingModel.GpuBatchDataStrideBytes).
|
||||||
|
|
@ -289,7 +288,14 @@ out vec3 vNormal;
|
||||||
out vec2 vTexCoord;
|
out vec2 vTexCoord;
|
||||||
out vec3 vWorldPos;
|
out vec3 vWorldPos;
|
||||||
out vec3 vLit; // A7: per-vertex Gouraud lighting (ambient + capped lights)
|
out vec3 vLit; // A7: per-vertex Gouraud lighting (ambient + capped lights)
|
||||||
out flat uvec2 vTextureHandle;
|
// Campaign V slice V6e: was `flat uvec2 vTextureHandle` — a raw 64-bit
|
||||||
|
// GL_ARB_bindless_texture handle handed across the stage boundary. A varying
|
||||||
|
// cannot carry a Vulkan descriptor, so what travels is the table SLOT and the
|
||||||
|
// fragment stage does the lookup (see mesh_modern.frag). Under GL the value
|
||||||
|
// sampled is bit-for-bit the one the vertex stage used to forward; the SSBO
|
||||||
|
// read simply happens one stage later, and `flat` keeps it one scalar load per
|
||||||
|
// primitive rather than per fragment.
|
||||||
|
out flat uint vTextureIndex;
|
||||||
out flat uint vTextureLayer;
|
out flat uint vTextureLayer;
|
||||||
out flat float vOpacityMultiplier; // #188
|
out flat float vOpacityMultiplier; // #188
|
||||||
out flat vec2 vSelectionLighting;
|
out flat vec2 vSelectionLighting;
|
||||||
|
|
@ -323,9 +329,9 @@ void main() {
|
||||||
vTexCoord = aTexCoord;
|
vTexCoord = aTexCoord;
|
||||||
|
|
||||||
BatchData b = Batches[uDrawIDOffset + gl_DrawIDARB];
|
BatchData b = Batches[uDrawIDOffset + gl_DrawIDARB];
|
||||||
// Campaign V slice V2: reconstruct the SAME uvec2 handle the shader used
|
// Campaign V slice V6e: forward the table SLOT untouched. V2 looked the
|
||||||
// to receive directly from BatchData, now via one binding=9 table lookup.
|
// handle up here and passed the handle; the lookup now lives at the sample
|
||||||
// vTextureHandle's type and every downstream frag-shader use are unchanged.
|
// site in mesh_modern.frag, which is the only form Vulkan can express.
|
||||||
vTextureHandle = ACDREAM_TEXTURE_HANDLE(b.textureIndex);
|
vTextureIndex = b.textureIndex;
|
||||||
vTextureLayer = b.textureLayer;
|
vTextureLayer = b.textureLayer;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
BIN
src/AcDream.App/Rendering/Shaders/spv/mesh_modern.frag.spv
Normal file
BIN
src/AcDream.App/Rendering/Shaders/spv/mesh_modern.frag.spv
Normal file
Binary file not shown.
BIN
src/AcDream.App/Rendering/Shaders/spv/mesh_modern.vert.spv
Normal file
BIN
src/AcDream.App/Rendering/Shaders/spv/mesh_modern.vert.spv
Normal file
Binary file not shown.
|
|
@ -25,31 +25,29 @@
|
||||||
"stage": "vert",
|
"stage": "vert",
|
||||||
"sourceSha256": "c35f767ab07fa9df805f9e77f4851f517c153dd2ef2efa6d49d0c24b688e4f56",
|
"sourceSha256": "c35f767ab07fa9df805f9e77f4851f517c153dd2ef2efa6d49d0c24b688e4f56",
|
||||||
"compiled": false,
|
"compiled": false,
|
||||||
"message": "mesh.vert:71: error: \u0027uModel\u0027 : undeclared identifier"
|
"message": "mesh.vert:73: error: \u0027uModel\u0027 : undeclared identifier"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": "frag",
|
"stage": "frag",
|
||||||
"sourceSha256": "4d6478543a9a903a3453581fa847e096aaecf01f38ebb2921572663bad8e24ea",
|
"sourceSha256": "4d6478543a9a903a3453581fa847e096aaecf01f38ebb2921572663bad8e24ea",
|
||||||
"compiled": false,
|
"compiled": false,
|
||||||
"message": "mesh.frag:158: error: \u0027uDiffuse\u0027 : undeclared identifier"
|
"message": "mesh.frag:160: error: \u0027uDiffuse\u0027 : undeclared identifier"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mesh_modern",
|
"name": "mesh_modern",
|
||||||
"vulkanReady": false,
|
"vulkanReady": true,
|
||||||
"stages": [
|
"stages": [
|
||||||
{
|
{
|
||||||
"stage": "vert",
|
"stage": "vert",
|
||||||
"sourceSha256": "1ec2f4af83e73102d87997244a35b69ad5e9ece4b1ad78e2b5ece4d58fab5530",
|
"sourceSha256": "770e6300e023bd2600e8fed52768c624d8fff2ff8d6f997f076980502d3f675b",
|
||||||
"compiled": false,
|
"compiled": true
|
||||||
"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",
|
"stage": "frag",
|
||||||
"sourceSha256": "3aea96cba6c2afc49caae7545f9e42603b6b17afa50b3254beca60f95af5d2f3",
|
"sourceSha256": "12f2ceec9a8420bd273650e95251ca56c3bf3c3691fce1a539e4c5aea7abaabf",
|
||||||
"compiled": false,
|
"compiled": true
|
||||||
"message": "mesh_modern.frag:106: error: \u0027sampler2DArray\u0027 : sampler-constructor requires the extension GL_ARB_bindless_texture enabled"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -61,13 +59,13 @@
|
||||||
"stage": "vert",
|
"stage": "vert",
|
||||||
"sourceSha256": "6a6ebeaacba95e5e4e8a308ed7c4cd805b80f305650c1e9e03e2bdfc6c18f5e7",
|
"sourceSha256": "6a6ebeaacba95e5e4e8a308ed7c4cd805b80f305650c1e9e03e2bdfc6c18f5e7",
|
||||||
"compiled": false,
|
"compiled": false,
|
||||||
"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"
|
"message": "particle.vert:85: 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",
|
"stage": "frag",
|
||||||
"sourceSha256": "3924ecbabf051349bc6a13e6cff370725a0832f8baf515decdb7e0394304006d",
|
"sourceSha256": "3924ecbabf051349bc6a13e6cff370725a0832f8baf515decdb7e0394304006d",
|
||||||
"compiled": false,
|
"compiled": false,
|
||||||
"message": "particle.frag:60: error: \u0027sampler2DArray\u0027 : sampler-constructor requires the extension GL_ARB_bindless_texture enabled"
|
"message": "particle.frag:62: error: \u0027sampler2DArray\u0027 : sampler-constructor requires the extension GL_ARB_bindless_texture enabled"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -84,7 +82,7 @@
|
||||||
"stage": "frag",
|
"stage": "frag",
|
||||||
"sourceSha256": "0da368243e967388990f4f4b90e2304044af6187de45f70499a3e4ece8dfd5a8",
|
"sourceSha256": "0da368243e967388990f4f4b90e2304044af6187de45f70499a3e4ece8dfd5a8",
|
||||||
"compiled": false,
|
"compiled": false,
|
||||||
"message": "particle_mesh.frag:62: error: \u0027uTextureIndex\u0027 : undeclared identifier"
|
"message": "particle_mesh.frag:64: error: \u0027uTextureIndex\u0027 : undeclared identifier"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -96,13 +94,13 @@
|
||||||
"stage": "vert",
|
"stage": "vert",
|
||||||
"sourceSha256": "d338e9b03686b7baf79d5121c5c8d0f24037979cc58f203957d7bd97b02b1cc2",
|
"sourceSha256": "d338e9b03686b7baf79d5121c5c8d0f24037979cc58f203957d7bd97b02b1cc2",
|
||||||
"compiled": false,
|
"compiled": false,
|
||||||
"message": "sky.vert:151: error: \u0027uUvScroll\u0027 : undeclared identifier"
|
"message": "sky.vert:153: error: \u0027uUvScroll\u0027 : undeclared identifier"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": "frag",
|
"stage": "frag",
|
||||||
"sourceSha256": "8084af39f65ae399c73e3ca864376ef20ba8a1c495ee4774be6a82af3872c51c",
|
"sourceSha256": "8084af39f65ae399c73e3ca864376ef20ba8a1c495ee4774be6a82af3872c51c",
|
||||||
"compiled": false,
|
"compiled": false,
|
||||||
"message": "sky.frag:76: error: \u0027uDiffuse\u0027 : undeclared identifier"
|
"message": "sky.frag:78: error: \u0027uDiffuse\u0027 : undeclared identifier"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -114,13 +112,13 @@
|
||||||
"stage": "vert",
|
"stage": "vert",
|
||||||
"sourceSha256": "4de580ce11b8d755d3558dc49bf7ebccec54d307595d91c38b5c5d552d645c7e",
|
"sourceSha256": "4de580ce11b8d755d3558dc49bf7ebccec54d307595d91c38b5c5d552d645c7e",
|
||||||
"compiled": false,
|
"compiled": false,
|
||||||
"message": "terrain_modern.vert:219: error: \u0027uProjection\u0027 : undeclared identifier"
|
"message": "terrain_modern.vert:221: error: \u0027uProjection\u0027 : undeclared identifier"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"stage": "frag",
|
"stage": "frag",
|
||||||
"sourceSha256": "6003b81df6da6cbea7f00310bd956348bc7b2525345dd490b0b6a3b6428340d9",
|
"sourceSha256": "6003b81df6da6cbea7f00310bd956348bc7b2525345dd490b0b6a3b6428340d9",
|
||||||
"compiled": false,
|
"compiled": false,
|
||||||
"message": "terrain_modern.frag:108: error: \u0027uTexTiling\u0027 : undeclared identifier"
|
"message": "terrain_modern.frag:110: error: \u0027uTexTiling\u0027 : undeclared identifier"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -101,6 +101,17 @@ internal static class VulkanGlslPreamble
|
||||||
// textures stay plain GL_TEXTURE_2D objects.
|
// textures stay plain GL_TEXTURE_2D objects.
|
||||||
text.AppendLine(
|
text.AppendLine(
|
||||||
"#define ACDREAM_SAMPLE_2D(idx, uv) texture(ACDREAM_TEXTURE(idx), vec3((uv), 0.0))");
|
"#define ACDREAM_SAMPLE_2D(idx, uv) texture(ACDREAM_TEXTURE(idx), vec3((uv), 0.0))");
|
||||||
|
// Slice V6e: the 2-D ARRAY read (world meshes, particles, terrain). GL
|
||||||
|
// reconstructs a sampler2DArray from the slot's bindless handle; Vulkan
|
||||||
|
// indexes set 2 directly. Sampling in one expression is what keeps the
|
||||||
|
// nonuniformEXT qualifier on the indexing operation itself.
|
||||||
|
text.AppendLine(
|
||||||
|
"#define ACDREAM_SAMPLE_ARRAY(idx, uvw) texture(ACDREAM_TEXTURE(idx), uvw)");
|
||||||
|
// Slice V6e: the reserved "no texture" slot. Under GL an empty slot can
|
||||||
|
// be recognised by the null handle it holds; a Vulkan descriptor array
|
||||||
|
// has nothing to compare, so the sentinel lives in the index and both
|
||||||
|
// dialects test it the same way. See common.glsl for the GL half.
|
||||||
|
text.AppendLine("#define ACDREAM_TEXTURE_NONE 0xFFFFFFFFu");
|
||||||
text.AppendLine();
|
text.AppendLine();
|
||||||
text.AppendLine("// §3.4 push constants: one shared 96-byte block, so switching pipelines");
|
text.AppendLine("// §3.4 push constants: one shared 96-byte block, so switching pipelines");
|
||||||
text.AppendLine("// mid-pass invalidates neither descriptors nor constants.");
|
text.AppendLine("// mid-pass invalidates neither descriptors nor constants.");
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue