| /* | |
| * Copyright 2026 Rive | |
| */ | |
| #include "rive/renderer/ore/hlsl_struct_layout.hpp" | |
| // Parser + register stripper are always compiled. Dead-code elimination | |
| // strips them when no caller references them, so non-Unreal binaries pay | |
| // nothing for the support. Compiling unconditionally lets unit tests | |
| // exercise the parser without depending on the `for_unreal` premake gate. | |
| #include <cstring> | |
| #include <regex> | |
| namespace rive::ore | |
| { | |
| namespace | |
| { | |
| inline uint16_t readU16LE(const uint8_t* p) | |
| { | |
| return static_cast<uint16_t>(p[0]) | (static_cast<uint16_t>(p[1]) << 8); | |
| } | |
| inline uint32_t readU32LE(const uint8_t* p) | |
| { | |
| return static_cast<uint32_t>(p[0]) | (static_cast<uint32_t>(p[1]) << 8) | | |
| (static_cast<uint32_t>(p[2]) << 16) | | |
| (static_cast<uint32_t>(p[3]) << 24); | |
| } | |
| inline int32_t readI32LE(const uint8_t* p) | |
| { | |
| return static_cast<int32_t>(readU32LE(p)); | |
| } | |
| // Read a u16-length-prefixed string at *p. Advances *p past the string on | |
| // success. Returns false if the string would run past `end`. | |
| bool readString(const uint8_t** p, const uint8_t* end, std::string* out) | |
| { | |
| if (*p + 2 > end) | |
| return false; | |
| const uint16_t len = readU16LE(*p); | |
| *p += 2; | |
| if (*p + len > end) | |
| return false; | |
| out->assign(reinterpret_cast<const char*>(*p), len); | |
| *p += len; | |
| return true; | |
| } | |
| } // namespace | |
| bool HLSLStructLayout::fromBlob(const uint8_t* data, | |
| size_t size, | |
| HLSLStructLayout* out) | |
| { | |
| if (out == nullptr || data == nullptr) | |
| return false; | |
| out->structs.clear(); | |
| out->resources.clear(); | |
| constexpr size_t kHeaderSize = 12; | |
| if (size < kHeaderSize) | |
| return false; | |
| if (readU32LE(data) != kHLSLStructLayoutMagic) | |
| return false; | |
| if (readU16LE(data + 4) != kHLSLStructLayoutVersion) | |
| return false; | |
| // bytes [6..8) reserved | |
| const uint16_t structCount = readU16LE(data + 8); | |
| const uint16_t resourceCount = readU16LE(data + 10); | |
| const uint8_t* p = data + kHeaderSize; | |
| const uint8_t* end = data + size; | |
| out->structs.reserve(structCount); | |
| for (uint16_t s = 0; s < structCount; ++s) | |
| { | |
| HLSLStructLayoutStruct st; | |
| if (!readString(&p, end, &st.name)) | |
| return false; | |
| if (p + 4 + 2 > end) | |
| return false; | |
| st.declaredSize = readU32LE(p); | |
| p += 4; | |
| const uint16_t memberCount = readU16LE(p); | |
| p += 2; | |
| st.members.reserve(memberCount); | |
| for (uint16_t m = 0; m < memberCount; ++m) | |
| { | |
| HLSLStructLayoutMember mb; | |
| if (!readString(&p, end, &mb.name)) | |
| return false; | |
| // 4 (offset) + 4 (base_type/rows/cols/flags) + 4 + 4 + 4 + 4 | |
| constexpr size_t kMemberFixed = 24; | |
| if (p + kMemberFixed > end) | |
| return false; | |
| mb.offset = readU32LE(p); | |
| p += 4; | |
| mb.baseType = static_cast<HLSLStructLayoutBaseType>(p[0]); | |
| mb.numRows = p[1]; | |
| mb.numColumns = p[2]; | |
| const uint8_t flags = p[3]; | |
| mb.isArray = (flags & kHLSLStructLayoutMemberFlagArray) != 0; | |
| p += 4; | |
| mb.arraySize = readU32LE(p); | |
| p += 4; | |
| mb.arrayStride = readU32LE(p); | |
| p += 4; | |
| mb.matrixStride = readU32LE(p); | |
| p += 4; | |
| mb.structIndex = readI32LE(p); | |
| p += 4; | |
| // Schema invariant: struct_index for Struct members must | |
| // reference a strictly-earlier table entry (topological order | |
| // guarantees children are visited before parents). | |
| if (mb.baseType == HLSLStructLayoutBaseType::Struct) | |
| { | |
| if (mb.structIndex < 0 || | |
| static_cast<size_t>(mb.structIndex) >= s) | |
| return false; | |
| } | |
| st.members.push_back(std::move(mb)); | |
| } | |
| out->structs.push_back(std::move(st)); | |
| } | |
| out->resources.reserve(resourceCount); | |
| for (uint16_t r = 0; r < resourceCount; ++r) | |
| { | |
| HLSLStructLayoutResource res; | |
| if (!readString(&p, end, &res.name)) | |
| return false; | |
| // 1 (kind) + 1 (texture_view_dim) + 1 (texture_sample_type) + | |
| // 1 (texture_multisampled) + 1 (stage_mask) + 4 (struct_index) = 9 | |
| constexpr size_t kResourceFixed = 9; | |
| if (p + kResourceFixed > end) | |
| return false; | |
| res.kind = static_cast<ResourceKind>(p[0]); | |
| res.textureViewDim = static_cast<TextureViewDim>(p[1]); | |
| res.textureSampleType = static_cast<TextureSampleType>(p[2]); | |
| res.textureMultisampled = (p[3] != 0); | |
| res.stageMask = p[4]; | |
| p += 5; | |
| res.structIndex = readI32LE(p); | |
| p += 4; | |
| if (res.structIndex >= 0 && | |
| static_cast<size_t>(res.structIndex) >= out->structs.size()) | |
| return false; | |
| out->resources.push_back(std::move(res)); | |
| } | |
| return true; | |
| } | |
| namespace | |
| { | |
| const char* baseScalarHLSLName(HLSLStructLayoutBaseType t) | |
| { | |
| switch (t) | |
| { | |
| case HLSLStructLayoutBaseType::Bool: | |
| return "bool"; | |
| case HLSLStructLayoutBaseType::Int: | |
| return "int"; | |
| case HLSLStructLayoutBaseType::UInt: | |
| return "uint"; | |
| case HLSLStructLayoutBaseType::Half: | |
| return "half"; | |
| case HLSLStructLayoutBaseType::Float: | |
| return "float"; | |
| case HLSLStructLayoutBaseType::Double: | |
| return "double"; | |
| case HLSLStructLayoutBaseType::Struct: | |
| case HLSLStructLayoutBaseType::Unknown: | |
| return ""; | |
| } | |
| return ""; | |
| } | |
| const char* textureViewDimHLSLName(TextureViewDim d, bool multisampled) | |
| { | |
| using D = TextureViewDim; | |
| if (multisampled) | |
| { | |
| // MS variants only exist for 2D / 2DArray. Cube / 3D MS aren't a | |
| // thing in HLSL. | |
| if (d == D::D2Array) | |
| return "Texture2DMSArray"; | |
| return "Texture2DMS"; | |
| } | |
| switch (d) | |
| { | |
| case D::D1: | |
| return "Texture1D"; | |
| case D::D2: | |
| return "Texture2D"; | |
| case D::D2Array: | |
| return "Texture2DArray"; | |
| case D::Cube: | |
| return "TextureCube"; | |
| case D::CubeArray: | |
| return "TextureCubeArray"; | |
| case D::D3: | |
| return "Texture3D"; | |
| case D::Undefined: | |
| return "Texture2D"; // safe default; HLSL most common | |
| } | |
| return "Texture2D"; | |
| } | |
| const char* textureSampleTypeHLSLName(TextureSampleType t) | |
| { | |
| using ST = TextureSampleType; | |
| switch (t) | |
| { | |
| case ST::Float: | |
| case ST::UnfilterableFloat: | |
| case ST::Depth: | |
| return "float4"; | |
| case ST::Sint: | |
| return "int4"; | |
| case ST::Uint: | |
| return "uint4"; | |
| case ST::Undefined: | |
| return "float4"; | |
| } | |
| return "float4"; | |
| } | |
| } // namespace | |
| std::string hlslTypeStringForMember(const HLSLStructLayoutMember& member, | |
| const HLSLStructLayout& layout) | |
| { | |
| if (member.baseType == HLSLStructLayoutBaseType::Struct) | |
| { | |
| if (member.structIndex < 0 || | |
| static_cast<size_t>(member.structIndex) >= layout.structs.size()) | |
| return ""; | |
| return layout.structs[static_cast<size_t>(member.structIndex)].name; | |
| } | |
| const char* base = baseScalarHLSLName(member.baseType); | |
| if (*base == 0) | |
| return ""; | |
| // Scalar: just "float" / "uint" / etc. | |
| if (member.numRows <= 1 && member.numColumns <= 1) | |
| return base; | |
| // Vector: "floatN" where N is the column count. | |
| if (member.numRows <= 1) | |
| return std::string(base) + std::to_string(member.numColumns); | |
| // Matrix: "floatRxC". | |
| return std::string(base) + std::to_string(member.numRows) + "x" + | |
| std::to_string(member.numColumns); | |
| } | |
| std::string hlslTypeStringForResource(const HLSLStructLayoutResource& resource, | |
| const HLSLStructLayout& layout) | |
| { | |
| switch (resource.kind) | |
| { | |
| case ResourceKind::UniformBuffer: | |
| { | |
| // Unreal references the cbuffer's block struct directly by | |
| // name (AddIncludedStruct / AddNestedStruct take struct | |
| // metadata, not a string). Return the struct's HLSL name so | |
| // the caller can match it against `layout.structs`. | |
| if (resource.structIndex >= 0 && | |
| static_cast<size_t>(resource.structIndex) < | |
| layout.structs.size()) | |
| { | |
| return layout.structs[static_cast<size_t>(resource.structIndex)] | |
| .name; | |
| } | |
| return ""; | |
| } | |
| case ResourceKind::StorageBufferRO: | |
| { | |
| std::string inner = "float4"; // safe default | |
| if (resource.structIndex >= 0 && | |
| static_cast<size_t>(resource.structIndex) < | |
| layout.structs.size()) | |
| { | |
| inner = | |
| layout.structs[static_cast<size_t>(resource.structIndex)] | |
| .name; | |
| } | |
| return "StructuredBuffer<" + inner + ">"; | |
| } | |
| case ResourceKind::StorageBufferRW: | |
| { | |
| std::string inner = "float4"; | |
| if (resource.structIndex >= 0 && | |
| static_cast<size_t>(resource.structIndex) < | |
| layout.structs.size()) | |
| { | |
| inner = | |
| layout.structs[static_cast<size_t>(resource.structIndex)] | |
| .name; | |
| } | |
| return "RWStructuredBuffer<" + inner + ">"; | |
| } | |
| case ResourceKind::SampledTexture: | |
| return std::string( | |
| textureViewDimHLSLName(resource.textureViewDim, | |
| resource.textureMultisampled)) + | |
| "<" + textureSampleTypeHLSLName(resource.textureSampleType) + | |
| ">"; | |
| case ResourceKind::StorageTexture: | |
| { | |
| // RW variants of texture types; SPIR-V doesn't carry the | |
| // exact texel format here, so default to float4 — the Unreal | |
| // RHI can override based on its own reflection if it cares. | |
| const char* dim = | |
| textureViewDimHLSLName(resource.textureViewDim, false); | |
| // Texture2D -> RWTexture2D, Texture3D -> RWTexture3D, etc. | |
| return std::string("RW") + dim + "<" + | |
| textureSampleTypeHLSLName(resource.textureSampleType) + ">"; | |
| } | |
| case ResourceKind::Sampler: | |
| return "SamplerState"; | |
| case ResourceKind::ComparisonSampler: | |
| return "SamplerComparisonState"; | |
| } | |
| return ""; | |
| } | |
| std::string stripHLSLRegisterAnnotations(const std::string& hlsl) | |
| { | |
| // Pattern: optional whitespace, ':' , optional whitespace, the literal | |
| // "register(", anything that isn't ')', closing ')'. Replaced with | |
| // empty string globally. See the design note in the .hpp header for | |
| // the false-positive analysis — SPIRV-Cross-emitted HLSL has no other | |
| // ':' annotation that could match this shape. | |
| // | |
| // ECMAScript regex (std::regex default); `\s` matches newlines so | |
| // multi-line declarations are handled correctly. | |
| static const std::regex kRegisterAnnotation(R"(\s*:\s*register\([^)]*\))"); | |
| return std::regex_replace(hlsl, kRegisterAnnotation, std::string()); | |
| } | |
| } // namespace rive::ore |