Add optional texture hashing

This commit is contained in:
wheremyfoodat 2025-06-23 02:45:13 +03:00
parent 6182d4cfe9
commit a0aa11dac0
10 changed files with 106 additions and 54 deletions

View file

@ -62,9 +62,13 @@ struct EmulatorConfig {
static constexpr RendererType rendererDefault = RendererType::OpenGL;
#endif
static constexpr bool hashTexturesDefault = true;
bool shaderJitEnabled = shaderJitDefault;
bool useUbershaders = ubershaderDefault;
bool accelerateShaders = accelerateShadersDefault;
bool hashTextures = hashTexturesDefault;
bool accurateShaderMul = false;
bool discordRpcEnabled = false;

View file

@ -51,6 +51,9 @@ class Renderer {
u32 outputWindowWidth = 400;
u32 outputWindowHeight = 240 * 2;
// Should hw renderers hash textures? Stored separately from emulatorConfig because we'll be accessing it constantly, might be merged eventually
bool hashTextures = false;
EmulatorConfig* emulatorConfig = nullptr;
void doSoftwareTextureCopy(u32 inputAddr, u32 outputAddr, u32 copySize, u32 inputWidth, u32 inputGap, u32 outputWidth, u32 outputGap);
@ -123,4 +126,5 @@ class Renderer {
}
void setConfig(EmulatorConfig* config) { emulatorConfig = config; }
void setHashTextures(bool setting) { hashTextures = setting; }
};

View file

@ -1,6 +1,8 @@
#pragma once
#include <array>
#include <string>
#include "PICA/pica_hash.hpp"
#include "PICA/regs.hpp"
#include "boost/icl/interval.hpp"
#include "helpers.hpp"
@ -11,55 +13,55 @@ template <typename T>
using Interval = boost::icl::right_open_interval<T>;
struct Texture {
u32 location;
u32 config; // Magnification/minification filter, wrapping configs, etc
PICA::TextureFmt format;
OpenGL::uvec2 size;
bool valid;
using Hash = PICAHash::HashType;
// Range of VRAM taken up by buffer
Interval<u32> range;
// OpenGL resources allocated to buffer
OpenGL::Texture texture;
u32 location;
u32 config; // Magnification/minification filter, wrapping configs, etc
Hash hash = Hash(0);
Texture() : valid(false) {}
PICA::TextureFmt format;
OpenGL::uvec2 size;
bool valid;
Texture(u32 loc, PICA::TextureFmt format, u32 x, u32 y, u32 config, bool valid = true)
: location(loc), format(format), size({x, y}), config(config), valid(valid) {
// Range of VRAM taken up by buffer
Interval<u32> range;
// OpenGL resources allocated to buffer
OpenGL::Texture texture;
u64 endLoc = (u64)loc + sizeInBytes();
// Check if start and end are valid here
range = Interval<u32>(loc, (u32)endLoc);
}
Texture() : valid(false) {}
// For 2 textures to "match" we only care about their locations, formats, and dimensions to match
// For other things, such as filtering mode, etc, we can just switch the attributes of the cached texture
bool matches(Texture& other) {
return location == other.location && format == other.format &&
size.x() == other.size.x() && size.y() == other.size.y();
}
Texture(u32 loc, PICA::TextureFmt format, u32 x, u32 y, u32 config, bool valid = true)
: location(loc), format(format), size({x, y}), config(config), valid(valid) {
u64 endLoc = (u64)loc + sizeInBytes();
// Check if start and end are valid here
range = Interval<u32>(loc, (u32)endLoc);
}
void allocate();
void setNewConfig(u32 newConfig);
void decodeTexture(std::span<const u8> data);
void free();
u64 sizeInBytes();
// For 2 textures to "match" we only care about their locations, formats, and dimensions to match
// For other things, such as filtering mode, etc, we can just switch the attributes of the cached texture
bool matches(Texture& other) {
return location == other.location && hash == other.hash && format == other.format && size.x() == other.size.x() && size.y() == other.size.y();
}
u32 decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, std::span<const u8> data);
void allocate();
void setNewConfig(u32 newConfig);
void decodeTexture(std::span<const u8> data);
void free();
u64 sizeInBytes();
// Get the morton interleave offset of a texel based on its U and V values
static u32 mortonInterleave(u32 u, u32 v);
// Get the byte offset of texel (u, v) in the texture
static u32 getSwizzledOffset(u32 u, u32 v, u32 width, u32 bytesPerPixel);
static u32 getSwizzledOffset_4bpp(u32 u, u32 v, u32 width);
u32 decodeTexel(u32 u, u32 v, PICA::TextureFmt fmt, std::span<const u8> data);
// Returns the format of this texture as a string
std::string_view formatToString() {
return PICA::textureFormatToString(format);
}
// Get the morton interleave offset of a texel based on its U and V values
static u32 mortonInterleave(u32 u, u32 v);
// Get the byte offset of texel (u, v) in the texture
static u32 getSwizzledOffset(u32 u, u32 v, u32 width, u32 bytesPerPixel);
static u32 getSwizzledOffset_4bpp(u32 u, u32 v, u32 width);
// Returns the texel at coordinates (u, v) of an ETC1(A4) texture
// TODO: Make hasAlpha a template parameter
u32 getTexelETC(bool hasAlpha, u32 u, u32 v, u32 width, std::span<const u8> data);
u32 decodeETC(u32 alpha, u32 u, u32 v, u64 colourData);
// Returns the format of this texture as a string
std::string_view formatToString() { return PICA::textureFormatToString(format); }
// Returns the texel at coordinates (u, v) of an ETC1(A4) texture
// TODO: Make hasAlpha a template parameter
u32 getTexelETC(bool hasAlpha, u32 u, u32 v, u32 width, std::span<const u8> data);
u32 decodeETC(u32 alpha, u32 u, u32 v, u64 colourData);
};

