aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDuck912 <350528965+Duck912@users.noreply.github.com>2026-11-01 21:42:19 -0500
committerDuck912 <350528965+Duck912@users.noreply.github.com>2026-11-01 21:54:48 -0500
commit808feaf3bf34b3c108ff30baae1d2261f216f4f3 (patch)
tree41d9d0eb9e746098797d0c5a55fd1841c333b681 /src
parent9acc324228b7028f947e053a08c18543f631e5ce (diff)
video_core: keep root tables and bind table 0 as constant bank 17
The 3D methods 0x504/0x508 write eight 256-byte root tables, which shaders read as constant banks 24-31; NVN2 keeps push constants in table 0 (bank 17 after the Ampere lowering). Fire Emblem: Fortune's Weave keeps its material constants there and never binds bank 17, and the blit constants pass replaced those reads with render-target sizes, flooding its 3D scenes red. Maxwell3D now keeps the tables, the buffer cache streams table 0 as bank 17 when the game has not bound one, and the blit constants pass leaves shaders that read bank 17 outside its parameter block alone. Assisted-by: Claude Opus 5.5 (debugging, implementation and drafting) Signed-off-by: Duck912 <350528965+Duck912@users.noreply.github.com>
Diffstat (limited to 'src')
-rw-r--r--src/shader_recompiler/ir_opt/position_pass.cpp21
-rw-r--r--src/video_core/buffer_cache/buffer_cache.h20
-rw-r--r--src/video_core/engines/maxwell_3d.cpp15
-rw-r--r--src/video_core/engines/maxwell_3d.h12
4 files changed, 68 insertions, 0 deletions
diff --git a/src/shader_recompiler/ir_opt/position_pass.cpp b/src/shader_recompiler/ir_opt/position_pass.cpp
index 4890346944..23b1200cfc 100644
--- a/src/shader_recompiler/ir_opt/position_pass.cpp
+++ b/src/shader_recompiler/ir_opt/position_pass.cpp
@@ -83,6 +83,27 @@ void BlitConstantsPass(Environment& env, IR::Program& program) {
if (env.ShaderStage() != Stage::VertexB && env.ShaderStage() != Stage::Fragment) {
return;
}
+ // Bank 24 is root table 0, which games may load themselves (3D methods 0x504/0x508; the buffer
+ // cache binds it as bank 17): Fire Emblem: Fortune's Weave keeps its material push constants
+ // there. A shader reading bank 17 outside the blit block reads such a table and keeps its reads.
+ const auto in_blit_block = [](u32 offset) {
+ return offset < 0x20 || (offset >= 0x30 && offset < 0x4c) || (offset >= 0x50 && offset < 0x60);
+ };
+ for (IR::Block* const block : program.post_order_blocks) {
+ for (IR::Inst& inst : block->Instructions()) {
+ const IR::Opcode opcode{inst.GetOpcode()};
+ if ((opcode != IR::Opcode::GetCbufU32 && opcode != IR::Opcode::GetCbufF32 &&
+ opcode != IR::Opcode::GetCbufU32x2) ||
+ !inst.Arg(0).IsImmediate() || inst.Arg(0).U32() != 17 || !inst.Arg(1).IsImmediate()) {
+ continue;
+ }
+ const u32 offset{inst.Arg(1).U32()};
+ const bool pair{opcode == IR::Opcode::GetCbufU32x2};
+ if (!in_blit_block(offset) || (pair && !in_blit_block(offset + 4))) {
+ return;
+ }
+ }
+ }
bool used{};
for (IR::Block* const block : program.post_order_blocks) {
for (IR::Inst& inst : block->Instructions()) {
diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h
index e90af3214d..2fc74cb6c8 100644
--- a/src/video_core/buffer_cache/buffer_cache.h
+++ b/src/video_core/buffer_cache/buffer_cache.h
@@ -994,6 +994,26 @@ void BufferCache<P>::BindHostGraphicsUniformBuffers(size_t stage) {
template <class P>
void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32 binding_index, bool needs_bind) {
++channel_state->uniform_cache_shots[0];
+ // Bank 24 (root table 0, NVN2's push constants) is read from bank 17, which such games do not
+ // bind: stream the table's current words as that buffer.
+ if (index == 17 && maxwell3d->root_tables.loaded && channel_state->uniform_buffers[stage][index].size == 0) {
+ const auto& words = maxwell3d->root_tables.tables[0];
+ const u32 size = (*channel_state->uniform_buffer_sizes)[stage][index];
+ const u32 copied = (std::min)(size, static_cast<u32>(sizeof(words)));
+ if constexpr (IS_OPENGL) {
+ std::array<u8, sizeof(words)> data{};
+ std::memcpy(data.data(), words.data(), copied);
+ runtime.BindFastUniformBuffer(stage, binding_index, copied);
+ runtime.PushFastUniformBuffer(stage, binding_index, std::span<const u8>(data.data(), copied));
+ } else {
+ const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size);
+ std::memset(span.data(), 0, span.size());
+ std::memcpy(span.data(), words.data(), copied);
+ }
+ channel_state->fast_bound_uniform_buffers[stage] |= 1u << binding_index;
+ channel_state->uniform_buffer_binding_sizes[stage][binding_index] = size;
+ return;
+ }
const Binding& binding = channel_state->uniform_buffers[stage][index];
const DAddr device_addr = binding.device_addr;
const u32 size = (std::min)(binding.size, (*channel_state->uniform_buffer_sizes)[stage][index]);
diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp
index 6adfcf89b8..4001284127 100644
--- a/src/video_core/engines/maxwell_3d.cpp
+++ b/src/video_core/engines/maxwell_3d.cpp
@@ -164,6 +164,8 @@ bool Maxwell3D::IsMethodExecutable(u32 method) {
case MAXWELL3D_REG_INDEX(vertex_array_instance_first):
case MAXWELL3D_REG_INDEX(vertex_array_instance_subsequent):
case MAXWELL3D_REG_INDEX(draw_texture.src_y0):
+ case RootTables::SelectMethod:
+ case RootTables::LoadMethod:
case MAXWELL3D_REG_INDEX(wait_for_idle):
case MAXWELL3D_REG_INDEX(shadow_ram_control):
case MAXWELL3D_REG_INDEX(load_mme.instruction_ptr):
@@ -346,6 +348,17 @@ void Maxwell3D::ProcessMethodCall(u32 method, u32 argument, u32 nonshadow_argume
LOG_DEBUG(HW_GPU, "Native GPU surface clear: value={:#x}", argument);
}
switch (method) {
+ case RootTables::SelectMethod:
+ root_tables.selected = argument & 7;
+ root_tables.offset = (argument >> 8) & 0xff;
+ return;
+ case RootTables::LoadMethod:
+ if (root_tables.offset < 256 && root_tables.offset % 4 == 0) {
+ root_tables.tables[root_tables.selected][root_tables.offset / 4] = argument;
+ root_tables.loaded = true;
+ }
+ root_tables.offset += 4;
+ return;
case MAXWELL3D_REG_INDEX(wait_for_idle):
return rasterizer->WaitForIdle();
case MAXWELL3D_REG_INDEX(shadow_ram_control):
@@ -819,6 +832,8 @@ bool FrontHandledOutsideDrawManager(u32 method) {
return true;
}
switch (method) {
+ case Maxwell3D::RootTables::SelectMethod:
+ case Maxwell3D::RootTables::LoadMethod:
case MAXWELL3D_REG_INDEX(wait_for_idle):
case MAXWELL3D_REG_INDEX(shadow_ram_control):
case MAXWELL3D_REG_INDEX(load_mme.instruction_ptr):
diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h
index 0121f1a01c..a32b31fd1e 100644
--- a/src/video_core/engines/maxwell_3d.h
+++ b/src/video_core/engines/maxwell_3d.h
@@ -3160,6 +3160,18 @@ public:
std::array<u32, 0xc00>& GetNativeDataRam() { return native_data_ram; }
std::span<u32> GetNativeMacroPositions() { return native_macro_context ? std::span<u32>{native_macro_context->positions} : std::span<u32>{macro_positions}; }
+ /// Root tables: eight 256-byte tables written inline (method 0x504 selects a table, bits 0-2,
+ /// and a byte offset, bits 8-15; each 0x508 writes the next word) that shaders read as constant
+ /// banks 24-31. NVN2 keeps push constants in bank 24, which the Ampere lowering reads as bank 17.
+ struct RootTables {
+ static constexpr u32 SelectMethod = 0x504 / 4;
+ static constexpr u32 LoadMethod = 0x508 / 4;
+ std::array<std::array<u32, 64>, 8> tables{};
+ u32 selected{};
+ u32 offset{};
+ bool loaded{};
+ } root_tables;
+
/// Write the value to the register identified by method.
void CallMethod(Core::System& system, u32 method, u32 method_argument, bool is_last_call) override;