Rether / docs
engine/gamegine/Shader contract
referenceEvery binding, push constant and vertex attribute, in one table

Shader contract

updated 2026.08.10created 2026.08.10~3 min

The interface between the C++ side and shader.slang. Both sides must agree exactly; nothing validates it at compile time, so this note is the authority. Vulkan validation layers catch the mismatches at pipeline creation, which is the next best thing.

Vertex input

From core::Vertex, declared once in core_vertex.h and pushed into every pipeline config by populatePipelineConfig.

LocationTypeMember
0float3position
1float3normal
2float2uv

No TANGENT. The fragment shader rebuilds the tangent frame from screen space derivatives.

Push constants

BytesContentStages
0–63float4x4 modelvertex

Declared as rhi::MeshPushConstants. The guaranteed minimum push constant block is 128 bytes on any Vulkan device, and this already spends half of it — which is why per-material data goes in a uniform buffer instead.

Set 0 — global

Created in Renderer::initialize, one set for the whole frame.

BindingTypeStagesC++
0uniform buffervertex + fragmentUniformBufferObject
// cpp
struct UniformBufferObject {
    glm::mat4 viewProj;
    glm::vec4 cameraPosition;  // w is padding
};

Both stages, not just vertex: the fragment shader needs the camera position to build the view vector. Declaring this binding vertex-only makes vkCreateGraphicsPipelines fail with VUID-VkGraphicsPipelineCreateInfo-layout-07988.

cameraPosition is a vec4 because a vec3 aligns on 16 bytes in std140 — the fourth component is padding, not data.

Set 1 — material

One set per material, allocated by AssetManager from Renderer::materialLayout(). Binding indices are the values of rnd::TextureSlot, so the enum is the contract.

BindingTypeSlotDevice format
0sampler2DBaseColorR8G8B8A8_SRGB
1sampler2DMetallicRoughnessR8G8B8A8_UNorm
2sampler2DNormalR8G8B8A8_UNorm
3sampler2DOcclusionR8G8B8A8_UNorm
4sampler2DEmissiveR8G8B8A8_SRGB
5uniform bufferMaterialParams

Binding 5 is rnd::k_PARAMS_BINDING, defined as k_TEXTURE_SLOT_COUNT so the two can never drift.

Base color and emissive carry sRGB encoded colors the hardware converts on every fetch. The other three carry linear measurements that must reach the shader untouched. The format follows the slot, not the file — the same image used in two slots is two GPU resources, which is why the texture cache is keyed by name and format.

Every binding is written on every material. A slot with no texture gets a 1×1 neutral: white where the factor multiplies, (128, 128, 255) for the normal slot, which is the encoding of (0, 0, 1).

MaterialParams

48 bytes, no padding. The member order is deliberate — metallicFactor fills the four bytes std140 leaves behind a vec3.

OffsetTypeMember
0vec4baseColorFactor
16vec3emissiveFactor
28floatmetallicFactor
32floatroughnessFactor
36floatnormalScale
40floatocclusionStrength
44floatalphaCutoff

alphaCutoff is set to 0.0 for anything that is not AlphaMode::Mask. Alpha is never negative, so baseColor.a < 0.0 is never true and nothing discards — an opaque material ignores its alpha and a blended one composites it, with no branch and no extra uniform in the shader.

Attachments

Format
Colorswapchain format (B8G8R8A8_SRGB)
DepthD32_SFloat

The color attachment being sRGB means the shader writes linear values and the hardware encodes them. Do not apply gamma in the shader.

Compilation

game/shaders/shader.slang holds both stages, compiled by slangc through the shader_compile CMake target, which example_gamegine depends on. The .spv regenerates on build — there is no manual step.

Keyboard

search this vaultK / /
step the sidebarjk
open selected
switch browse mode123
switch vaultgv
toggle themet
close / clearesc
this help?
esc to close