VideoCommon: optionally apply materials to EFBs

After an EFB operation is turned into a texture, if there is a graphics mod action that provides a custom material, update the EFB texture with the material
This commit is contained in:
iwubcode 2025-11-17 18:40:21 -06:00
parent 7bfd43eb1a
commit 1f72403ec7
2 changed files with 127 additions and 0 deletions

View File

@ -2329,6 +2329,26 @@ void TextureCacheBase::CopyRenderTargetToTexture(
isIntensity, gamma, clamp_top, clamp_bottom,
GetVRAMCopyFilterCoefficients(filter_coefficients));
if (g_ActiveConfig.bGraphicMods)
{
FBInfo info;
info.m_width = tex_w;
info.m_height = tex_h;
info.m_texture_format = baseFormat;
if (!is_xfb_copy)
{
GraphicsModActionData::PostEFB efb;
for (const auto& action : g_graphics_mod_manager->GetEFBActions(info))
{
action->AfterEFB(&efb);
if (efb.material)
{
ApplyMaterialToCacheEntry(*efb.material, entry.get());
}
}
}
}
if (is_xfb_copy && (g_ActiveConfig.bDumpXFBTarget || g_ActiveConfig.bGraphicMods))
{
const std::string id = fmt::format("{}x{}", tex_w, tex_h);
@ -3035,6 +3055,110 @@ bool TextureCacheBase::DecodeTextureOnGPU(RcTcacheEntry& entry, u32 dst_level, c
return true;
}
void TextureCacheBase::ApplyMaterialToCacheEntry(const VideoCommon::MaterialResource& material,
TCacheEntry* entry)
{
const auto material_data = material.GetData();
if (!material_data) [[unlikely]]
return;
// Make a copy, we can't write to our texture and use its framebuffer
// at the same time
auto new_entry = AllocateCacheEntry(entry->texture->GetConfig());
new_entry->SetGeneralParameters(entry->addr, entry->size_in_bytes, entry->format,
entry->should_force_safe_hashing);
new_entry->SetDimensions(entry->native_width, entry->native_height, 1);
new_entry->SetEfbCopy(entry->memory_stride);
new_entry->may_have_overlapping_textures = false;
new_entry->frameCount = FRAMECOUNT_INVALID;
g_gfx->BeginUtilityDrawing();
entry->texture->FinishedRendering();
const auto custom_uniforms = material_data->GetUniforms();
// Set up uniforms.
// TODO: this struct should be shared with post processing
struct Uniforms
{
std::array<float, 4> source_resolution;
std::array<float, 4> target_resolution;
std::array<float, 4> window_resolution;
std::array<float, 4> source_rectangle;
s32 source_layer;
s32 source_layer_pad[3];
u32 time;
u32 time_pad[3];
s32 graphics_api;
s32 graphics_api_pad[3];
u32 efb_scale;
u32 efb_scale_pad[3];
} uniforms;
const float rcp_src_width = 1.0f / entry->texture->GetWidth();
const float rcp_src_height = 1.0f / entry->texture->GetHeight();
uniforms.source_resolution = {static_cast<float>(entry->texture->GetWidth()),
static_cast<float>(entry->texture->GetHeight()), rcp_src_width,
rcp_src_height};
// The target resolution is the same here, since we're
// injecting into the texture
uniforms.target_resolution = uniforms.source_resolution;
const auto present_rect = g_presenter->GetTargetRectangle();
uniforms.window_resolution = {static_cast<float>(present_rect.GetWidth()),
static_cast<float>(present_rect.GetHeight()),
1.0f / static_cast<float>(present_rect.GetWidth()),
1.0f / static_cast<float>(present_rect.GetHeight())};
uniforms.source_rectangle = {0, 0, 1, 1};
uniforms.source_layer = 0;
uniforms.time = 0;
uniforms.graphics_api = static_cast<s32>(g_backend_info.api_type);
uniforms.efb_scale = g_framebuffer_manager->GetEFBScale();
Common::UniqueBuffer<u8> uniform_buffer(custom_uniforms.size() + sizeof(uniforms));
std::memcpy(uniform_buffer.data(), &uniforms, sizeof(uniforms));
std::memcpy(uniform_buffer.data() + sizeof(uniforms), custom_uniforms.data(),
custom_uniforms.size());
g_vertex_manager->UploadUtilityUniforms(uniform_buffer.data(),
static_cast<u32>(uniform_buffer.size()));
// Set framebuffer and viewport based on new entry
g_gfx->SetAndDiscardFramebuffer(new_entry->framebuffer.get());
g_gfx->SetViewportAndScissor(new_entry->framebuffer->GetRect());
g_gfx->SetPipeline(material_data->GetPipeline());
g_gfx->SetTexture(0, entry->texture.get());
g_gfx->SetSamplerState(0, RenderState::GetPointSamplerState());
for (const auto texture : material_data->GetTextures())
{
g_gfx->SetTexture(texture.sampler_index, texture.texture);
g_gfx->SetSamplerState(texture.sampler_index, texture.sampler);
}
g_gfx->Draw(0, 3);
g_gfx->EndUtilityDrawing();
// Finish rendering new entry
new_entry->texture->FinishedRendering();
// Swap new entry and existing entry
std::swap(entry->texture, new_entry->texture);
std::swap(entry->framebuffer, new_entry->framebuffer);
// Return old entry to pool for use in another pass
// or future functionality
ReleaseToPool(new_entry.get());
if (auto* const next_material = material_data->GetNextMaterial(); next_material)
{
ApplyMaterialToCacheEntry(*next_material, entry);
}
}
u32 TCacheEntry::BytesPerRow() const
{
// RGBA takes two cache lines per block; all others take one

View File

@ -41,6 +41,7 @@ namespace VideoCommon
{
class CustomTextureData;
class GameTextureAsset;
class MaterialResource;
} // namespace VideoCommon
constexpr std::string_view EFB_DUMP_PREFIX = "efb1";
@ -407,6 +408,8 @@ private:
void DoSaveState(PointerWrap& p);
void DoLoadState(PointerWrap& p);
void ApplyMaterialToCacheEntry(const VideoCommon::MaterialResource& material, TCacheEntry* entry);
// m_textures_by_address is the authoritive version of what's actually "in" the texture cache
// but it's possible for invalidated TCache entries to live on elsewhere
TexAddrCache m_textures_by_address;