[GPU] Clamp out-of-bounds vertex fetch words to 0

Words at or past the size in the fetch constant read as 0 now instead of whatever sits in shared memory, matching real hardware.

Co-authored-by: Herman S. <429230+has207@users.noreply.github.com>
This commit is contained in:
goldislead
2026-08-07 18:29:00 -07:00
committed by Radosław Gliński
parent e6bdb0fdf7
commit 9e9d3cdd3f
4 changed files with 89 additions and 27 deletions

View File

@@ -147,14 +147,41 @@ void DxbcShaderTranslator::ProcessVertexFetchInstruction(
address_src = address_temp_src;
}
// Words at or past the end of the fetch buffer must read as 0. The shared
// memory binding covers all of physical memory, so a word out of bounds
// would load unrelated guest data where the hardware clamps and returns
// zeros. Games rely on that. An overallocated draw expects the vertices it
// never wrote to collapse into degenerate primitives. Compute the exclusive
// end of the buffer in bytes from the fetch constant and a mask of which
// words of the element fall inside it.
uint32_t bounds_temp = PushSystemTemp(0, 2);
uint32_t word_mask_temp = bounds_temp + 1;
// bounds_temp.x = buffer size in words (bits 2:25 of the second fetch
// constant word).
a_.OpUBFE(dxbc::Dest::R(bounds_temp, 0b0001), dxbc::Src::LU(24),
dxbc::Src::LU(2), fetch_constant_src.SelectFromSwizzled(1));
// bounds_temp.y = base address of the buffer in bytes.
a_.OpAnd(dxbc::Dest::R(bounds_temp, 0b0010),
fetch_constant_src.SelectFromSwizzled(0),
dxbc::Src::LU(~uint32_t(3)));
// bounds_temp.x = exclusive end of the buffer in bytes.
a_.OpUMAd(dxbc::Dest::R(bounds_temp, 0b0001),
dxbc::Src::R(bounds_temp, dxbc::Src::kXXXX), dxbc::Src::LU(4),
dxbc::Src::R(bounds_temp, dxbc::Src::kYYYY));
// word_mask_temp = byte addresses of the words of the element.
a_.OpIAdd(dxbc::Dest::R(word_mask_temp), address_src,
dxbc::Src::LI((0 - int32_t(first_word_index)) * 4,
(1 - int32_t(first_word_index)) * 4,
(2 - int32_t(first_word_index)) * 4,
(3 - int32_t(first_word_index)) * 4));
// word_mask_temp = whether each word is within the buffer bounds.
a_.OpULT(dxbc::Dest::R(word_mask_temp, needed_words),
dxbc::Src::R(word_mask_temp),
dxbc::Src::R(bounds_temp, dxbc::Src::kXXXX));
// - Load needed words to system_temp_result_, words 0, 1, 2, 3 to X, Y, Z, W
// respectively.
// FIXME(Triang3l): Bound checking is not done here, but haven't encountered
// any games relying on out-of-bounds access. On Adreno 200 on Android (LG
// P705), however, words (not full elements) out of glBufferData bounds
// contain 0.
// Loading the FXC way, Load4.xyw becomes Load2 and Load - would be a
// compromise between AMD, where there are load_dwordx2/3/4, and Nvidia, where
// a ByteAddressBuffer is more like an R32_UINT buffer.
@@ -224,6 +251,10 @@ void DxbcShaderTranslator::ProcessVertexFetchInstruction(
}
a_.OpEndIf();
a_.OpAnd(dxbc::Dest::R(system_temp_result_, needed_words),
dxbc::Src::R(system_temp_result_), dxbc::Src::R(word_mask_temp));
PopSystemTemp(2);
dxbc::Src result_src(dxbc::Src::R(system_temp_result_));
// - Endian swap the words.

View File

