TestCase FlatStackApertures(u32 wave_size) { TestCase test; test.name = wave_size == 32 ? "FlatStackApertures32" : "FlatStackApertures64"; auto &code = test.code; code = {EncodeSop1(0x04, 8, 237), // private base EncodeSop1(0x0b, 11, InlineU32(1)), // S_BREV_B32 shared aperture, as in GTA. EncodeVop2(0x1a, 20, InlineU32(2), 0), EncodeVop2(0x1b, 22, InlineU32(1), 0), EncodeVop2(0x1a, 22, InlineU32(31), 22)}; AppendVop3(&code, 0x159, 21, 9, 11, Vgpr(22)); // Clamp the stack address to the local apertures. AppendVMovU32(&code, 30, 100); code.push_back(EncodeVop2(0x25, 30, Vgpr(0), 30)); code.insert(code.end(), {EncodeFlat0(0x1c, 1), EncodeFlat1(0, 0x7d, 30, 20)}); AppendVMovU32(&code, 30, 200); code.push_back(EncodeVop2(0x25, 30, Vgpr(0), 30)); code.insert(code.end(), {EncodeDs0(0x0d), EncodeDs1(0, 30, 20)}); AppendVMovU32(&code, 30, 300); code.push_back(EncodeVop2(0x25, 30, Vgpr(0), 30)); code.insert(code.end(), {EncodeFlat0(0x1c, 0), EncodeFlat1(0, 0x7d, 30, 20)}); // FLAT writes must alias both explicit memory instructions, per invocation. code.insert(code.end(), {EncodeFlat0(0x0c, 1), EncodeFlat1(24, 0x7d, 0, 20), EncodeDs0(0x36), EncodeDs1(25, 0, 20), EncodeFlat0(0x0c, 0), EncodeFlat1(26, 0x7d, 0, 20)}); AppendStoreVgprAtLaneDwordOffset(&code, 24, 0, 0); AppendStoreVgprAtLaneDwordOffset(&code, 25, 0, wave_size); AppendStoreVgprAtLaneDwordOffset(&code, 26, 0, 2 * wave_size); // Inactive lanes must preserve both stores and destination VGPRs. AppendSMovLiteral(&code, 126, 0x55555555); if (wave_size == 64) AppendSMovLiteral(&code, 127, 0x55555555); code.push_back(EncodeVop2(0x25, 22, InlineU32(1), 22)); AppendVop3(&code, 0x159, 21, 9, 11, Vgpr(22)); AppendVMovU32(&code, 30, 400); code.push_back(EncodeVop2(0x25, 30, Vgpr(0), 30)); code.insert(code.end(), {EncodeFlat0(0x1c, 0), EncodeFlat1(0, 0x7d, 30, 20)}); code.insert(code.end(), {EncodeFlat0(0x0c, 0), EncodeFlat1(26, 0x7d, 0, 20)}); code.push_back(EncodeSop1(0x04, 126, 193)); // -1 sign-extends across EXEC AppendStoreVgprAtLaneDwordOffset(&code, 26, 0, 3 * wave_size); // Noncanonical addresses outside either aperture, aperture overflow, and // offsets outside allocated local storage must not wrap into valid storage. AppendVMovLiteral(&code, 21, 0x78000000); code.insert(code.end(), {EncodeFlat0(0x1c, 0), EncodeFlat1(0, 0x7d, 30, 20), EncodeFlat0(0x0c, 0), EncodeFlat1(26, 0x7d, 0, 20)}); AppendStoreVgprAtLaneDwordOffset(&code, 26, 0, 4 * wave_size); code.push_back(EncodeVop1(0x01, 21, 11)); AppendVMovLiteral(&code, 20, 0xfffffffc); code.insert(code.end(), {EncodeFlat0(0x1c, 0, 4), EncodeFlat1(0, 0x7d, 30, 20), EncodeFlat0(0x0c, 0, 4), EncodeFlat1(26, 0x7d, 0, 20)}); AppendStoreVgprAtLaneDwordOffset(&code, 26, 0, 5 * wave_size); AppendVMovU32(&code, 20, wave_size * 4); code.insert(code.end(), {EncodeFlat0(0x1c, 0), EncodeFlat1(0, 0x7d, 30, 20), EncodeFlat0(0x0c, 0), EncodeFlat1(26, 0x7d, 0, 20)}); AppendStoreVgprAtLaneDwordOffset(&code, 26, 0, 6 * wave_size); AppendVMovU32(&code, 20, 1); code.insert(code.end(), {EncodeFlat0(0x1c, 0), EncodeFlat1(0, 0x7d, 30, 20), EncodeFlat0(0x0c, 0), EncodeFlat1(26, 0x7d, 0, 20)}); AppendStoreVgprAtLaneDwordOffset(&code, 26, 0, 9 * wave_size); code.push_back(EncodeVop2(0x1a, 20, InlineU32(2), 0)); code.insert(code.end(), {EncodeFlat0(0x0c, 1), EncodeFlat1(24, 0x7d, 0, 20), EncodeDs0(0x36), EncodeDs1(25, 0, 20)}); AppendStoreVgprAtLaneDwordOffset(&code, 24, 0, 7 * wave_size); AppendStoreVgprAtLaneDwordOffset(&code, 25, 0, 8 * wave_size); AppendEnd(&code); test.expected.resize(10 * wave_size); for (u32 lane = 0; lane < wave_size; ++lane) { const bool shared = lane & 1; test.expected[lane] = (shared ? 100 : 300) + lane; test.expected[wave_size + lane] = (shared ? 300 : 200) + lane; test.expected[2 * wave_size + lane] = 300 + lane; test.expected[3 * wave_size + lane] = (shared ? 300 : 400) + lane; test.expected[7 * wave_size + lane] = (shared ? 100 : 400) + lane; test.expected[8 * wave_size + lane] = (shared ? 300 : 200) + lane; } test.compute_info.wave_size = wave_size; test.compute_info.threads_num[0] = wave_size; test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.compute_info.scratch_size_dwords = wave_size; test.compute_info.lds_size_dwords = wave_size; test.has_compute_info = true; test.decoded_counts = {{"S_BREV_B32", 1}}; test.ir_counts = {{" = BitReverse32 ", 0}}; test.forbidden_spirv = {"get_bda_pointer"}; return test; } TestCase BvhIntersections(bool barycentrics, bool sorted, u32 grow = 0, bool wide = false) { TestCase test; test.name = barycentrics ? (sorted ? "BvhBarycentricSorted" : "BvhBarycentricUnsorted") : (sorted ? "BvhTriangleIdSorted" : "BvhTriangleIdUnsorted"); if (grow != 0) test.name = "BvhBoxExitGrowth"; if (wide) test.name = "Bvh64HighNodePointers"; constexpr u32 NodeStart = 1024, OutputStart = 512, LaneCount = 32; constexpr uint64_t GuestBase = 0x10000; const u32 node_base = wide ? 1u << 29 : 0; const auto bits = [](float value) { return std::bit_cast(value); }; constexpr u32 Infinity = 0x7f800000, Invalid = UINT32_MAX, Flag = 0x04040404; test.initial.resize(NodeStart + 258 * 16); auto &data = test.initial; // Pack five float3 vertices and the triangle flag into a 64-byte node. const std::array vertices{-1, -1, 2, 1, -1, 2, 0, 1, 2, 2, 1, 2, 0, 3, 2}; for (u32 i = 0; i < vertices.size(); ++i) data[NodeStart + i] = bits(vertices[i]); data[NodeStart + 15] = Flag; constexpr std::array children{0x88, 0x90, 0x98, 0xa0}; constexpr std::array z{5, 1, 3, 7}; constexpr std::array half_z{0x4500, 0x3c00, 0x4200, 0x4700}; constexpr std::array half_far{0x4600, 0x4000, 0x4400, 0x4800}; for (u32 child = 0; child < 4; ++child) { data[NodeStart + 16 + child] = data[NodeStart + 32 + child] = children[child]; const auto fp16 = NodeStart + 20 + child * 3; data[fp16] = 0xbc00bc00; // min.xy = -1,-1 data[fp16 + 1] = 0x3c000000 | half_z[child]; data[fp16 + 2] = (half_far[child] << 16) | 0x3c00; const auto fp32 = NodeStart + 36 + child * 6; const std::array bounds{-1, -1, z[child], 1, 1, z[child] + 1}; for (u32 i = 0; i < 6; ++i) data[fp32 + i] = bits(bounds[i]); } // A grazing ray enters one ULP after exiting on another axis. for (u32 child = 0; child < 4; ++child) { data[NodeStart + 64 + child] = children[child]; const std::array bounds = child == 0 ? std::array{0, 0, std::nextafter(1.0f, 2.0f), 1, 2, 2} : child == 1 ? std::array{-1, 1, -1, -0.0f, 2, 2} : std::array{1, 1, 1, -1, -1, -1}; for (u32 i = 0; i < 6; ++i) data[NodeStart + 68 + child * 6 + i] = bits(bounds[i]); } // An FP32 node straddles two guest pages with a 64-byte gap in physical backing. std::copy_n(data.begin() + NodeStart + 32, 16, data.begin() + NodeStart + 255 * 16); std::copy_n(data.begin() + NodeStart + 48, 16, data.begin() + NodeStart + 257 * 16); const std::array, 4> origins{{{0, 0}, {1, 0}, {.5f, 1.5f}, {-.25f, 1}}}; std::array, LaneCount> expected{}; for (u32 lane = 0; lane < LaneCount; ++lane) { const auto kind = lane % 8; const auto input = lane * 16; data[input] = kind < 4 ? kind : kind == 4 ? 12 : kind == 5 ? 21 : kind == 6 ? 6 : 0x1000; data[input + 1] = bits(6); data[input + 2] = bits(kind < 4 ? origins[kind][0] : 0); data[input + 3] = bits(kind < 4 ? origins[kind][1] : 0); data[input + 7] = bits(1); data[input + 8] = data[input + 9] = Infinity; data[input + 10] = bits(1); if (kind < 4) { const float denominator = kind == 3 ? -2 : -4; const float i = kind == 0 || kind == 3 ? -1 : -2; const float j = kind == 3 ? -.5f : -1; expected[lane] = {bits(2 * denominator), bits(denominator), barycentrics ? bits(i) : Flag + kind, barycentrics ? bits(j) : 1}; } else if (kind < 6) { expected[lane] = sorted ? std::array{children[1], children[2], children[0], Invalid} : std::array{children[0], children[1], children[2], Invalid}; } else { expected[lane] = kind == 6 ? std::array{Invalid, Invalid, Invalid, Invalid} : std::array{Infinity, bits(1), Invalid, 0}; } if (lane >= 8 && lane < 12) { data[input + 2] = 0x7fc00000; // invalid ray origin expected[lane] = {Infinity, bits(1), Invalid, 0}; } else if (lane == 12 || lane == 13) { data[input + 1] = bits(1); // nearest face equals the exclusive extent expected[lane] = {Invalid, Invalid, Invalid, Invalid}; } else if (lane >= 16 && lane < 20) { const u32 axis = lane & 1; data[input + 2 + axis] = bits(origins[kind][axis] - 4); data[input + 5 + axis] = bits(2); data[input + 8 + axis] = bits(.5f); // Dominant X/Y projection scales all intersection numerators and the // denominator by four; the distance and barycentrics remain unchanged. for (u32 i = 0; i < (barycentrics ? 4u : 2u); ++i) expected[lane][i] = bits(std::bit_cast(expected[lane][i]) * 4); } else if (lane == 22) { data[input] = 37; // node 4, FP32 data[input + 1] = bits(10); for (u32 i = 5; i <= 10; ++i) data[input + i] = bits(1); expected[lane] = {grow ? children[0] : Invalid, Invalid, Invalid, Invalid}; } else if (lane >= 24 && lane < 28) { data[input + 4] = bits(4); data[input + 7] = data[input + 10] = bits(-1); for (u32 i = 0; i < (barycentrics ? 4u : 2u); ++i) expected[lane][i] = bits(-std::bit_cast(expected[lane][i])); } else if (lane == 28) { data[input] = 0; data[input + 3] = bits(-1); // horizontal triangle edge: top-left rule rejects expected[lane] = {Infinity, bits(1), barycentrics ? bits(-2) : Flag, barycentrics ? bits(-2) : 0}; } else if (lane == 29) { data[input] = (255 << 3) | 5; } if (wide) { // Keep the same small backing while placing nodes beyond the 32-bit pointer range. std::copy_backward(data.begin() + input + 1, data.begin() + input + 11, data.begin() + input + 12); data[input + 1] = 1; if (lane == 14 || lane == 15) { data[input] = lane == 14 ? 0 : 5; // Neither an oversized index nor a shifted-address wrap may read node zero. data[input + 1] = lane == 14 ? 0x00001000 : 0x20000000; expected[lane] = lane == 14 ? std::array{Infinity, bits(1), Invalid, 0} : std::array{Invalid, Invalid, Invalid, Invalid}; } } } test.expected = test.initial; for (u32 lane = 0; lane < LaneCount; ++lane) { std::copy(expected[lane].begin(), expected[lane].end(), test.expected.begin() + OutputStart + lane * 4); auto bounded = expected[lane]; if (lane % 8 == 5) bounded = {Invalid, Invalid, Invalid, Invalid}; if (lane >= 20) bounded = {0x12345678, 0x12345679, 0x1234567a, 0x1234567b}; std::copy(bounded.begin(), bounded.end(), test.expected.begin() + OutputStart + LaneCount * 4 + lane * 4); } auto &code = test.code; // The resource is a GPU value, not a materialized host image descriptor. AppendSMovLiteral(&code, 8, GuestBase >> 8); AppendSMovLiteral(&code, 9, (sorted ? 0x80000000 : 0) | (grow << 23)); AppendSMovLiteral(&code, 10, node_base + 256); AppendSMovLiteral(&code, 11, barycentrics ? 0x81000000 : 0x80000000); code.push_back(EncodeVop2(0x1a, 1, InlineU32(6), 0)); for (u32 offset : {0u, 4u, 8u}) { code.insert(code.end(), {EncodeMubuf0(offset == 8 && !wide ? 0x0f : 0x0e, offset * 4), EncodeMubuf1(4 + offset, 0, 1)}); } code.insert(code.end(), {EncodeMimg0(wide ? 0xe7 : 0xe6, 15, 0, false, 0, true), EncodeMimg1(20, 4, 2)}); code.push_back(EncodeVop2(0x1a, 2, InlineU32(2), 0)); for (u32 i = 0; i < 4; ++i) AppendStoreVgprAtLaneDwordOffset(&code, 20 + i, 2, OutputStart + i); // NSA addressing, the FP32 second-block bound, and inactive destination lanes. AppendSMovLiteral(&code, 10, node_base + 2); for (u32 i = 0; i < 4; ++i) AppendVMovLiteral(&code, 24 + i, 0x12345678 + i); AppendSMovLiteral(&code, 126, 0x000fffff); code.insert(code.end(), {EncodeMimg0(wide ? 0xe7 : 0xe6, 15, 3, false, 0, true), EncodeMimg1(24, 4, 2), 0x08070605, 0x0c0b0a09, wide ? 0x000f0e0du : 0x00000e0du}); code.push_back(EncodeSop1(0x04, 126, 193)); for (u32 i = 0; i < 4; ++i) AppendStoreVgprAtLaneDwordOffset(&code, 24 + i, 2, OutputStart + LaneCount * 4 + i); AppendEnd(&code); const auto nodes_address = GuestBase + (uint64_t{node_base} << 6); test.bda_mappings = {{nodes_address, NodeStart * sizeof(u32)}, {nodes_address + 0x4000, (NodeStart + 257 * 16) * sizeof(u32)}}; test.compute_info.wave_size = LaneCount; test.compute_info.threads_num[0] = LaneCount; test.compute_info.threads_num[1] = test.compute_info.threads_num[2] = 1; test.compute_info.thread_ids_num = 1; test.has_compute_info = true; test.required_spirv = {"get_bda_pointer"}; return test; }