325 lines
17 KiB
C#
325 lines
17 KiB
C#
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
|
|
using SharpEmu.ShaderCompiler;
|
|
|
|
namespace SharpEmu.ShaderCompiler.Vulkan;
|
|
|
|
// IMAGE_BVH_INTERSECT_RAY tests one ray against one BVH node; the guest shader owns the
|
|
// traversal loop and stack. This is the GFX10 (RT IP 1.1) node test as AMD's GPURT
|
|
// software fallback defines it (IntersectCommon.hlsl, image_bvh64_intersect_ray_base),
|
|
// with node layouts from GPURT's gfx10 TriangleNode1_0 and BoxNode1_0.
|
|
public static partial class Gen5SpirvTranslator
|
|
{
|
|
private sealed partial class CompilationContext
|
|
{
|
|
private const uint InvalidNode = 0xFFFF_FFFF;
|
|
|
|
// Node pointer: the byte offset divided by eight above bits 2:0, which hold the type.
|
|
private const uint NodeTypeTriangle1 = 1;
|
|
private const uint NodeTypeBoxFloat16 = 4;
|
|
private const uint NodeTypeBoxFloat32 = 5;
|
|
|
|
// Triangle node: four float3 vertices, then flags/indices, the triangle id at dword 15.
|
|
private const uint TriangleIdDword = 15;
|
|
private const int TriangleIdBitStride = 8;
|
|
private const int TriangleIdISourceShift = 0;
|
|
private const int TriangleIdJSourceShift = 2;
|
|
|
|
// Box nodes: four child pointers, then per child a float3 min/max pair (six floats)
|
|
// or six packed halves (three dwords).
|
|
private const uint BoxBoundsDword = 4;
|
|
|
|
private uint[]? _rayResult;
|
|
|
|
private void EmitRayIntersect(Gen5RayIntersectControl ray, bool bvh64)
|
|
{
|
|
_rayResult ??= DeclareRayResult();
|
|
EmitExecConditional(() => EmitRayIntersectActive(ray, bvh64));
|
|
}
|
|
|
|
private uint[] DeclareRayResult()
|
|
{
|
|
var result = new uint[Gen5RayIntersectControl.ResultDwords];
|
|
for (var component = 0; component < result.Length; component++)
|
|
{
|
|
result[component] = _module.AddGlobalVariable(_privateUintPointer, SpirvStorageClass.Private, UInt(InvalidNode));
|
|
_module.AddName(result[component], $"rayIntersectResult{component}");
|
|
_interfaces.Add(result[component]);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private void EmitRayIntersectActive(Gen5RayIntersectControl ray, bool bvh64)
|
|
{
|
|
var result = _rayResult!;
|
|
var nodeLow = LoadV(ray.GetAddressRegister(0));
|
|
var nodePointer = bvh64 ? Pair64(nodeLow, LoadV(ray.GetAddressRegister(1))) : Widen(nodeLow);
|
|
var nodeType = BitwiseAnd(nodeLow, UInt(7));
|
|
var nodeIndex = ShiftRightLogical64(nodePointer, ULong(3));
|
|
|
|
// The BVH T#: base_address[39:0] in 256-byte units, box_grow_value [62:55],
|
|
// box_sort_en [63], size [105:64] (the last valid 64-byte node index),
|
|
// triangle_return_mode [120].
|
|
var word0 = LoadS(ray.ScalarResource);
|
|
var word1 = LoadS(ray.ScalarResource + 1);
|
|
var word2 = LoadS(ray.ScalarResource + 2);
|
|
var word3 = LoadS(ray.ScalarResource + 3);
|
|
var baseUnits = Pair64(word0, BitwiseAnd(word1, UInt(0xFF)));
|
|
var bvhBase = ShiftLeftLogical64(baseUnits, ULong(8));
|
|
var lastNode = Pair64(word2, BitwiseAnd(word3, UInt(0x3FF)));
|
|
var boxGrow = BitwiseAnd(ShiftRightLogical(word1, UInt(23)), UInt(0xFF));
|
|
var boxSort = IsNotZero(BitwiseAnd(word1, UInt(0x8000_0000)));
|
|
var barycentrics = IsNotZero(BitwiseAnd(word3, UInt(1u << 24)));
|
|
var nodeAddress = IAdd64(bvhBase, ShiftLeftLogical64(And64(nodePointer, ULong(~7ul)), ULong(3)));
|
|
|
|
var present = _module.AddInstruction(SpirvOp.INotEqual, _boolType, baseUnits, ULong(0));
|
|
var inRange = _module.AddInstruction(SpirvOp.ULessThanEqual, _boolType, nodeIndex, lastNode);
|
|
// A float32 box node spans two 64-byte units.
|
|
var wideInRange = _module.AddInstruction(SpirvOp.ULessThan, _boolType, nodeIndex, lastNode);
|
|
var valid = LogicalAnd(present, inRange);
|
|
var isTriangle = LogicalAnd(valid, _module.AddInstruction(SpirvOp.ULessThanEqual, _boolType, nodeType, UInt(NodeTypeTriangle1)));
|
|
var isBox16 = LogicalAnd(valid, _module.AddInstruction(SpirvOp.IEqual, _boolType, nodeType, UInt(NodeTypeBoxFloat16)));
|
|
var isBox32 = LogicalAnd(LogicalAnd(present, wideInRange),
|
|
_module.AddInstruction(SpirvOp.IEqual, _boolType, nodeType, UInt(NodeTypeBoxFloat32)));
|
|
|
|
var extent = Bitcast(_floatType, LoadV(ray.GetAddressRegister(bvh64 ? 2 : 1)));
|
|
var origin = new uint[3];
|
|
for (var axis = 0; axis < 3; axis++)
|
|
origin[axis] = Bitcast(_floatType, LoadV(ray.GetAddressRegister((bvh64 ? 3 : 2) + axis)));
|
|
var direction = LoadRayVector(ray, bvh64, inverse: false);
|
|
var inverseDirection = LoadRayVector(ray, bvh64, inverse: true);
|
|
|
|
// Other node types (user nodes 6 and 7, unused 2 and 3), a null BVH or an
|
|
// out-of-range node return four invalid dwords.
|
|
foreach (var variable in result)
|
|
Store(variable, UInt(InvalidNode));
|
|
EmitConditional(isTriangle, () =>
|
|
{
|
|
var values = EmitRayTriangle(nodeAddress, nodeType, barycentrics, origin, direction);
|
|
for (var component = 0; component < values.Length; component++)
|
|
Store(result[component], values[component]);
|
|
});
|
|
// Each box format reads only its own node bytes.
|
|
void EmitBoxes(bool fp16)
|
|
{
|
|
var children = EmitRayBoxes(nodeAddress, fp16, boxGrow, boxSort, extent, origin, inverseDirection);
|
|
for (var component = 0; component < children.Length; component++)
|
|
Store(result[component], children[component]);
|
|
}
|
|
|
|
EmitConditional(isBox16, () => EmitBoxes(fp16: true));
|
|
EmitConditional(isBox32, () => EmitBoxes(fp16: false));
|
|
|
|
for (var component = 0u; component < Gen5RayIntersectControl.ResultDwords; component++)
|
|
StoreV(ray.VectorData + component, Load(_uintType, result[component]));
|
|
}
|
|
|
|
// Direction and inverse direction follow the origin; A16 packs both as six halves
|
|
// into three dwords.
|
|
private uint[] LoadRayVector(Gen5RayIntersectControl ray, bool bvh64, bool inverse)
|
|
{
|
|
var first = bvh64 ? 6 : 5;
|
|
var values = new uint[3];
|
|
if (!ray.A16)
|
|
{
|
|
for (var axis = 0; axis < 3; axis++)
|
|
values[axis] = Bitcast(_floatType, LoadV(ray.GetAddressRegister(first + (inverse ? 3 : 0) + axis)));
|
|
return values;
|
|
}
|
|
|
|
for (var axis = 0; axis < 3; axis++)
|
|
{
|
|
var half = (inverse ? 3 : 0) + axis;
|
|
var word = LoadV(ray.GetAddressRegister(first + half / 2));
|
|
var bits = half % 2 == 0 ? BitwiseAnd(word, UInt(0xFFFF)) : ShiftRightLogical(word, UInt(16));
|
|
values[axis] = Bitcast(_floatType, EmitHalfToFloat(bits));
|
|
}
|
|
|
|
return values;
|
|
}
|
|
|
|
private uint LoadNodeDword(uint nodeAddress, uint dword) =>
|
|
LoadDeviceDword(IAdd64(nodeAddress, ULong(dword * 4ul)));
|
|
|
|
private uint LoadNodeFloat(uint nodeAddress, uint dword) =>
|
|
Bitcast(_floatType, LoadNodeDword(nodeAddress, dword));
|
|
|
|
// fast_intersect_triangle and SwizzleBarycentrics. Node type 0 tests (v0, v1, v2),
|
|
// type 1 tests (v1, v3, v2). A miss returns t_num = +inf over t_denom = 1. The
|
|
// barycentric return mode yields the i/j numerators the builder's rotation maps
|
|
// back; the other mode yields the triangle id and a hit flag.
|
|
private uint[] EmitRayTriangle(uint nodeAddress, uint nodeType, uint barycentrics,
|
|
IReadOnlyList<uint> origin, IReadOnlyList<uint> direction)
|
|
{
|
|
var second = _module.AddInstruction(SpirvOp.IEqual, _boolType, nodeType, UInt(NodeTypeTriangle1));
|
|
var v1 = new uint[3];
|
|
var v2 = new uint[3];
|
|
var v3 = new uint[3];
|
|
for (var axis = 0u; axis < 3; axis++)
|
|
{
|
|
var first = LoadNodeFloat(nodeAddress, axis);
|
|
var vertex1 = LoadNodeFloat(nodeAddress, 3 + axis);
|
|
var vertex2 = LoadNodeFloat(nodeAddress, 6 + axis);
|
|
var vertex3 = LoadNodeFloat(nodeAddress, 9 + axis);
|
|
v1[axis] = SelectF(second, vertex1, first);
|
|
v2[axis] = SelectF(second, vertex3, vertex1);
|
|
v3[axis] = vertex2;
|
|
}
|
|
|
|
var e1 = Subtract(v2, v1);
|
|
var e2 = Subtract(v3, v1);
|
|
var e3 = Subtract(origin, v1);
|
|
var s1 = Cross(direction, e2);
|
|
var s2 = Cross(e3, e1);
|
|
var tNum = Dot(e2, s2);
|
|
var tDenom = Dot(s1, e1);
|
|
var iNum = Dot(e3, s1);
|
|
var jNum = Dot(direction, s2);
|
|
|
|
var t = FDiv(tNum, tDenom);
|
|
var u = FDiv(iNum, tDenom);
|
|
var v = FDiv(jNum, tDenom);
|
|
var zero = Float(0f);
|
|
var one = Float(1f);
|
|
var missed = Any(
|
|
FCompare(SpirvOp.FOrdLessThan, u, zero),
|
|
FCompare(SpirvOp.FOrdGreaterThan, u, one),
|
|
FCompare(SpirvOp.FOrdLessThan, v, zero),
|
|
FCompare(SpirvOp.FOrdGreaterThan, FAdd(u, v), one),
|
|
FCompare(SpirvOp.FOrdLessThan, t, zero));
|
|
tNum = SelectF(missed, Float(float.PositiveInfinity), tNum);
|
|
tDenom = SelectF(missed, one, tDenom);
|
|
|
|
var triangleId = LoadNodeDword(nodeAddress, TriangleIdDword);
|
|
var shift = ShiftLeftLogical(nodeType, UInt(3));
|
|
uint Barycentric(int sourceShift)
|
|
{
|
|
var source = BitwiseAnd(ShiftRightLogical(triangleId, IAdd(shift, UInt((uint)sourceShift))), UInt(3));
|
|
return SelectF(_module.AddInstruction(SpirvOp.IEqual, _boolType, source, UInt(1)), iNum,
|
|
SelectF(_module.AddInstruction(SpirvOp.IEqual, _boolType, source, UInt(2)), jNum,
|
|
FSub(FSub(tDenom, iNum), jNum)));
|
|
}
|
|
|
|
var hit = LogicalNot(missed);
|
|
return
|
|
[
|
|
Bitcast(_uintType, tNum),
|
|
Bitcast(_uintType, tDenom),
|
|
SelectU(barycentrics, Bitcast(_uintType, Barycentric(TriangleIdISourceShift)), triangleId),
|
|
SelectU(barycentrics, Bitcast(_uintType, Barycentric(TriangleIdJSourceShift)), SelectU(hit, UInt(1), UInt(0))),
|
|
];
|
|
}
|
|
|
|
// IntersectNodeBvh4 with fast_intersect_bbox: slab test clipped to [0, extent], a
|
|
// NaN interval misses, box_grow_value widens the exit time by that many 2^-24
|
|
// steps, and the optional sort orders hit children by entry time.
|
|
private uint[] EmitRayBoxes(uint nodeAddress, bool fp16, uint boxGrow, uint boxSort, uint extent,
|
|
IReadOnlyList<uint> origin, IReadOnlyList<uint> inverseDirection)
|
|
{
|
|
var zero = Float(0f);
|
|
var growFactor = FAdd(Float(1f), FMul(
|
|
_module.AddInstruction(SpirvOp.ConvertUToF, _floatType, boxGrow), Float(5.960464478e-8f)));
|
|
var children = new uint[4];
|
|
var keys = new uint[4];
|
|
for (var child = 0u; child < 4; child++)
|
|
{
|
|
var pointer = LoadNodeDword(nodeAddress, child);
|
|
var enter = Float(float.NegativeInfinity);
|
|
var leave = Float(float.PositiveInfinity);
|
|
for (var axis = 0u; axis < 3; axis++)
|
|
{
|
|
var min = BoxBound(nodeAddress, fp16, child, axis);
|
|
var max = BoxBound(nodeAddress, fp16, child, axis + 3);
|
|
var planeMin = FMul(FSub(min, origin[(int)axis]), inverseDirection[(int)axis]);
|
|
var planeMax = FMul(FSub(max, origin[(int)axis]), inverseDirection[(int)axis]);
|
|
var positive = FCompare(SpirvOp.FOrdGreaterThanEqual, inverseDirection[(int)axis], zero);
|
|
var near = SelectF(positive, planeMin, planeMax);
|
|
var far = SelectF(positive, planeMax, planeMin);
|
|
// HLSL max3/min3: NaN operands propagate, so the NaN check below sees them.
|
|
enter = axis == 0 ? near : NanMax(enter, near);
|
|
leave = axis == 0 ? far : NanMin(leave, far);
|
|
}
|
|
|
|
var nan = _module.AddInstruction(SpirvOp.LogicalOr, _boolType,
|
|
_module.AddInstruction(SpirvOp.IsNan, _boolType, enter),
|
|
_module.AddInstruction(SpirvOp.IsNan, _boolType, leave));
|
|
var minT = SelectF(nan, Float(float.PositiveInfinity), Ext(40, _floatType, enter, zero));
|
|
var maxT = SelectF(nan, Float(float.NegativeInfinity), Ext(37, _floatType, leave, extent));
|
|
var hit = FCompare(SpirvOp.FOrdLessThanEqual, minT, FMul(maxT, growFactor));
|
|
children[child] = SelectU(hit, pointer, UInt(InvalidNode));
|
|
keys[child] = minT;
|
|
}
|
|
|
|
// The hardware sorting network; an invalid child always sinks.
|
|
var sorted = (uint[])children.Clone();
|
|
(int, int)[] network = [(0, 2), (1, 3), (0, 1), (2, 3), (1, 2)];
|
|
foreach (var (a, b) in network)
|
|
{
|
|
var bValid = _module.AddInstruction(SpirvOp.INotEqual, _boolType, sorted[b], UInt(InvalidNode));
|
|
var aInvalid = _module.AddInstruction(SpirvOp.IEqual, _boolType, sorted[a], UInt(InvalidNode));
|
|
var swap = _module.AddInstruction(SpirvOp.LogicalOr, _boolType,
|
|
LogicalAnd(bValid, FCompare(SpirvOp.FOrdLessThan, keys[b], keys[a])), aInvalid);
|
|
(sorted[a], sorted[b]) = (SelectU(swap, sorted[b], sorted[a]), SelectU(swap, sorted[a], sorted[b]));
|
|
(keys[a], keys[b]) = (SelectF(swap, keys[b], keys[a]), SelectF(swap, keys[a], keys[b]));
|
|
}
|
|
|
|
for (var child = 0; child < 4; child++)
|
|
children[child] = SelectU(boxSort, sorted[child], children[child]);
|
|
return children;
|
|
}
|
|
|
|
// Bound 0-2 is the minimum, 3-5 the maximum of one child box.
|
|
private uint BoxBound(uint nodeAddress, bool fp16, uint child, uint bound)
|
|
{
|
|
var index = child * 6 + bound;
|
|
if (!fp16)
|
|
return LoadNodeFloat(nodeAddress, BoxBoundsDword + index);
|
|
|
|
var packed = LoadNodeDword(nodeAddress, BoxBoundsDword + index / 2);
|
|
var bits = index % 2 == 0 ? BitwiseAnd(packed, UInt(0xFFFF)) : ShiftRightLogical(packed, UInt(16));
|
|
return Bitcast(_floatType, EmitHalfToFloat(bits));
|
|
}
|
|
|
|
private uint NanMax(uint left, uint right) => SelectF(
|
|
_module.AddInstruction(SpirvOp.LogicalOr, _boolType,
|
|
_module.AddInstruction(SpirvOp.IsNan, _boolType, left),
|
|
FCompare(SpirvOp.FOrdGreaterThan, left, right)), left, right);
|
|
|
|
private uint NanMin(uint left, uint right) => SelectF(
|
|
_module.AddInstruction(SpirvOp.LogicalOr, _boolType,
|
|
_module.AddInstruction(SpirvOp.IsNan, _boolType, left),
|
|
FCompare(SpirvOp.FOrdLessThan, left, right)), left, right);
|
|
|
|
private uint[] Subtract(IReadOnlyList<uint> left, IReadOnlyList<uint> right) =>
|
|
[FSub(left[0], right[0]), FSub(left[1], right[1]), FSub(left[2], right[2])];
|
|
|
|
private uint[] Cross(IReadOnlyList<uint> a, IReadOnlyList<uint> b) =>
|
|
[
|
|
FSub(FMul(a[1], b[2]), FMul(a[2], b[1])),
|
|
FSub(FMul(a[2], b[0]), FMul(a[0], b[2])),
|
|
FSub(FMul(a[0], b[1]), FMul(a[1], b[0])),
|
|
];
|
|
|
|
private uint Dot(IReadOnlyList<uint> a, IReadOnlyList<uint> b) =>
|
|
FAdd(FAdd(FMul(a[0], b[0]), FMul(a[1], b[1])), FMul(a[2], b[2]));
|
|
|
|
private uint Any(params uint[] conditions)
|
|
{
|
|
var result = conditions[0];
|
|
for (var index = 1; index < conditions.Length; index++)
|
|
result = _module.AddInstruction(SpirvOp.LogicalOr, _boolType, result, conditions[index]);
|
|
return result;
|
|
}
|
|
|
|
private uint FCompare(SpirvOp op, uint left, uint right) => _module.AddInstruction(op, _boolType, left, right);
|
|
private uint FAdd(uint left, uint right) => _module.AddInstruction(SpirvOp.FAdd, _floatType, left, right);
|
|
private uint FSub(uint left, uint right) => _module.AddInstruction(SpirvOp.FSub, _floatType, left, right);
|
|
private uint FMul(uint left, uint right) => _module.AddInstruction(SpirvOp.FMul, _floatType, left, right);
|
|
private uint FDiv(uint left, uint right) => _module.AddInstruction(SpirvOp.FDiv, _floatType, left, right);
|
|
private uint SelectF(uint condition, uint whenTrue, uint whenFalse) =>
|
|
_module.AddInstruction(SpirvOp.Select, _floatType, condition, whenTrue, whenFalse);
|
|
}
|
|
}
|