blob: ea62e1cf3efbf7f173564a0c43baf5911268b82c [file]
/*
* 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