ShaderBundle RIFF Format (YSLB)#
The ShaderBundle class serialises a compiled + transpiled shader program into a
compact binary container based on the RIFF (Resource Interchange File Format)
chunk structure. This lets a shader be compiled once (through glslang /
SPIRV-Cross) and reused at runtime without paying the compilation cost again.
A bundle stores two kinds of payload:
The SPIR-V binary for each pipeline stage.
One
ShaderInfoper(stage × target language)combination, each holding the transpiled source, the original Vulkan GLSL input source, and fullShaderReflection.
Reference implementation: shading/yup_ShaderBundle.cpp
1. Primitives & Conventions#
Byte order#
All multi-byte integers are written little-endian (YUP writeInt / readInt).
This includes chunk sizes, enum values, and string lengths.
FourCC#
A FourCC is a 32-bit tag built from four ASCII characters, packed least-significant-byte first so the bytes appear in reading order on disk:
makeFourCC('R','I','F','F') → bytes: 52 49 46 46 ('R' 'I' 'F' 'F')
Chunk layout#
Every chunk follows the classic RIFF triplet. The size counts only the data
bytes (not the header, not the padding). Chunks are padded to an even byte
boundary; the pad byte is 0x00 and is not included in size.
0 4 8 8 + size
+--------+--------+---------------------------+-----------+
| FourCC | size | data ... | pad(0/1) |
+--------+--------+---------------------------+-----------+
4 bytes 4 bytes 'size' bytes (LE u32) 0 or 1 byte
(present when
size is odd)
LIST chunk#
A LIST chunk is a container whose data begins with a 4-byte list type
FourCC, followed by an arbitrary sequence of sub-chunks:
+--------+--------+----------+-----------------------------+
| 'LIST' | size | listType | sub-chunk, sub-chunk, ... |
+--------+--------+----------+-----------------------------+
4 bytes
String encoding#
Two string forms are used:
Length-prefixed (
writeStringRaw):int32UTF-8 byte length, then the raw UTF-8 bytes (no NUL terminator). A length<= 0decodes to an empty string.Raw / chunk-sized: the string fills the entire chunk data (e.g.
ISRC). Its length is implied by the chunksize.
2. Top-Level Structure#
The whole file is a single RIFF chunk whose form type is YSLB.
+===========================================================================+
| RIFF | size | 'YSLB' |
+===========================================================================+
| |
| +--------+--------+------------------+ |
| | VERS | 4 | version (u32) | format version = 2 |
| +--------+--------+------------------+ |
| |
| +--------+--------+----------+ |
| | LIST | size | 'SHAD' | <-- list of shader stages |
| +--------+--------+----------+ |
| | |
| +---> SHDR (stage 0) |
| +---> SHDR (stage 1) |
| +---> ... |
+===========================================================================+
FourCC |
Kind |
Meaning |
|---|---|---|
|
chunk |
Outer container. Form type is |
|
form |
Bundle magic (“YUP Shader Language Bundle”). |
|
chunk |
Format version (currently |
|
list |
List type |
On load, the reader rejects the file if
VERSis missing or if the version does not exactly match the supported version (kCurrentVersion = 2). Both older and newer versions are rejected with an error message that identifies the mismatch.
3. Per-Stage Chunk (SHDR)#
Each SHDR chunk inside the SHAD list describes one pipeline stage. It begins
with two int32 header fields, followed by the SPIR-V blob and a VARS list of
transpiled language variants.
+--------+--------+---------------------------------------------------------+
| 'SHDR' | size | |
+--------+--------+ |
| |
| +------------------+ stage (int32, ShaderStage enum) |
| +------------------+ srcLang (int32, ShaderLanguage enum) |
| |
| +--------+--------+----------------------------+ |
| | SPVB | size | SPIR-V binary (bytes) | binary chunk |
| +--------+--------+----------------------------+ |
| |
| +--------+--------+----------+ |
| | LIST | size | 'VARS' | <-- list of transpiled variants |
| +--------+--------+----------+ |
| | |
| +---> VART (variant 0) |
| +---> VART (variant 1) |
| +---> ... |
+---------------------------------------------------------------------------+
Field |
Type |
Meaning |
|---|---|---|
|
|
|
|
|
|
|
chunk |
SPIR-V binary for this stage. May be empty (size |
|
list |
List type |
Stage ordering: stages that appear in
shadersare written first (in insertion order), then any SPIR-V-only stages that have no transpiled variant.
4. Per-Variant Chunk (VART)#
Each VART chunk holds one transpiled (stage × language) variant. The stage is
inherited from the enclosing SHDR; the variant only stores its target language,
entry point, transpiled source, optional original input source, and reflection.
+--------+--------+--------------------------------------------------------+
| 'VART' | size | |
+--------+--------+ |
| |
| +------------------+ language (int32, ShaderLanguage enum) |
| |
| +------------------+------------------------+ |
| | len (int32) | entryPoint (UTF-8 bytes) | length-prefixed str |
| +------------------+------------------------+ |
| |
| +------------------+------------------------+ |
| | len (int32) | source (UTF-8 bytes) | length-prefixed str |
| +------------------+------------------------+ |
| |
| +--------+--------+----------------------------+ (optional) |
| | ISRC | size | input source (UTF-8) | raw chunk-sized str |
| +--------+--------+----------------------------+ |
| |
| +--------+--------+----------------------------+ |
| | REFL | size | reflection (archive blob) | binary chunk |
| +--------+--------+----------------------------+ |
+--------------------------------------------------------------------------+
Field |
Type |
Meaning |
|---|---|---|
|
|
Target |
|
prefixed str |
Entry-point function name (e.g. |
|
prefixed str |
Transpiled source code in |
|
chunk |
Original Vulkan GLSL input source, raw UTF-8. Optional — omitted when empty. |
|
chunk |
Serialised |
ISRC — Original input source#
The ISRC sub-chunk stores the original Vulkan GLSL (#version 450) source that
was compiled to SPIR-V for this stage. It is populated by ShaderBundleCompiler
from ShaderBundleCompileRequest::source and maps to ShaderInfo::inputSource.
Why this matters for GL/GLES recompilation. The source field contains
SPIRV-Cross decompiled output (already-combined sampler2D uniforms). Feeding
that back into glslang as Vulkan GLSL produces SPIR-V with no separate
image/sampler resources, so build_combined_image_samplers() returns nothing,
glCombinedSamplers is empty, and the GL fixup blob is empty — causing samplers
to default to texture unit 0 (black texture). ISRC preserves the original source
so compileFromGlsl() round-trips correctly.
When ISRC is absent (bundles without an input source), ShaderInfo::inputSource
is empty and callers should fall back to ShaderInfo::source.
5. Reflection Blob (REFL)#
The REFL chunk data is not RIFF; it is a BinaryOutputArchive blob produced
by YUP’s serialisation layer (detail::doSave / detail::doLoad) over the
ShaderReflection struct. Its internal layout is owned by the archive format and
the SerialisationTraits<ShaderReflection> specialisation in
shading/yup_ShaderBundle.h.
Serialised reflection fields include:
entryPointsResource-binding arrays:
uniformBuffers,storageBuffers,stageInputs,stageOutputs,subpassInputs,storageImages,sampledImages,atomicCounters,accelerationStructures,glPlainUniforms,tensors,pushConstantBuffers,shaderRecordBuffers,separateImages,separateSamplersbuiltinInputs,builtinOutputsspecConstantsworkgroupSizepositionInvariantcapabilities,extensionsglCombinedSamplers(folded combinedsampler2Dentries with texture units, used bymakeGLFixupBlobto bind samplers by name after program linking)
Because reflection uses the versioned archive layer, its schema can evolve independently of the RIFF envelope version.
6. FourCC Reference#
FourCC |
Constant |
Role |
|---|---|---|
|
|
Outer RIFF container. |
|
|
Generic list container. |
|
|
Bundle form type (magic). |
|
|
Format version chunk. |
|
|
List type for the stage list. |
|
|
Per-stage chunk. |
|
|
SPIR-V binary chunk. |
|
|
List type for the variant list. |
|
|
Per-variant chunk. |
|
|
Original input source chunk (per-variant, optional). |
|
|
Reflection archive chunk. |
Current format version: 2 (kCurrentVersion).
Version history#
Version |
Change |
|---|---|
1 |
Initial format. Top-level |
2 |
Removed |
7. Enum Encodings#
Enums are serialised as their underlying int32 ordinal (declaration order in
shading/yup_ShaderTypes.h).
ShaderStage
Value |
Name |
|---|---|
0 |
|
1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
ShaderLanguage
Value |
Name |
|---|---|
0 |
|
1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
8. Complete Nesting Overview#
RIFF 'YSLB'
├── VERS format version (u32) = 2
└── LIST 'SHAD' stage list
├── SHDR stage #0
│ ├── stage (i32)
│ ├── srcLang (i32)
│ ├── SPVB SPIR-V binary
│ └── LIST 'VARS' variant list
│ ├── VART variant #0
│ │ ├── language (i32)
│ │ ├── entryPoint (len-prefixed str)
│ │ ├── source (len-prefixed str)
│ │ ├── ISRC original input source (optional, raw UTF-8)
│ │ └── REFL reflection archive blob
│ └── VART ...
└── SHDR ...
9. Reading & Writing#
The API mirrors save/load across streams, files, raw data, and MemoryBlock:
// Compile once and persist.
yup::ShaderBundleCompiler compiler;
auto result = compiler.compile (request);
if (result)
result.getReference().saveToFile (yup::File ("myShader.ysl"));
// Load at runtime and look up a variant.
auto loaded = yup::ShaderBundle::loadFromFile (yup::File ("myShader.ysl"));
if (loaded)
if (auto* info = loaded.getReference().findShader (ShaderStage::vertex, ShaderLanguage::msl))
useSource (info->source);
// For live recompilation on GL/GLES, prefer inputSource over source:
if (auto* info = loaded.getReference().findShader (ShaderStage::vertex, ShaderLanguage::glsl))
{
const auto& src = info->inputSource.isNotEmpty() ? info->inputSource : info->source;
auto recompiled = GpuPipeline::compileFromGlsl (ctx, src, fragSrc, options);
}
Save |
Load |
|---|---|
|
|
|
|
|
|
|
Version rejection#
The loader performs an exact version check: if VERS != kCurrentVersion (2),
loading fails immediately with an error message of the form:
ShaderBundle: bundle version N is not supported (expected 2)
This applies to both older bundles (version 1, missing ISRC) and future bundles.
Existing .ysl files must be regenerated with the current yup_shader_bundler
tool whenever the format version changes.
Parser robustness#
Within a given version, the loader is tolerant of unknown chunks inside VART:
iterateChunks walks every [fourcc, size, data] triplet and ignores tags it
does not recognise, consuming pad bytes between chunks. Adding new optional
sub-chunks to VART in a future version is safe as long as the VERS value is
bumped and old bundles are regenerated.