View file

@ -4,6 +4,7 @@
#include <array>
#include <string>
#include "PICA/pica_hash.hpp"
#include "PICA/regs.hpp"
#include "boost/icl/interval.hpp"
#include "helpers.hpp"
@ -17,10 +18,14 @@ using Interval = boost::icl::right_open_interval<T>;
namespace Metal {
struct Texture {
using Hash = PICAHash::HashType;
MTL::Device* device;
u32 location;
u32 config; // Magnification/minification filter, wrapping configs, etc
Hash hash = Hash(0);
PICA::TextureFmt format;
OpenGL::uvec2 size;
bool valid;
@ -45,7 +50,8 @@ namespace Metal {
// For 2 textures to "match" we only care about their locations, formats, and dimensions to match
// For other things, such as filtering mode, etc, we can just switch the attributes of the cached texture
bool matches(Texture& other) {
return location == other.location && format == other.format && size.x() == other.size.x() && size.y() == other.size.y();
return location == other.location && hash == other.hash && format == other.format && size.x() == other.size.x() &&
size.y() == other.size.y();
}
void allocate();

View file

@ -90,6 +90,7 @@ void EmulatorConfig::load() {
forceShadergenForLights = toml::find_or<toml::boolean>(gpu, "ForceShadergenForLighting", true);
lightShadergenThreshold = toml::find_or<toml::integer>(gpu, "ShadergenLightThreshold", 1);
hashTextures = toml::find_or<toml::boolean>(gpu, "HashTextures", hashTexturesDefault);
enableRenderdoc = toml::find_or<toml::boolean>(gpu, "EnableRenderdoc", false);
}
}
@ -101,7 +102,7 @@ void EmulatorConfig::load() {
auto dspCoreName = toml::find_or<std::string>(audio, "DSPEmulation", "HLE");
dspType = Audio::DSPCore::typeFromString(dspCoreName);
audioEnabled = toml::find_or<toml::boolean>(audio, "EnableAudio", audioEnabledDefault);
aacEnabled = toml::find_or<toml::boolean>(audio, "EnableAACAudio", true);
printDSPFirmware = toml::find_or<toml::boolean>(audio, "PrintDSPFirmware", false);
@ -180,7 +181,7 @@ void EmulatorConfig::save() {
data["Window"]["WindowPosY"] = windowSettings.y;
data["Window"]["WindowWidth"] = windowSettings.width;
data["Window"]["WindowHeight"] = windowSettings.height;
data["GPU"]["EnableShaderJIT"] = shaderJitEnabled;
data["GPU"]["Renderer"] = std::string(Renderer::typeToString(rendererType));
data["GPU"]["EnableVSync"] = vsyncEnabled;
@ -190,6 +191,7 @@ void EmulatorConfig::save() {
data["GPU"]["ShadergenLightThreshold"] = lightShadergenThreshold;
data["GPU"]["AccelerateShaders"] = accelerateShaders;
data["GPU"]["EnableRenderdoc"] = enableRenderdoc;
data["GPU"]["HashTextures"] = hashTextures;
data["Audio"]["DSPEmulation"] = std::string(Audio::DSPCore::typeToString(dspType));
data["Audio"]["EnableAudio"] = audioEnabled;

View file

@ -8,6 +8,7 @@
#include "PICA/float_types.hpp"
#include "PICA/gpu.hpp"
#include "PICA/pica_frag_uniforms.hpp"
#include "PICA/pica_hash.hpp"
#include "PICA/pica_simd.hpp"
#include "PICA/regs.hpp"
#include "PICA/shader_decompiler.hpp"
@ -359,17 +360,29 @@ void RendererGL::bindTexturesToSlots() {
const u32 width = getBits<16, 11>(dim);
const u32 addr = (regs[ioBase + 4] & 0x0FFFFFFF) << 3;
u32 format = regs[ioBase + (i == 0 ? 13 : 5)] & 0xF;
glActiveTexture(GL_TEXTURE0 + i);
if (addr != 0) [[likely]] {
Texture targetTex(addr, static_cast<PICA::TextureFmt>(format), width, height, config);
if (hashTextures) {
const u8* startPointer = gpu.getPointerPhys<u8>(targetTex.location);
const usize sizeInBytes = targetTex.sizeInBytes();
if (startPointer == nullptr || (sizeInBytes > 0 && gpu.getPointerPhys<u8>(targetTex.location + sizeInBytes - 1) == nullptr))
[[unlikely]] {
Helpers::warn("Out-of-bounds texture fetch");
} else {
targetTex.hash = PICAHash::computeHash((const char*)startPointer, sizeInBytes);
}
}
OpenGL::Texture tex = getTexture(targetTex);
tex.bind();
} else {
// Mapping a texture from NULL. PICA seems to read the last sampled colour, but for now we will display a black texture instead since it is far easier.
// Games that do this don't really care what it does, they just expect the PICA to not crash, since it doesn't have a PU/MMU and can do all sorts of
// Weird invalid memory accesses without crashing
// Mapping a texture from NULL. PICA seems to read the last sampled colour, but for now we will display a black texture instead since it
// is far easier. Games that do this don't really care what it does, they just expect the PICA to not crash, since it doesn't have a
// PU/MMU and can do all sorts of Weird invalid memory accesses without crashing
blankTexture.bind();
}
}

View file

@ -9,6 +9,7 @@
#undef NO
#include "PICA/gpu.hpp"
#include "PICA/pica_hash.hpp"
#include "SDL_metal.h"
using namespace PICA;
@ -729,6 +730,19 @@ void RendererMTL::bindTexturesToSlots() {
if (addr != 0) [[likely]] {
Metal::Texture targetTex(device, addr, static_cast<PICA::TextureFmt>(format), width, height, config);
if (hashTextures) {
const u8* startPointer = gpu.getPointerPhys<u8>(targetTex.location);
const usize sizeInBytes = targetTex.sizeInBytes();
if (startPointer == nullptr || (sizeInBytes > 0 && gpu.getPointerPhys<u8>(targetTex.location + sizeInBytes - 1) == nullptr))
[[unlikely]] {
Helpers::warn("Out-of-bounds texture fetch");
} else {
targetTex.hash = PICAHash::computeHash((const char*)startPointer, sizeInBytes);
}
}
auto tex = getTexture(targetTex);
commandEncoder.setFragmentTexture(tex.texture, i);
commandEncoder.setFragmentSamplerState(tex.sampler ? tex.sampler : nearestSampler, i);

View file

@ -32,18 +32,14 @@ Emulator::Emulator()
dspService.setDSPCore(dsp.get());
audioDevice.init(dsp->getSamples());
setAudioEnabled(config.audioEnabled);
if (Renderdoc::isSupported() && config.enableRenderdoc) {
loadRenderdoc();
}
#ifdef PANDA3DS_ENABLE_DISCORD_RPC
if (config.discordRpcEnabled) {
discordRpc.init();
updateDiscord();
}
#endif
reloadSettings();
reset(ReloadOption::NoReload);
}
@ -461,6 +457,8 @@ void Emulator::reloadSettings() {
loadRenderdoc();
}
gpu.getRenderer()->setHashTextures(config.hashTextures);
#ifdef PANDA3DS_ENABLE_DISCORD_RPC
// Reload RPC setting if we're compiling with RPC support

View file

@ -176,6 +176,9 @@ static void configInit() {
{"panda3ds_use_ubershader", EmulatorConfig::ubershaderDefault ? "Use ubershaders (No stutter, maybe slower); enabled|disabled"
: "Use ubershaders (No stutter, maybe slower); disabled|enabled"},
{"panda3ds_use_vsync", "Enable VSync; enabled|disabled"},
{"panda3ds_hash_textures", EmulatorConfig::hashTexturesDefault ? "Hash textures (Better graphics, maybe slower); enabled|disabled"
: "Hash textures (Better graphics, maybe slower); disabled|enabled"},
{"panda3ds_system_language", "System language; En|Fr|Es|De|It|Pt|Nl|Ru|Ja|Zh|Ko|Tw"},
{"panda3ds_dsp_emulation", "DSP emulation; HLE|LLE|Null"},
{"panda3ds_use_audio", EmulatorConfig::audioEnabledDefault ? "Enable audio; enabled|disabled" : "Enable audio; disabled|enabled"},
@ -216,6 +219,7 @@ static void configUpdate() {
config.accurateShaderMul = fetchVariableBool("panda3ds_accurate_shader_mul", false);
config.useUbershaders = fetchVariableBool("panda3ds_use_ubershader", EmulatorConfig::ubershaderDefault);
config.accelerateShaders = fetchVariableBool("panda3ds_accelerate_shaders", EmulatorConfig::accelerateShadersDefault);
config.hashTextures = fetchVariableBool("panda3ds_hash_textures", EmulatorConfig::hashTexturesDefault);
config.forceShadergenForLights = fetchVariableBool("panda3ds_ubershader_lighting_override", true);
config.lightShadergenThreshold = fetchVariableRange("panda3ds_ubershader_lighting_override_threshold", 1, 8);

View file

@ -231,6 +231,11 @@ ConfigWindow::ConfigWindow(ConfigCallback configCallback, MainWindowCallback win
connectCheckbox(accelerateShaders, config.accelerateShaders);
gpuLayout->addRow(accelerateShaders);
QCheckBox* hashTextures = new QCheckBox(tr("Hash textures"));
hashTextures->setToolTip(tr("Enable this to reduce texture mismatches at the cost of slightly lower performance"));
connectCheckbox(hashTextures, config.hashTextures);
gpuLayout->addRow(hashTextures);
QCheckBox* forceShadergenForLights = new QCheckBox(tr("Force shadergen when rendering lights"));
connectCheckbox(forceShadergenForLights, config.forceShadergenForLights);
gpuLayout->addRow(forceShadergenForLights);