@@ -543,6 +543,9 @@ void SpirvShaderTranslator::StartTranslation() {
var_main_vfetch_address_ = builder_->createVariable(
spv::NoPrecision, spv::StorageClassFunction, type_int_,
"xe_var_vfetch_address", const_int_0_);
var_main_vfetch_bound_ = builder_->createVariable(
spv::NoPrecision, spv::StorageClassFunction, type_int_,
"xe_var_vfetch_bound", const_int_0_);
var_main_tfetch_lod_ = builder_->createVariable(
spv::NoPrecision, spv::StorageClassFunction, type_float_,
"xe_var_tfetch_lod", const_float_0_);

View File

@@ -1118,6 +1118,9 @@ class SpirvShaderTranslator : public ShaderTranslator {
// `base + index * stride` in dwords from the last vfetch_full as it may be
// needed by vfetch_mini - int.
spv::Id var_main_vfetch_address_;
// Exclusive end (base + size) in dwords of the last vfetch_full's buffer, for
// clamping out-of-bounds words to 0 in both it and its vfetch_mini - int.
spv::Id var_main_vfetch_bound_;
// float.
spv::Id var_main_tfetch_lod_;
// float3.

View File

@@ -43,11 +43,33 @@ void SpirvShaderTranslator::ProcessVertexFetchInstruction(
EnsureBuildPointAvailable();
uint32_t fetch_constant_word_0_index = instr.operands[1].storage_index << 1;
uint32_t fetch_constant_word_1_index = fetch_constant_word_0_index + 1;
// Load the second fetch constant word up front. It holds the endianness
// (bits 0:1) for the swap below and the buffer size in words (bits 2:25)
// used for bound checking here.
id_vector_temp_.clear();
// The only element of the fetch constant buffer.
id_vector_temp_.push_back(const_int_0_);
// Vector index.
id_vector_temp_.push_back(
builder_->makeIntConstant(int(fetch_constant_word_1_index >> 2)));
// Component index.
id_vector_temp_.push_back(
builder_->makeIntConstant(int(fetch_constant_word_1_index & 3)));
spv::Id fetch_constant_word_1 = builder_->createLoad(
builder_->createAccessChain(spv::StorageClassUniform,
uniform_fetch_constants_, id_vector_temp_),
spv::NoPrecision);
spv::Id address;
// Exclusive end of the fetch buffer in dwords (base + size). Words at or past
// it read as 0, like the hardware clamping out-of-bounds lanes.
spv::Id fetch_end;
if (instr.is_mini_fetch) {
// `base + index * stride` loaded by vfetch_full.
// `base + index * stride` and the end bound loaded by vfetch_full.
address = builder_->createLoad(var_main_vfetch_address_, spv::NoPrecision);
fetch_end = builder_->createLoad(var_main_vfetch_bound_, spv::NoPrecision);
} else {
// Get the base address in dwords from the bits 2:31 of the first fetch
// constant word.
@@ -73,6 +95,20 @@ void SpirvShaderTranslator::ProcessVertexFetchInstruction(
builder_->createBinOp(spv::OpShiftRightLogical, type_uint_,
fetch_constant_word_0,
builder_->makeUintConstant(2)));
// address is the base now. The exclusive end is base + size (size in words
// in bits 2:25 of the second word). Store it for the subsequent
// vfetch_mini, which reuses this fetch constant.
fetch_end = builder_->createBinOp(
spv::OpIAdd, type_int_, address,
builder_->createUnaryOp(
spv::OpBitcast, type_int_,
builder_->createBinOp(
spv::OpBitwiseAnd, type_uint_,
builder_->createBinOp(spv::OpShiftRightLogical, type_uint_,
fetch_constant_word_1,
builder_->makeUintConstant(2)),
builder_->makeUintConstant((uint32_t(1) << 24) - 1))));
builder_->createStore(fetch_end, var_main_vfetch_bound_);
if (instr.attributes.stride) {
// Convert the index to an integer by flooring or by rounding to the
// nearest (as floor(index + 0.5) because rounding to the nearest even
@@ -129,12 +165,15 @@ void SpirvShaderTranslator::ProcessVertexFetchInstruction(
builder_->makeIntConstant(int(word_offset)));
}
word_composite_indices[word_index] = word_count;
// FIXME(Triang3l): Bound checking is not done here, but haven't encountered
// any games relying on out-of-bounds access. On Adreno 200 on Android (LG
// P705), however, words (not full elements) out of glBufferData bounds
// contain 0.
word_composite_constituents[word_count++] =
LoadUint32FromSharedMemory(word_address);
// Words at or past the end of the fetch buffer read as 0, matching the
// hardware's bounds clamping. Games rely on this. An overallocated draw
// expects the vertices it never wrote to collapse into degenerate
// primitives.
spv::Id loaded_word = LoadUint32FromSharedMemory(word_address);
spv::Id word_in_bounds = builder_->createBinOp(spv::OpULessThan, type_bool_,
word_address, fetch_end);
word_composite_constituents[word_count++] = builder_->createTriOp(
spv::OpSelect, type_uint_, word_in_bounds, loaded_word, const_uint_0_);
}
spv::Id words;
if (word_count > 1) {
@@ -151,21 +190,7 @@ void SpirvShaderTranslator::ProcessVertexFetchInstruction(
}
// Endian swap the words, getting the endianness from bits 0:1 of the second
// fetch constant word.
uint32_t fetch_constant_word_1_index = fetch_constant_word_0_index + 1;
id_vector_temp_.clear();
// The only element of the fetch constant buffer.
id_vector_temp_.push_back(const_int_0_);
// Vector index.
id_vector_temp_.push_back(
builder_->makeIntConstant(int(fetch_constant_word_1_index >> 2)));
// Component index.
id_vector_temp_.push_back(
builder_->makeIntConstant(int(fetch_constant_word_1_index & 3)));
spv::Id fetch_constant_word_1 = builder_->createLoad(
builder_->createAccessChain(spv::StorageClassUniform,
uniform_fetch_constants_, id_vector_temp_),
spv::NoPrecision);
// fetch constant word (loaded above).
words = EndianSwap32Uint(
words, builder_->createBinOp(spv::OpBitwiseAnd, type_uint_,
fetch_constant_word_1,