diff --git a/src/xenia/gpu/dxbc_shader_translator_fetch.cc b/src/xenia/gpu/dxbc_shader_translator_fetch.cc index 32cb178f5..e4babadd6 100644 --- a/src/xenia/gpu/dxbc_shader_translator_fetch.cc +++ b/src/xenia/gpu/dxbc_shader_translator_fetch.cc @@ -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. diff --git a/src/xenia/gpu/spirv_shader_translator.cc b/src/xenia/gpu/spirv_shader_translator.cc index 0de5d9a98..03bd03557 100644 --- a/src/xenia/gpu/spirv_shader_translator.cc +++ b/src/xenia/gpu/spirv_shader_translator.cc @@ -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_); diff --git a/src/xenia/gpu/spirv_shader_translator.h b/src/xenia/gpu/spirv_shader_translator.h index c189020f3..1c504448a 100644 --- a/src/xenia/gpu/spirv_shader_translator.h +++ b/src/xenia/gpu/spirv_shader_translator.h @@ -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. diff --git a/src/xenia/gpu/spirv_shader_translator_fetch.cc b/src/xenia/gpu/spirv_shader_translator_fetch.cc index b75c58e3f..ec99debeb 100644 --- a/src/xenia/gpu/spirv_shader_translator_fetch.cc +++ b/src/xenia/gpu/spirv_shader_translator_fetch.cc @@ -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,