From 6f3c7d358b777d04175835f1abc019c6b5a3a884 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 18 Jan 2024 00:27:17 +0200 Subject: [PATCH 001/251] Add GLSL shader gen files --- CMakeLists.txt | 4 ++-- include/PICA/shader_gen.hpp | 24 ++++++++++++++++++++++++ src/core/PICA/shader_gen_glsl.cpp | 30 ++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 2 deletions(-) create mode 100644 include/PICA/shader_gen.hpp create mode 100644 src/core/PICA/shader_gen_glsl.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b4b1503a..2d5df370 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -179,7 +179,7 @@ set(SERVICE_SOURCE_FILES src/core/services/service_manager.cpp src/core/services set(PICA_SOURCE_FILES src/core/PICA/gpu.cpp src/core/PICA/regs.cpp src/core/PICA/shader_unit.cpp src/core/PICA/shader_interpreter.cpp src/core/PICA/dynapica/shader_rec.cpp src/core/PICA/dynapica/shader_rec_emitter_x64.cpp src/core/PICA/pica_hash.cpp - src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp + src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp src/core/PICA/shader_gen_glsl.cpp ) set(LOADER_SOURCE_FILES src/core/loader/elf.cpp src/core/loader/ncsd.cpp src/core/loader/ncch.cpp src/core/loader/3dsx.cpp src/core/loader/lz77.cpp) @@ -244,7 +244,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/services/news_u.hpp include/applets/software_keyboard.hpp include/applets/applet_manager.hpp include/fs/archive_user_save_data.hpp include/services/amiibo_device.hpp include/services/nfc_types.hpp include/swap.hpp include/services/csnd.hpp include/services/nwm_uds.hpp include/fs/archive_system_save_data.hpp include/lua_manager.hpp include/memory_mapped_file.hpp include/hydra_icon.hpp - include/PICA/dynapica/shader_rec_emitter_arm64.hpp + include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/PICA/shader_gen.hpp ) cmrc_add_resource_library( diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp new file mode 100644 index 00000000..b52cd7ab --- /dev/null +++ b/include/PICA/shader_gen.hpp @@ -0,0 +1,24 @@ +#pragma once +#include + +#include "PICA/gpu.hpp" +#include "PICA/regs.hpp" +#include "helpers.hpp" + +namespace PICA::ShaderGen { + // Graphics API this shader is targetting + enum class API { GL, GLES, Vulkan }; + + // Shading language to use (Only GLSL for the time being) + enum class Language { GLSL }; + + class FragmentGenerator { + using PICARegs = std::array; + API api; + Language language; + + public: + FragmentGenerator(API api, Language language) : api(api), language(language) {} + std::string generate(const PICARegs& regs); + }; +}; // namespace PICA::ShaderGen \ No newline at end of file diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp new file mode 100644 index 00000000..661002ac --- /dev/null +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -0,0 +1,30 @@ +#include "PICA/shader_gen.hpp" +using namespace PICA::ShaderGen; + +std::string FragmentGenerator::generate(const PICARegs& regs) { + std::string ret = ""; + + switch (api) { + case API::GL: ret += "#version 410 core"; break; + case API::GLES: ret += "#version 300 es"; break; + default: break; + } + + // Input and output attributes + ret += R"( + in vec3 v_tangent; + in vec3 v_normal; + in vec3 v_bitangent; + in vec4 v_colour; + in vec3 v_texcoord0; + in vec2 v_texcoord1; + in vec3 v_view; + in vec2 v_texcoord2; + flat in vec4 v_textureEnvColor[6]; + flat in vec4 v_textureEnvBufferColor; + + out vec4 fragColour; + )"; + + return ret; +} \ No newline at end of file From ef2467bc6029b9e79bfba5651090f7767ea51863 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 18 Jan 2024 02:59:29 +0200 Subject: [PATCH 002/251] TEV definitions for shader generator --- include/PICA/regs.hpp | 104 +++++++++++++++++++++++++++ include/PICA/shader_gen.hpp | 2 + include/renderer_gl/renderer_gl.hpp | 5 +- src/core/PICA/shader_gen_glsl.cpp | 41 +++++++++++ src/core/renderer_gl/renderer_gl.cpp | 1 + 5 files changed, 152 insertions(+), 1 deletion(-) diff --git a/include/PICA/regs.hpp b/include/PICA/regs.hpp index 70cecf7b..100a0573 100644 --- a/include/PICA/regs.hpp +++ b/include/PICA/regs.hpp @@ -344,4 +344,108 @@ namespace PICA { GeometryPrimitive = 3, }; + struct TexEnvConfig { + enum class Source : u8 { + PrimaryColor = 0x0, + PrimaryFragmentColor = 0x1, + SecondaryFragmentColor = 0x2, + Texture0 = 0x3, + Texture1 = 0x4, + Texture2 = 0x5, + Texture3 = 0x6, + // TODO: Inbetween values are unknown + PreviousBuffer = 0xD, + Constant = 0xE, + Previous = 0xF, + }; + + enum class ColorOperand : u8 { + SourceColor = 0x0, + OneMinusSourceColor = 0x1, + SourceAlpha = 0x2, + OneMinusSourceAlpha = 0x3, + SourceRed = 0x4, + OneMinusSourceRed = 0x5, + // TODO: Inbetween values are unknown + SourceGreen = 0x8, + OneMinusSourceGreen = 0x9, + // Inbetween values are unknown + SourceBlue = 0xC, + OneMinusSourceBlue = 0xD, + }; + + enum class AlphaOperand : u8 { + SourceAlpha = 0x0, + OneMinusSourceAlpha = 0x1, + SourceRed = 0x2, + OneMinusSourceRed = 0x3, + SourceGreen = 0x4, + OneMinusSourceGreen = 0x5, + SourceBlue = 0x6, + OneMinusSourceBlue = 0x7, + }; + + enum class Operation : u8 { + Replace = 0, + Modulate = 1, + Add = 2, + AddSigned = 3, + Lerp = 4, + Subtract = 5, + Dot3RGB = 6, + Dot3RGBA = 7, + MultiplyAdd = 8, + AddMultiply = 9, + }; + + // RGB sources + Source colorSource1, colorSource2, colorSource3; + // Alpha sources + Source alphaSource1, alphaSource2, alphaSource3; + + // RGB operands + ColorOperand colorOperand1, colorOperand2, colorOperand3; + // Alpha operands + AlphaOperand alphaOperand1, alphaOperand2, alphaOperand3; + + // Texture environment operations for this stage + Operation colorOp, alphaOp; + + u32 constColor; + + private: + // These are the only private members since their value doesn't actually reflect the scale + // So we make them public so we'll always use the appropriate member functions instead + u8 colorScale; + u8 alphaScale; + + public: + // Create texture environment object from TEV registers + TexEnvConfig(u32 source, u32 operand, u32 combiner, u32 color, u32 scale) : constColor(color) { + colorSource1 = Helpers::getBits<0, 4, Source>(source); + colorSource2 = Helpers::getBits<4, 4, Source>(source); + colorSource3 = Helpers::getBits<8, 4, Source>(source); + + alphaSource1 = Helpers::getBits<16, 4, Source>(source); + alphaSource2 = Helpers::getBits<20, 4, Source>(source); + alphaSource3 = Helpers::getBits<24, 4, Source>(source); + + colorOperand1 = Helpers::getBits<0, 4, ColorOperand>(operand); + colorOperand2 = Helpers::getBits<4, 4, ColorOperand>(operand); + colorOperand3 = Helpers::getBits<8, 4, ColorOperand>(operand); + + alphaOperand1 = Helpers::getBits<12, 3, AlphaOperand>(operand); + alphaOperand2 = Helpers::getBits<16, 3, AlphaOperand>(operand); + alphaOperand3 = Helpers::getBits<20, 3, AlphaOperand>(operand); + + colorOp = Helpers::getBits<0, 4, Operation>(combiner); + alphaOp = Helpers::getBits<16, 4, Operation>(combiner); + + colorScale = Helpers::getBits<0, 2>(scale); + alphaScale = Helpers::getBits<16, 2>(scale); + } + + u32 getColorScale() { return (colorScale <= 2) ? (1 << colorScale) : 1; } + u32 getAlphaScale() { return (alphaScale <= 2) ? (1 << alphaScale) : 1; } + }; } // namespace PICA diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index b52cd7ab..3fa66871 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -17,6 +17,8 @@ namespace PICA::ShaderGen { API api; Language language; + void compileTEV(std::string& shader, int stage, const PICARegs& regs); + public: FragmentGenerator(API api, Language language) : api(api), language(language) {} std::string generate(const PICARegs& regs); diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index 92f02662..b662023f 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -6,6 +6,7 @@ #include "PICA/float_types.hpp" #include "PICA/pica_vertex.hpp" #include "PICA/regs.hpp" +#include "PICA/shader_gen.hpp" #include "gl_state.hpp" #include "helpers.hpp" #include "logger.hpp" @@ -60,6 +61,8 @@ class RendererGL final : public Renderer { OpenGL::Framebuffer getColourFBO(); OpenGL::Texture getTexture(Texture& tex); + PICA::ShaderGen::FragmentGenerator fragShaderGen; + MAKE_LOG_FUNCTION(log, rendererLogger) void setupBlending(); void setupStencilTest(bool stencilEnable); @@ -71,7 +74,7 @@ class RendererGL final : public Renderer { public: RendererGL(GPU& gpu, const std::array& internalRegs, const std::array& externalRegs) - : Renderer(gpu, internalRegs, externalRegs) {} + : Renderer(gpu, internalRegs, externalRegs), fragShaderGen(PICA::ShaderGen::API::GL, PICA::ShaderGen::Language::GLSL) {} ~RendererGL() override; void reset() override; diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 661002ac..d423016d 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -1,4 +1,5 @@ #include "PICA/shader_gen.hpp" +using namespace PICA; using namespace PICA::ShaderGen; std::string FragmentGenerator::generate(const PICARegs& regs) { @@ -10,6 +11,8 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { default: break; } + bool unimplementedFlag = false; + // Input and output attributes ret += R"( in vec3 v_tangent; @@ -24,7 +27,45 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { flat in vec4 v_textureEnvBufferColor; out vec4 fragColour; + uniform sampler2D u_tex0; + uniform sampler2D u_tex1; + uniform sampler2D u_tex2; + uniform sampler1DArray u_tex_lighting_lut; + + vec4 tevSources[16]; + vec4 tevNextPreviousBuffer; + + vec3 regToColor(uint reg) { + // Normalization scale to convert from [0...255] to [0.0...1.0] + const float scale = 1.0 / 255.0; + + return scale * vec3(float(bitfieldExtract(reg, 20, 8)), float(bitfieldExtract(reg, 10, 8)), float(bitfieldExtract(reg, 00, 8))); + } )"; + // Emit main function for fragment shader + // When not initialized, source 13 is set to vec4(0.0) and 15 is set to the vertex colour + ret += R"( + void main() { + tevSources[0] = v_colour; + tevSources[13] = vec4(0.0); // Previous buffer colour + tevSources[15] = v_colour; // Previous combiner + )"; + + for (int i = 0; i < 6; i++) { + compileTEV(ret, i, regs); + } + return ret; +} + +void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICARegs& regs) { + // Base address for each TEV stage's configuration + static constexpr std::array ioBases = { + InternalRegs::TexEnv0Source, InternalRegs::TexEnv1Source, InternalRegs::TexEnv2Source, + InternalRegs::TexEnv3Source, InternalRegs::TexEnv4Source, InternalRegs::TexEnv5Source, + }; + + const u32 ioBase = ioBases[stage]; + TexEnvConfig tev(regs[ioBase], regs[ioBase + 1], regs[ioBase + 2], regs[ioBase + 3], regs[ioBase + 4]); } \ No newline at end of file diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index a11a6ffa..4828e4e6 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -388,6 +388,7 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v OpenGL::TriangleFan, OpenGL::Triangle, }; + std::cout << fragShaderGen.generate(regs); const auto primitiveTopology = primTypes[static_cast(primType)]; gl.disableScissor(); From c13c8046d47cf6e7a1a0654406ccb7129356f697 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 18 Jan 2024 03:11:41 +0200 Subject: [PATCH 003/251] Detect passthrough TEV stages --- include/PICA/regs.hpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/include/PICA/regs.hpp b/include/PICA/regs.hpp index 100a0573..b807ae5c 100644 --- a/include/PICA/regs.hpp +++ b/include/PICA/regs.hpp @@ -447,5 +447,17 @@ namespace PICA { u32 getColorScale() { return (colorScale <= 2) ? (1 << colorScale) : 1; } u32 getAlphaScale() { return (alphaScale <= 2) ? (1 << alphaScale) : 1; } + + bool isPassthroughStage() { + // clang-format off + // Thank you to the Citra dev that wrote this out + return ( + colorOp == Operation::Replace && alphaOp == Operation::Replace && + colorSource1 == Source::Previous && alphaSource1 == Source::Previous && + colorOperand1 == ColorOperand::SourceColor && alphaOperand1 == AlphaOperand::SourceAlpha && + getColorScale() == 1 && getAlphaScale() == 1 + ); + // clang-format on + } }; } // namespace PICA From 45ae6bd3a8ff7b05c93a188c34fa9d61295f4116 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 2 Feb 2024 01:16:45 +0200 Subject: [PATCH 004/251] Getting TEV operations working --- include/PICA/shader_gen.hpp | 5 ++ src/core/PICA/shader_gen_glsl.cpp | 145 ++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index 3fa66871..aaef8b30 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -18,6 +18,11 @@ namespace PICA::ShaderGen { Language language; void compileTEV(std::string& shader, int stage, const PICARegs& regs); + void getSource(std::string& shader, PICA::TexEnvConfig::Source source, int index); + void getColorOperand(std::string& shader, PICA::TexEnvConfig::Source source, PICA::TexEnvConfig::ColorOperand color, int index); + void getAlphaOperand(std::string& shader, PICA::TexEnvConfig::Source source, PICA::TexEnvConfig::AlphaOperand alpha, int index); + void getColorOperation(std::string& shader, PICA::TexEnvConfig::Operation op); + void getAlphaOperation(std::string& shader, PICA::TexEnvConfig::Operation op); public: FragmentGenerator(API api, Language language) : api(api), language(language) {} diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index d423016d..0c41b8d0 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -52,10 +52,23 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { tevSources[15] = v_colour; // Previous combiner )"; + ret += R"( + vec3 colorOperand1 = vec3(0.0); + vec3 colorOperand2 = vec3(0.0); + vec3 colorOperand3 = vec3(0.0); + + float alphaOperand1 = 0.0; + float alphaOperand2 = 0.0; + float alphaOperand3 = 0.0; + )"; + for (int i = 0; i < 6; i++) { compileTEV(ret, i, regs); } + ret += "}"; // End of main function + ret += "\n\n\n\n\n\n\n\n\n\n\n\n\n"; + return ret; } @@ -68,4 +81,136 @@ void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICAReg const u32 ioBase = ioBases[stage]; TexEnvConfig tev(regs[ioBase], regs[ioBase + 1], regs[ioBase + 2], regs[ioBase + 3], regs[ioBase + 4]); + + if (!tev.isPassthroughStage()) { + // Get color operands + shader += "colorOp1 = "; + getColorOperand(shader, tev.colorSource1, tev.colorOperand1, stage); + + shader += ";\ncolorOp2 = "; + getColorOperand(shader, tev.colorSource2, tev.colorOperand2, stage); + + shader += ";\ncolorOp3 = "; + getColorOperand(shader, tev.colorSource3, tev.colorOperand3, stage); + + shader += ";\nvec3 outputColor" + std::to_string(stage) + " = vec3(1.0)"; + shader += ";\n"; + + if (tev.colorOp == TexEnvConfig::Operation::Dot3RGBA) { + // Dot3 RGBA also writes to the alpha component so we don't need to do anything more + shader += "float outputAlpha" + std::to_string(stage) + " = colorOutput" + std::to_string(stage) + ".x;\n"; + } else { + // Get alpha operands + shader += "alphaOp1 = "; + getAlphaOperand(shader, tev.alphaSource1, tev.alphaOperand1, stage); + + shader += ";\nalphaOp2 = "; + getAlphaOperand(shader, tev.alphaSource2, tev.alphaOperand2, stage); + + shader += ";\nalphaOp3 = "; + getAlphaOperand(shader, tev.alphaSource3, tev.alphaOperand3, stage); + + shader += ";\nvec3 outputAlpha" + std::to_string(stage) + " = 1.0"; + shader += ";\n"; + } + } +} + +void FragmentGenerator::getColorOperand(std::string& shader, TexEnvConfig::Source source, TexEnvConfig::ColorOperand color, int index) { + using OperandType = TexEnvConfig::ColorOperand; + + // For inverting operands, add the 1.0 - x subtraction + if (color == OperandType::OneMinusSourceColor || color == OperandType::OneMinusSourceRed || color == OperandType::OneMinusSourceGreen || + color == OperandType::OneMinusSourceBlue || color == OperandType::OneMinusSourceAlpha) { + shader += "vec3(1.0, 1.0, 1.0) - "; + } + + switch (color) { + case OperandType::SourceColor: + case OperandType::OneMinusSourceColor: + getSource(shader, source, index); + shader += ".rgb"; + break; + + case OperandType::SourceRed: + case OperandType::OneMinusSourceRed: + getSource(shader, source, index); + shader += ".rrr"; + break; + + case OperandType::SourceGreen: + case OperandType::OneMinusSourceGreen: + getSource(shader, source, index); + shader += ".ggg"; + break; + + case OperandType::SourceBlue: + case OperandType::OneMinusSourceBlue: + getSource(shader, source, index); + shader += ".bbb"; + break; + + case OperandType::SourceAlpha: + case OperandType::OneMinusSourceAlpha: + getSource(shader, source, index); + shader += ".aaa"; + break; + + default: + shader += "vec3(1.0, 1.0, 1.0)"; + Helpers::warn("FragmentGenerator: Invalid TEV color operand"); + break; + } +} + +void FragmentGenerator::getAlphaOperand(std::string& shader, TexEnvConfig::Source source, TexEnvConfig::AlphaOperand color, int index) { + using OperandType = TexEnvConfig::AlphaOperand; + + // For inverting operands, add the 1.0 - x subtraction + if (color == OperandType::OneMinusSourceRed || color == OperandType::OneMinusSourceGreen || color == OperandType::OneMinusSourceBlue || + color == OperandType::OneMinusSourceAlpha) { + shader += "1.0 - "; + } + + switch (color) { + case OperandType::SourceRed: + case OperandType::OneMinusSourceRed: + getSource(shader, source, index); + shader += ".r"; + break; + + case OperandType::SourceGreen: + case OperandType::OneMinusSourceGreen: + getSource(shader, source, index); + shader += ".g"; + break; + + case OperandType::SourceBlue: + case OperandType::OneMinusSourceBlue: + getSource(shader, source, index); + shader += ".b"; + break; + + case OperandType::SourceAlpha: + case OperandType::OneMinusSourceAlpha: + getSource(shader, source, index); + shader += ".a"; + break; + + default: + shader += "1.0"; + Helpers::warn("FragmentGenerator: Invalid TEV color operand"); + break; + } +} + +void FragmentGenerator::getSource(std::string& shader, TexEnvConfig::Source source, int index) { + switch (source) { + case TexEnvConfig::Source::PrimaryColor: shader += "v_colour"; break; + + default: + Helpers::warn("Unimplemented TEV source: %d", static_cast(source)); + shader += "vec4(1.0, 1.0, 1.0, 1.0)"; + break; + } } \ No newline at end of file From 7e77af0de88d5a6bdbbb35b4d7372aec23ce187b Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 2 Feb 2024 01:34:33 +0200 Subject: [PATCH 005/251] GLSL Fragment Generator: Working color operations --- src/core/PICA/shader_gen_glsl.cpp | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 0c41b8d0..829aec03 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -93,7 +93,8 @@ void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICAReg shader += ";\ncolorOp3 = "; getColorOperand(shader, tev.colorSource3, tev.colorOperand3, stage); - shader += ";\nvec3 outputColor" + std::to_string(stage) + " = vec3(1.0)"; + shader += ";\nvec3 outputColor" + std::to_string(stage) + " = "; + getColorOperation(shader, tev.colorOp); shader += ";\n"; if (tev.colorOp == TexEnvConfig::Operation::Dot3RGBA) { @@ -213,4 +214,24 @@ void FragmentGenerator::getSource(std::string& shader, TexEnvConfig::Source sour shader += "vec4(1.0, 1.0, 1.0, 1.0)"; break; } +} + +void FragmentGenerator::getColorOperation(std::string& shader, TexEnvConfig::Operation op) { + switch (op) { + case TexEnvConfig::Operation::Replace: shader += "colorOp1"; break; + case TexEnvConfig::Operation::Add: shader += "colorOp1 + colorOp2"; break; + case TexEnvConfig::Operation::AddSigned: shader += "clamp(colorOp1 + colorOp2 - 0.5, 0.0, 1.0);"; break; + case TexEnvConfig::Operation::Subtract: shader += "colorOp1 - colorOp2"; break; + case TexEnvConfig::Operation::Modulate: shader += "colorOp1 * colorOp2"; break; + case TexEnvConfig::Operation::Lerp: shader += "colorOp1 * colorOp3 + colorOp2 * (vec(1.0) - colorOp3)"; break; + + case TexEnvConfig::Operation::AddMultiply: shader += "min(colorOp1 + colorOp2, vec3(1.0)) * colorOp3"; break; + case TexEnvConfig::Operation::MultiplyAdd: shader += "colorOp1 * colorOp2 + colorOp3"; break; + case TexEnvConfig::Operation::Dot3RGB: + case TexEnvConfig::Operation::Dot3RGBA: shader += "vec3(4.0 * dot(colorOp1 - 0.5, colorOp2 - 0.5))"; break; + default: + Helpers::warn("FragmentGenerator: Unimplemented color op"); + shader += "vec3(1.0)"; + break; + } } \ No newline at end of file From 0be8c45e377a47a6db0cb530c508063b0f129f6b Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 2 Feb 2024 01:51:37 +0200 Subject: [PATCH 006/251] Fragment shader gen: Properly track TEV outputs --- src/core/PICA/shader_gen_glsl.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 829aec03..28dbc5ab 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -26,7 +26,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { flat in vec4 v_textureEnvColor[6]; flat in vec4 v_textureEnvBufferColor; - out vec4 fragColour; + out vec4 fragColor; uniform sampler2D u_tex0; uniform sampler2D u_tex1; uniform sampler2D u_tex2; @@ -50,6 +50,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { tevSources[0] = v_colour; tevSources[13] = vec4(0.0); // Previous buffer colour tevSources[15] = v_colour; // Previous combiner + vec4 combinerOutput = v_colour; // Last TEV output )"; ret += R"( @@ -66,6 +67,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { compileTEV(ret, i, regs); } + ret += "fragColor = combinerOutput;\n"; ret += "}"; // End of main function ret += "\n\n\n\n\n\n\n\n\n\n\n\n\n"; @@ -114,6 +116,10 @@ void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICAReg shader += ";\nvec3 outputAlpha" + std::to_string(stage) + " = 1.0"; shader += ";\n"; } + + shader += "combinerOutput = vec4(clamp(outputColor" + std::to_string(stage) + " * " + std::to_string(tev.getColorScale()) + + ".0, vec3(0.0), vec3(1.0)), clamp(outputAlpha" + std::to_string(stage) + " * " + std::to_string(tev.getAlphaScale()) + + ".0, 0.0, 1.0));\n"; } } From 10654ce1ca94bf47f702fa36b30707c1de49434c Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 4 Feb 2024 18:54:29 +0200 Subject: [PATCH 007/251] GLSL generator: Add textures and alpha operations --- include/PICA/shader_gen.hpp | 2 ++ src/core/PICA/shader_gen_glsl.cpp | 40 +++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index aaef8b30..80c57d46 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -24,6 +24,8 @@ namespace PICA::ShaderGen { void getColorOperation(std::string& shader, PICA::TexEnvConfig::Operation op); void getAlphaOperation(std::string& shader, PICA::TexEnvConfig::Operation op); + u32 textureConfig = 0; + public: FragmentGenerator(API api, Language language) : api(api), language(language) {} std::string generate(const PICARegs& regs); diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 28dbc5ab..47cf2a7b 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -63,6 +63,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { float alphaOperand3 = 0.0; )"; + textureConfig = regs[InternalRegs::TexUnitCfg]; for (int i = 0; i < 6; i++) { compileTEV(ret, i, regs); } @@ -113,8 +114,10 @@ void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICAReg shader += ";\nalphaOp3 = "; getAlphaOperand(shader, tev.alphaSource3, tev.alphaOperand3, stage); - shader += ";\nvec3 outputAlpha" + std::to_string(stage) + " = 1.0"; - shader += ";\n"; + shader += ";\nvec3 outputAlpha" + std::to_string(stage) + " = "; + getAlphaOperation(shader, tev.alphaOp); + // Clamp the alpha value to [0.0, 1.0] + shader += ";\nclamp(outputAlpha" + std::to_string(stage) + ", 0.0, 1.0);\n"; } shader += "combinerOutput = vec4(clamp(outputColor" + std::to_string(stage) + " * " + std::to_string(tev.getColorScale()) + @@ -214,6 +217,19 @@ void FragmentGenerator::getAlphaOperand(std::string& shader, TexEnvConfig::Sourc void FragmentGenerator::getSource(std::string& shader, TexEnvConfig::Source source, int index) { switch (source) { case TexEnvConfig::Source::PrimaryColor: shader += "v_colour"; break; + case TexEnvConfig::Source::Texture0: shader += "texture(u_tex0, v_texcoord0.xy)"; break; + case TexEnvConfig::Source::Texture1: shader += "texture(u_tex1, v_texcoord1)"; break; + case TexEnvConfig::Source::Texture2: { + // If bit 13 in texture config is set then we use the texcoords for texture 1, otherwise for texture 2 + if (Helpers::getBit<13>(textureConfig)) { + shader += "texture(u_tex2, v_texcoord1)"; + } else { + shader += "texture(u_tex2, v_texcoord2)"; + } + break; + } + + case TexEnvConfig::Source::Previous: shader += "combinerOutput"; break; default: Helpers::warn("Unimplemented TEV source: %d", static_cast(source)); @@ -240,4 +256,24 @@ void FragmentGenerator::getColorOperation(std::string& shader, TexEnvConfig::Ope shader += "vec3(1.0)"; break; } +} + +void FragmentGenerator::getAlphaOperation(std::string& shader, TexEnvConfig::Operation op) { + switch (op) { + case TexEnvConfig::Operation::Replace: shader += "alphaOp1"; break; + case TexEnvConfig::Operation::Add: shader += "alphaOp1 + alphaOp2"; break; + case TexEnvConfig::Operation::AddSigned: shader += "clamp(alphaOp1 + alphaOp2 - 0.5, 0.0, 1.0);"; break; + case TexEnvConfig::Operation::Subtract: shader += "alphaOp1 - alphaOp2"; break; + case TexEnvConfig::Operation::Modulate: shader += "alphaOp1 * alphaOp2"; break; + case TexEnvConfig::Operation::Lerp: shader += "alphaOp1 * alphaOp3 + alphaOp2 * (vec(1.0) - alphaOp3)"; break; + + case TexEnvConfig::Operation::AddMultiply: shader += "min(alphaOp1 + alphaOp2, vec3(1.0)) * alphaOp3"; break; + case TexEnvConfig::Operation::MultiplyAdd: shader += "alphaOp1 * alphaOp2 + alphaOp3"; break; + case TexEnvConfig::Operation::Dot3RGB: + case TexEnvConfig::Operation::Dot3RGBA: shader += "vec3(4.0 * dot(alphaOp1 - 0.5, alphaOp2 - 0.5))"; break; + default: + Helpers::warn("FragmentGenerator: Unimplemented alpha op"); + shader += "vec3(1.0)"; + break; + } } \ No newline at end of file From ddc14cea0967b231e45a378d6bb8797779177951 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 29 Feb 2024 00:56:24 +0200 Subject: [PATCH 008/251] Fix shader compilation errors --- include/PICA/shader_gen.hpp | 1 + src/core/PICA/shader_gen_glsl.cpp | 77 +++++++++++++++++++++++++--- src/core/renderer_gl/renderer_gl.cpp | 10 +++- 3 files changed, 80 insertions(+), 8 deletions(-) diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index 80c57d46..e07575a5 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -29,5 +29,6 @@ namespace PICA::ShaderGen { public: FragmentGenerator(API api, Language language) : api(api), language(language) {} std::string generate(const PICARegs& regs); + std::string getVertexShader(const PICARegs& regs); }; }; // namespace PICA::ShaderGen \ No newline at end of file diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 47cf2a7b..3a7e9b74 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -2,6 +2,69 @@ using namespace PICA; using namespace PICA::ShaderGen; +std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { + std::string ret = ""; + + switch (api) { + case API::GL: ret += "#version 410 core"; break; + case API::GLES: ret += "#version 300 es"; break; + default: break; + } + + ret += R"( + layout(location = 0) in vec4 a_coords; + layout(location = 1) in vec4 a_quaternion; + layout(location = 2) in vec4 a_vertexColour; + layout(location = 3) in vec2 a_texcoord0; + layout(location = 4) in vec2 a_texcoord1; + layout(location = 5) in float a_texcoord0_w; + layout(location = 6) in vec3 a_view; + layout(location = 7) in vec2 a_texcoord2; + + out vec3 v_normal; + out vec3 v_tangent; + out vec3 v_bitangent; + out vec4 v_colour; + out vec3 v_texcoord0; + out vec2 v_texcoord1; + out vec3 v_view; + out vec2 v_texcoord2; + flat out vec4 v_textureEnvColor[6]; + flat out vec4 v_textureEnvBufferColor; + + //out float gl_ClipDistance[2]; + + vec4 abgr8888ToVec4(uint abgr) { + const float scale = 1.0 / 255.0; + return scale * vec4(float(abgr & 0xffu), float((abgr >> 8) & 0xffu), float((abgr >> 16) & 0xffu), float(abgr >> 24)); + } + + vec3 rotateVec3ByQuaternion(vec3 v, vec4 q) { + vec3 u = q.xyz; + float s = q.w; + return 2.0 * dot(u, v) * u + (s * s - dot(u, u)) * v + 2.0 * s * cross(u, v); + } + + void main() { + gl_Position = a_coords; + vec4 colourAbs = abs(a_vertexColour); + v_colour = min(colourAbs, vec4(1.f)); + + // Flip y axis of UVs because OpenGL uses an inverted y for texture sampling compared to the PICA + v_texcoord0 = vec3(a_texcoord0.x, 1.0 - a_texcoord0.y, a_texcoord0_w); + v_texcoord1 = vec2(a_texcoord1.x, 1.0 - a_texcoord1.y); + v_texcoord2 = vec2(a_texcoord2.x, 1.0 - a_texcoord2.y); + v_view = a_view; + + v_normal = normalize(rotateVec3ByQuaternion(vec3(0.0, 0.0, 1.0), a_quaternion)); + v_tangent = normalize(rotateVec3ByQuaternion(vec3(1.0, 0.0, 0.0), a_quaternion)); + v_bitangent = normalize(rotateVec3ByQuaternion(vec3(0.0, 1.0, 0.0), a_quaternion)); + } +)"; + + return ret; +} + std::string FragmentGenerator::generate(const PICARegs& regs) { std::string ret = ""; @@ -54,13 +117,13 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { )"; ret += R"( - vec3 colorOperand1 = vec3(0.0); - vec3 colorOperand2 = vec3(0.0); - vec3 colorOperand3 = vec3(0.0); + vec3 colorOp1 = vec3(0.0); + vec3 colorOp2 = vec3(0.0); + vec3 colorOp3 = vec3(0.0); - float alphaOperand1 = 0.0; - float alphaOperand2 = 0.0; - float alphaOperand3 = 0.0; + float alphaOp1 = 0.0; + float alphaOp2 = 0.0; + float alphaOp3 = 0.0; )"; textureConfig = regs[InternalRegs::TexUnitCfg]; @@ -114,7 +177,7 @@ void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICAReg shader += ";\nalphaOp3 = "; getAlphaOperand(shader, tev.alphaSource3, tev.alphaOperand3, stage); - shader += ";\nvec3 outputAlpha" + std::to_string(stage) + " = "; + shader += ";\nfloat outputAlpha" + std::to_string(stage) + " = "; getAlphaOperation(shader, tev.alphaOp); // Clamp the alpha value to [0.0, 1.0] shader += ";\nclamp(outputAlpha" + std::to_string(stage) + ", 0.0, 1.0);\n"; diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 4828e4e6..95175130 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -388,7 +388,15 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v OpenGL::TriangleFan, OpenGL::Triangle, }; - std::cout << fragShaderGen.generate(regs); + + std::string vs = fragShaderGen.getVertexShader(regs); + std::string fs = fragShaderGen.generate(regs); + std::cout << fs << "\n\n\n"; + + OpenGL::Program program; + OpenGL::Shader vertShader({vs.c_str(), vs.size()}, OpenGL::Vertex); + OpenGL::Shader fragShader({fs.c_str(), fs.size()}, OpenGL::Fragment); + program.create({vertShader, fragShader}); const auto primitiveTopology = primTypes[static_cast(primType)]; gl.disableScissor(); From fdfb012aa149487c46732d6642ab417460a56d4b Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 29 Feb 2024 01:28:00 +0200 Subject: [PATCH 009/251] GL: Add RendererGL::getSpecializedShader --- include/renderer_gl/renderer_gl.hpp | 1 + src/core/renderer_gl/renderer_gl.cpp | 25 +++++++++++++++++++++---- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index b662023f..7bc1087a 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -60,6 +60,7 @@ class RendererGL final : public Renderer { OpenGL::Framebuffer getColourFBO(); OpenGL::Texture getTexture(Texture& tex); + OpenGL::Program getSpecializedShader(); PICA::ShaderGen::FragmentGenerator fragShaderGen; diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 95175130..0bb592cf 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -393,10 +393,7 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v std::string fs = fragShaderGen.generate(regs); std::cout << fs << "\n\n\n"; - OpenGL::Program program; - OpenGL::Shader vertShader({vs.c_str(), vs.size()}, OpenGL::Vertex); - OpenGL::Shader fragShader({fs.c_str(), fs.size()}, OpenGL::Fragment); - program.create({vertShader, fragShader}); + OpenGL::Program program = getSpecializedShader(); const auto primitiveTopology = primTypes[static_cast(primType)]; gl.disableScissor(); @@ -787,6 +784,26 @@ std::optional RendererGL::getColourBuffer(u32 addr, PICA::ColorFmt return colourBufferCache.add(sampleBuffer); } +OpenGL::Program RendererGL::getSpecializedShader() { + OpenGL::Program program; + + std::string vs = fragShaderGen.getVertexShader(regs); + std::string fs = fragShaderGen.generate(regs); + + OpenGL::Shader vertShader({vs.c_str(), vs.size()}, OpenGL::Vertex); + OpenGL::Shader fragShader({fs.c_str(), fs.size()}, OpenGL::Fragment); + program.create({vertShader, fragShader}); + program.use(); + + // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 + glUniform1i(OpenGL::uniformLocation(program, "u_tex0"), 0); + glUniform1i(OpenGL::uniformLocation(program, "u_tex1"), 1); + glUniform1i(OpenGL::uniformLocation(program, "u_tex2"), 2); + glUniform1i(OpenGL::uniformLocation(program, "u_tex_lighting_lut"), 3); + + return program; +} + void RendererGL::screenshot(const std::string& name) { constexpr uint width = 400; constexpr uint height = 2 * 240; From 67fe3214fe00ddff37dd0a133c0fd1c38859987f Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 2 Mar 2024 20:41:23 +0200 Subject: [PATCH 010/251] Add shader cache --- include/renderer_gl/renderer_gl.hpp | 64 ++++++++++++++----- src/core/PICA/shader_gen_glsl.cpp | 8 +-- src/core/renderer_gl/renderer_gl.cpp | 92 ++++++++++++++++------------ 3 files changed, 106 insertions(+), 58 deletions(-) diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index 7bc1087a..e8eaeacb 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -1,9 +1,13 @@ #pragma once #include +#include +#include #include +#include #include "PICA/float_types.hpp" +#include "PICA/pica_hash.hpp" #include "PICA/pica_vertex.hpp" #include "PICA/regs.hpp" #include "PICA/shader_gen.hpp" @@ -17,6 +21,32 @@ // More circular dependencies! class GPU; +namespace PICA { + struct FragmentConfig { + u32 texUnitConfig; + u32 texEnvUpdateBuffer; + + // TODO: This should probably be a uniform + u32 texEnvBufferColor; + + // There's 6 TEV stages, and each one is configured via 5 word-sized registers + std::array tevConfigs; + + // Hash function and equality operator required by std::unordered_map + bool operator==(const FragmentConfig& config) const { + return std::memcmp(this, &config, sizeof(FragmentConfig)) == 0; + } + }; +} // namespace PICA + +// Override std::hash for our fragment config class +template <> +struct std::hash { + std::size_t operator()(const PICA::FragmentConfig& config) const noexcept { + return PICAHash::computeHash((const char*)&config, sizeof(config)); + } +}; + class RendererGL final : public Renderer { GLStateManager gl = {}; @@ -26,20 +56,23 @@ class RendererGL final : public Renderer { OpenGL::VertexArray vao; OpenGL::VertexBuffer vbo; - // TEV configuration uniform locations - GLint textureEnvSourceLoc = -1; - GLint textureEnvOperandLoc = -1; - GLint textureEnvCombinerLoc = -1; - GLint textureEnvColorLoc = -1; - GLint textureEnvScaleLoc = -1; + // Data + struct { + // TEV configuration uniform locations + GLint textureEnvSourceLoc = -1; + GLint textureEnvOperandLoc = -1; + GLint textureEnvCombinerLoc = -1; + GLint textureEnvColorLoc = -1; + GLint textureEnvScaleLoc = -1; - // Uniform of PICA registers - GLint picaRegLoc = -1; + // Uniform of PICA registers + GLint picaRegLoc = -1; - // Depth configuration uniform locations - GLint depthOffsetLoc = -1; - GLint depthScaleLoc = -1; - GLint depthmapEnableLoc = -1; + // Depth configuration uniform locations + GLint depthOffsetLoc = -1; + GLint depthScaleLoc = -1; + GLint depthmapEnableLoc = -1; + } ubershaderData; float oldDepthScale = -1.0; float oldDepthOffset = 0.0; @@ -48,6 +81,7 @@ class RendererGL final : public Renderer { SurfaceCache depthBufferCache; SurfaceCache colourBufferCache; SurfaceCache textureCache; + bool usingUbershader = false; // Dummy VAO/VBO for blitting the final output OpenGL::VertexArray dummyVAO; @@ -58,9 +92,11 @@ class RendererGL final : public Renderer { OpenGL::Framebuffer screenFramebuffer; OpenGL::Texture blankTexture; + std::unordered_map shaderCache; + OpenGL::Framebuffer getColourFBO(); OpenGL::Texture getTexture(Texture& tex); - OpenGL::Program getSpecializedShader(); + OpenGL::Program& getSpecializedShader(); PICA::ShaderGen::FragmentGenerator fragShaderGen; @@ -99,4 +135,4 @@ class RendererGL final : public Renderer { // Take a screenshot of the screen and store it in a file void screenshot(const std::string& name) override; -}; +}; \ No newline at end of file diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 3a7e9b74..1bcae30c 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -308,7 +308,7 @@ void FragmentGenerator::getColorOperation(std::string& shader, TexEnvConfig::Ope case TexEnvConfig::Operation::AddSigned: shader += "clamp(colorOp1 + colorOp2 - 0.5, 0.0, 1.0);"; break; case TexEnvConfig::Operation::Subtract: shader += "colorOp1 - colorOp2"; break; case TexEnvConfig::Operation::Modulate: shader += "colorOp1 * colorOp2"; break; - case TexEnvConfig::Operation::Lerp: shader += "colorOp1 * colorOp3 + colorOp2 * (vec(1.0) - colorOp3)"; break; + case TexEnvConfig::Operation::Lerp: shader += "colorOp1 * colorOp3 + colorOp2 * (vec3(1.0) - colorOp3)"; break; case TexEnvConfig::Operation::AddMultiply: shader += "min(colorOp1 + colorOp2, vec3(1.0)) * colorOp3"; break; case TexEnvConfig::Operation::MultiplyAdd: shader += "colorOp1 * colorOp2 + colorOp3"; break; @@ -328,15 +328,15 @@ void FragmentGenerator::getAlphaOperation(std::string& shader, TexEnvConfig::Ope case TexEnvConfig::Operation::AddSigned: shader += "clamp(alphaOp1 + alphaOp2 - 0.5, 0.0, 1.0);"; break; case TexEnvConfig::Operation::Subtract: shader += "alphaOp1 - alphaOp2"; break; case TexEnvConfig::Operation::Modulate: shader += "alphaOp1 * alphaOp2"; break; - case TexEnvConfig::Operation::Lerp: shader += "alphaOp1 * alphaOp3 + alphaOp2 * (vec(1.0) - alphaOp3)"; break; + case TexEnvConfig::Operation::Lerp: shader += "alphaOp1 * alphaOp3 + alphaOp2 * (1.0 - alphaOp3)"; break; - case TexEnvConfig::Operation::AddMultiply: shader += "min(alphaOp1 + alphaOp2, vec3(1.0)) * alphaOp3"; break; + case TexEnvConfig::Operation::AddMultiply: shader += "min(alphaOp1 + alphaOp2, 1.0) * alphaOp3"; break; case TexEnvConfig::Operation::MultiplyAdd: shader += "alphaOp1 * alphaOp2 + alphaOp3"; break; case TexEnvConfig::Operation::Dot3RGB: case TexEnvConfig::Operation::Dot3RGBA: shader += "vec3(4.0 * dot(alphaOp1 - 0.5, alphaOp2 - 0.5))"; break; default: Helpers::warn("FragmentGenerator: Unimplemented alpha op"); - shader += "vec3(1.0)"; + shader += "1.0"; break; } } \ No newline at end of file diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 0bb592cf..a0e09bba 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -38,9 +38,9 @@ void RendererGL::reset() { oldDepthOffset = 0.0; // Default depth offset to 0 oldDepthmapEnable = false; // Enable w buffering - glUniform1f(depthScaleLoc, oldDepthScale); - glUniform1f(depthOffsetLoc, oldDepthOffset); - glUniform1i(depthmapEnableLoc, oldDepthmapEnable); + glUniform1f(ubershaderData.depthScaleLoc, oldDepthScale); + glUniform1f(ubershaderData.depthOffsetLoc, oldDepthOffset); + glUniform1i(ubershaderData.depthmapEnableLoc, oldDepthmapEnable); gl.useProgram(oldProgram); // Switch to old GL program } @@ -59,16 +59,16 @@ void RendererGL::initGraphicsContextInternal() { triangleProgram.create({vert, frag}); gl.useProgram(triangleProgram); - textureEnvSourceLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvSource"); - textureEnvOperandLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvOperand"); - textureEnvCombinerLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvCombiner"); - textureEnvColorLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvColor"); - textureEnvScaleLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvScale"); + ubershaderData.textureEnvSourceLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvSource"); + ubershaderData.textureEnvOperandLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvOperand"); + ubershaderData.textureEnvCombinerLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvCombiner"); + ubershaderData.textureEnvColorLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvColor"); + ubershaderData.textureEnvScaleLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvScale"); - depthScaleLoc = OpenGL::uniformLocation(triangleProgram, "u_depthScale"); - depthOffsetLoc = OpenGL::uniformLocation(triangleProgram, "u_depthOffset"); - depthmapEnableLoc = OpenGL::uniformLocation(triangleProgram, "u_depthmapEnable"); - picaRegLoc = OpenGL::uniformLocation(triangleProgram, "u_picaRegs"); + ubershaderData.depthScaleLoc = OpenGL::uniformLocation(triangleProgram, "u_depthScale"); + ubershaderData.depthOffsetLoc = OpenGL::uniformLocation(triangleProgram, "u_depthOffset"); + ubershaderData.depthmapEnableLoc = OpenGL::uniformLocation(triangleProgram, "u_depthmapEnable"); + ubershaderData.picaRegLoc = OpenGL::uniformLocation(triangleProgram, "u_picaRegs"); // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 glUniform1i(OpenGL::uniformLocation(triangleProgram, "u_tex0"), 0); @@ -289,7 +289,6 @@ void RendererGL::setupStencilTest(bool stencilEnable) { glStencilOp(stencilOps[stencilFailOp], stencilOps[depthFailOp], stencilOps[passOp]); } - void RendererGL::setupTextureEnvState() { // TODO: Only update uniforms when the TEV config changed. Use an UBO potentially. @@ -314,11 +313,11 @@ void RendererGL::setupTextureEnvState() { textureEnvScaleRegs[i] = regs[ioBase + 4]; } - glUniform1uiv(textureEnvSourceLoc, 6, textureEnvSourceRegs); - glUniform1uiv(textureEnvOperandLoc, 6, textureEnvOperandRegs); - glUniform1uiv(textureEnvCombinerLoc, 6, textureEnvCombinerRegs); - glUniform1uiv(textureEnvColorLoc, 6, textureEnvColourRegs); - glUniform1uiv(textureEnvScaleLoc, 6, textureEnvScaleRegs); + glUniform1uiv(ubershaderData.textureEnvSourceLoc, 6, textureEnvSourceRegs); + glUniform1uiv(ubershaderData.textureEnvOperandLoc, 6, textureEnvOperandRegs); + glUniform1uiv(ubershaderData.textureEnvCombinerLoc, 6, textureEnvCombinerRegs); + glUniform1uiv(ubershaderData.textureEnvColorLoc, 6, textureEnvColourRegs); + glUniform1uiv(ubershaderData.textureEnvScaleLoc, 6, textureEnvScaleRegs); } void RendererGL::bindTexturesToSlots() { @@ -389,11 +388,7 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v OpenGL::Triangle, }; - std::string vs = fragShaderGen.getVertexShader(regs); - std::string fs = fragShaderGen.generate(regs); - std::cout << fs << "\n\n\n"; - - OpenGL::Program program = getSpecializedShader(); + OpenGL::Program& program = getSpecializedShader(); const auto primitiveTopology = primTypes[static_cast(primType)]; gl.disableScissor(); @@ -427,17 +422,17 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v // Update depth uniforms if (oldDepthScale != depthScale) { oldDepthScale = depthScale; - glUniform1f(depthScaleLoc, depthScale); + glUniform1f(ubershaderData.depthScaleLoc, depthScale); } if (oldDepthOffset != depthOffset) { oldDepthOffset = depthOffset; - glUniform1f(depthOffsetLoc, depthOffset); + glUniform1f(ubershaderData.depthOffsetLoc, depthOffset); } if (oldDepthmapEnable != depthMapEnable) { oldDepthmapEnable = depthMapEnable; - glUniform1i(depthmapEnableLoc, depthMapEnable); + glUniform1i(ubershaderData.depthmapEnableLoc, depthMapEnable); } setupTextureEnvState(); @@ -445,7 +440,7 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v // Upload PICA Registers as a single uniform. The shader needs access to the rasterizer registers (for depth, starting from index 0x48) // The texturing and the fragment lighting registers. Therefore we upload them all in one go to avoid multiple slow uniform updates - glUniform1uiv(picaRegLoc, 0x200 - 0x48, ®s[0x48]); + glUniform1uiv(ubershaderData.picaRegLoc, 0x200 - 0x48, ®s[0x48]); if (gpu.lightingLUTDirty) { updateLightingLUT(); @@ -784,22 +779,39 @@ std::optional RendererGL::getColourBuffer(u32 addr, PICA::ColorFmt return colourBufferCache.add(sampleBuffer); } -OpenGL::Program RendererGL::getSpecializedShader() { - OpenGL::Program program; +OpenGL::Program& RendererGL::getSpecializedShader() { + PICA::FragmentConfig fsConfig; + fsConfig.texUnitConfig = regs[InternalRegs::TexUnitCfg]; + fsConfig.texEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; + fsConfig.texEnvBufferColor = regs[InternalRegs::TexEnvBufferColor]; - std::string vs = fragShaderGen.getVertexShader(regs); - std::string fs = fragShaderGen.generate(regs); + // Set up TEV stages + std::memcpy(&fsConfig.tevConfigs[0 * 5], ®s[InternalRegs::TexEnv0Source], 5 * sizeof(u32)); + std::memcpy(&fsConfig.tevConfigs[1 * 5], ®s[InternalRegs::TexEnv1Source], 5 * sizeof(u32)); + std::memcpy(&fsConfig.tevConfigs[2 * 5], ®s[InternalRegs::TexEnv2Source], 5 * sizeof(u32)); + std::memcpy(&fsConfig.tevConfigs[3 * 5], ®s[InternalRegs::TexEnv3Source], 5 * sizeof(u32)); + std::memcpy(&fsConfig.tevConfigs[4 * 5], ®s[InternalRegs::TexEnv4Source], 5 * sizeof(u32)); + std::memcpy(&fsConfig.tevConfigs[5 * 5], ®s[InternalRegs::TexEnv5Source], 5 * sizeof(u32)); - OpenGL::Shader vertShader({vs.c_str(), vs.size()}, OpenGL::Vertex); - OpenGL::Shader fragShader({fs.c_str(), fs.size()}, OpenGL::Fragment); - program.create({vertShader, fragShader}); - program.use(); + OpenGL::Program& program = shaderCache[fsConfig]; + if (!program.exists()) { + printf("Creating specialized shader\n"); - // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 - glUniform1i(OpenGL::uniformLocation(program, "u_tex0"), 0); - glUniform1i(OpenGL::uniformLocation(program, "u_tex1"), 1); - glUniform1i(OpenGL::uniformLocation(program, "u_tex2"), 2); - glUniform1i(OpenGL::uniformLocation(program, "u_tex_lighting_lut"), 3); + std::string vs = fragShaderGen.getVertexShader(regs); + std::string fs = fragShaderGen.generate(regs); + std::cout << vs << "\n\n" << fs << "\n"; + + OpenGL::Shader vertShader({vs.c_str(), vs.size()}, OpenGL::Vertex); + OpenGL::Shader fragShader({fs.c_str(), fs.size()}, OpenGL::Fragment); + program.create({vertShader, fragShader}); + program.use(); + + // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 + glUniform1i(OpenGL::uniformLocation(program, "u_tex0"), 0); + glUniform1i(OpenGL::uniformLocation(program, "u_tex1"), 1); + glUniform1i(OpenGL::uniformLocation(program, "u_tex2"), 2); + glUniform1i(OpenGL::uniformLocation(program, "u_tex_lighting_lut"), 3); + } return program; } From fc83d518e23796e8008cef7bbfa047dd14608ff1 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 2 Mar 2024 22:35:56 +0200 Subject: [PATCH 011/251] Hook up specialized shaders to GL renderer --- src/core/renderer_gl/renderer_gl.cpp | 43 +++++++++++++++++----------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index a0e09bba..4aa65586 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -291,6 +291,9 @@ void RendererGL::setupStencilTest(bool stencilEnable) { void RendererGL::setupTextureEnvState() { // TODO: Only update uniforms when the TEV config changed. Use an UBO potentially. + if (!usingUbershader) { + return; + } static constexpr std::array ioBases = { PICA::InternalRegs::TexEnv0Source, PICA::InternalRegs::TexEnv1Source, PICA::InternalRegs::TexEnv2Source, @@ -388,13 +391,17 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v OpenGL::Triangle, }; - OpenGL::Program& program = getSpecializedShader(); + if (usingUbershader) { + gl.useProgram(triangleProgram); + } else { + OpenGL::Program& program = getSpecializedShader(); + gl.useProgram(program); + } const auto primitiveTopology = primTypes[static_cast(primType)]; gl.disableScissor(); gl.bindVBO(vbo); gl.bindVAO(vao); - gl.useProgram(triangleProgram); gl.enableClipPlane(0); // Clipping plane 0 is always enabled if (regs[PICA::InternalRegs::ClipEnable] & 1) { @@ -420,27 +427,31 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v const bool depthMapEnable = regs[PICA::InternalRegs::DepthmapEnable] & 1; // Update depth uniforms - if (oldDepthScale != depthScale) { - oldDepthScale = depthScale; - glUniform1f(ubershaderData.depthScaleLoc, depthScale); - } + if (usingUbershader) { + if (oldDepthScale != depthScale) { + oldDepthScale = depthScale; + glUniform1f(ubershaderData.depthScaleLoc, depthScale); + } - if (oldDepthOffset != depthOffset) { - oldDepthOffset = depthOffset; - glUniform1f(ubershaderData.depthOffsetLoc, depthOffset); - } + if (oldDepthOffset != depthOffset) { + oldDepthOffset = depthOffset; + glUniform1f(ubershaderData.depthOffsetLoc, depthOffset); + } - if (oldDepthmapEnable != depthMapEnable) { - oldDepthmapEnable = depthMapEnable; - glUniform1i(ubershaderData.depthmapEnableLoc, depthMapEnable); + if (oldDepthmapEnable != depthMapEnable) { + oldDepthmapEnable = depthMapEnable; + glUniform1i(ubershaderData.depthmapEnableLoc, depthMapEnable); + } } setupTextureEnvState(); bindTexturesToSlots(); - // Upload PICA Registers as a single uniform. The shader needs access to the rasterizer registers (for depth, starting from index 0x48) - // The texturing and the fragment lighting registers. Therefore we upload them all in one go to avoid multiple slow uniform updates - glUniform1uiv(ubershaderData.picaRegLoc, 0x200 - 0x48, ®s[0x48]); + if (usingUbershader) { + // Upload PICA Registers as a single uniform. The shader needs access to the rasterizer registers (for depth, starting from index 0x48) + // The texturing and the fragment lighting registers. Therefore we upload them all in one go to avoid multiple slow uniform updates + glUniform1uiv(ubershaderData.picaRegLoc, 0x200 - 0x48, ®s[0x48]); + } if (gpu.lightingLUTDirty) { updateLightingLUT(); From e5c09a092d76fb7339bd4202ba67630e18c0d775 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 2 Mar 2024 23:29:22 +0200 Subject: [PATCH 012/251] Fix specialized shaders on Android --- include/PICA/shader_gen.hpp | 5 +++++ src/core/renderer_gl/renderer_gl.cpp | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index e07575a5..23a87120 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -30,5 +30,10 @@ namespace PICA::ShaderGen { FragmentGenerator(API api, Language language) : api(api), language(language) {} std::string generate(const PICARegs& regs); std::string getVertexShader(const PICARegs& regs); + + void setTarget(API api, Language language) { + this->api = api; + this->language = language; + } }; }; // namespace PICA::ShaderGen \ No newline at end of file diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 4aa65586..8119cff5 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -44,6 +44,10 @@ void RendererGL::reset() { gl.useProgram(oldProgram); // Switch to old GL program } + +#ifdef __ANDROID__ + fragShaderGen.setTarget(PICA::ShaderGen::API::GLES, PICA::ShaderGen::Language::GLSL); +#endif } void RendererGL::initGraphicsContextInternal() { From 4b07ebed863fa1f23d060f8cbb3b8bdc41683854 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 3 Mar 2024 01:51:45 +0200 Subject: [PATCH 013/251] Fix shader cache bypassing GL state manager --- src/core/renderer_gl/renderer_gl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 8119cff5..5d3ed1b1 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -819,7 +819,7 @@ OpenGL::Program& RendererGL::getSpecializedShader() { OpenGL::Shader vertShader({vs.c_str(), vs.size()}, OpenGL::Vertex); OpenGL::Shader fragShader({fs.c_str(), fs.size()}, OpenGL::Fragment); program.create({vertShader, fragShader}); - program.use(); + gl.useProgram(program); // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 glUniform1i(OpenGL::uniformLocation(program, "u_tex0"), 0); From 5ba773a393a599897af821d8b1ef1fb4cfa85318 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 3 Mar 2024 02:43:41 +0200 Subject: [PATCH 014/251] Add GLES detection to fragment shader recompiler --- src/core/PICA/shader_gen_glsl.cpp | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 1bcae30c..c3056815 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -11,6 +11,15 @@ std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { default: break; } + if (api == API::GLES) { + ret += R"( + #define USING_GLES 1 + + precision mediump int; + precision mediump float; + )"; + } + ret += R"( layout(location = 0) in vec4 a_coords; layout(location = 1) in vec4 a_quaternion; @@ -75,6 +84,14 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { } bool unimplementedFlag = false; + if (api == API::GLES) { + ret += R"( + #define USING_GLES 1 + + precision mediump int; + precision mediump float; + )"; + } // Input and output attributes ret += R"( @@ -93,17 +110,13 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { uniform sampler2D u_tex0; uniform sampler2D u_tex1; uniform sampler2D u_tex2; + // GLES doesn't support sampler1DArray, as such we'll have to change how we handle lighting later +#ifndef USING_GLES uniform sampler1DArray u_tex_lighting_lut; +#endif vec4 tevSources[16]; vec4 tevNextPreviousBuffer; - - vec3 regToColor(uint reg) { - // Normalization scale to convert from [0...255] to [0.0...1.0] - const float scale = 1.0 / 255.0; - - return scale * vec3(float(bitfieldExtract(reg, 20, 8)), float(bitfieldExtract(reg, 10, 8)), float(bitfieldExtract(reg, 00, 8))); - } )"; // Emit main function for fragment shader @@ -339,4 +352,4 @@ void FragmentGenerator::getAlphaOperation(std::string& shader, TexEnvConfig::Ope shader += "1.0"; break; } -} \ No newline at end of file +} From 2fc9c0a5737262fd78ebc29cb054ba98d1c35c6f Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 28 Apr 2024 16:58:42 +0300 Subject: [PATCH 015/251] DSP HLE: Broken PCM16 and handle DSP voice status better --- include/audio/dsp_shared_mem.hpp | 2 +- include/audio/hle_core.hpp | 25 ++++-- src/core/audio/hle_core.cpp | 136 +++++++++++++++++++++++++++---- 3 files changed, 138 insertions(+), 25 deletions(-) diff --git a/include/audio/dsp_shared_mem.hpp b/include/audio/dsp_shared_mem.hpp index 148986f9..25806ea1 100644 --- a/include/audio/dsp_shared_mem.hpp +++ b/include/audio/dsp_shared_mem.hpp @@ -297,7 +297,7 @@ namespace Audio::HLE { u8 isEnabled; ///< Is this channel enabled? (Doesn't have to be playing anything.) u8 currentBufferIDDirty; ///< Non-zero when current_buffer_id changes u16_le syncCount; ///< Is set by the DSP to the value of SourceConfiguration::sync_count - u32_dsp bufferPosition; ///< Number of samples into the current buffer + u32_dsp samplePosition; ///< Number of samples into the current buffer u16_le currentBufferID; ///< Updated when a buffer finishes playing u16_le lastBufferID; ///< Updated when all buffers in the queue finish playing }; diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index 257ab5ac..cee2b0c8 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -32,8 +32,8 @@ namespace Audio { SampleFormat format; SourceType sourceType; - bool fromQueue = false; // Is this buffer from the buffer queue or an embedded buffer? - bool hasPlayedOnce = false; // Has the buffer been played at least once before? + bool fromQueue = false; // Is this buffer from the buffer queue or an embedded buffer? + bool hasPlayedOnce = false; // Has the buffer been played at least once before? bool operator<(const Buffer& other) const { // Lower ID = Higher priority @@ -47,9 +47,17 @@ namespace Audio { using BufferQueue = std::priority_queue; BufferQueue buffers; + SampleFormat sampleFormat = SampleFormat::ADPCM; + SourceType sourceType = SourceType::Stereo; + std::array gain0, gain1, gain2; + u32 samplePosition; // Sample number into the current audio buffer u16 syncCount; - bool enabled; // Is the source enabled? + u16 currentBufferID; + u16 previousBufferID; + + bool enabled; // Is the source enabled? + bool isBufferIDDirty = false; // Did we change buffers? // ADPCM decoding info: // An array of fixed point S5.11 coefficients. These provide "weights" for the history samples @@ -65,6 +73,10 @@ namespace Audio { int index = 0; // Index of the voice in [0, 23] for debugging void reset(); + + // Push a buffer to the buffer queue + void pushBuffer(const Buffer& buffer) { buffers.push(buffer); } + // Pop a buffer from the buffer queue and return it Buffer popBuffer() { assert(!buffers.empty()); @@ -114,9 +126,6 @@ namespace Audio { std::array sources; // DSP voices Audio::HLE::DspMemory dspRam; - SampleFormat sampleFormat = SampleFormat::ADPCM; - SourceType sourceType = SourceType::Stereo; - void resetAudioPipe(); bool loaded = false; // Have we loaded a component? @@ -159,9 +168,13 @@ namespace Audio { void updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config, s16_le* adpcmCoefficients); void generateFrame(StereoFrame& frame); + void generateFrame(DSPSource& source); void outputFrame(); + // Decode an entire buffer worth of audio void decodeBuffer(DSPSource& source); + + SampleBuffer decodePCM16(const u8* data, usize sampleCount, Source& source); SampleBuffer decodeADPCM(const u8* data, usize sampleCount, Source& source); public: diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 4ee5a1dc..e92432b5 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -7,6 +7,8 @@ #include "services/dsp.hpp" +std::vector> samplezorz = {}; + namespace Audio { namespace DSPPipeType { enum : u32 { @@ -64,10 +66,6 @@ namespace Audio { dspState = DSPState::Off; loaded = false; - // Initialize these to some sane defaults - sampleFormat = SampleFormat::ADPCM; - sourceType = SourceType::Stereo; - for (auto& e : pipeData) { e.clear(); } @@ -212,12 +210,16 @@ namespace Audio { updateSourceConfig(source, config, read.adpcmCoefficients.coeff[i]); // Generate audio - if (source.enabled && !source.buffers.empty()) { - const auto& buffer = source.buffers.top(); - const u8* data = getPointerPhys(buffer.paddr); + if (source.enabled) { + generateFrame(source); - if (data != nullptr) { - // TODO + if (samplezorz.size() > 160 * 60 * 60 * 3) { + using namespace std; + ofstream fout("audio_data.bin", ios::out | ios::binary); + fout.write((char*)&samplezorz[0], samplezorz.size() * sizeof(Sample)); + fout.close(); + + Helpers::panic("Bwaa"); } } @@ -225,6 +227,13 @@ namespace Audio { auto& status = write.sourceStatuses.status[i]; status.isEnabled = source.enabled; status.syncCount = source.syncCount; + status.currentBufferIDDirty = source.isBufferIDDirty ? 1 : 0; + status.currentBufferID = source.currentBufferID; + status.lastBufferID = source.previousBufferID; + // TODO: Properly update sample position + status.samplePosition = source.samplePosition; + + source.isBufferIDDirty = false; } } @@ -265,11 +274,11 @@ namespace Audio { // TODO: Should we check bufferQueueDirty here too? if (config.formatDirty || config.embeddedBufferDirty) { - sampleFormat = config.format; + source.sampleFormat = config.format; } if (config.monoOrStereoDirty || config.embeddedBufferDirty) { - sourceType = config.monoOrStereo; + source.sourceType = config.monoOrStereo; } if (config.embeddedBufferDirty) { @@ -285,8 +294,8 @@ namespace Audio { .looping = config.isLooping != 0, .bufferID = config.bufferID, .playPosition = config.playPosition, - .format = sampleFormat, - .sourceType = sourceType, + .format = source.sampleFormat, + .sourceType = source.sourceType, .fromQueue = false, .hasPlayedOnce = false, }; @@ -327,13 +336,95 @@ namespace Audio { return; } - switch (buffer.format) { - case SampleFormat::PCM8: - case SampleFormat::PCM16: Helpers::warn("Unimplemented sample format!"); break; + source.currentBufferID = buffer.bufferID; + source.previousBufferID = 0; + // For looping buffers, this is only set for the first time we play it. Loops do not set the dirty bit. + source.isBufferIDDirty = !buffer.hasPlayedOnce && buffer.fromQueue; - case SampleFormat::ADPCM: source.currentSamples = decodeADPCM(data, buffer.sampleCount, source); break; - default: Helpers::warn("Invalid DSP sample format"); break; + if (buffer.hasPlayedOnce) { + source.samplePosition = 0; + } else { + // Mark that the buffer has already been played once, needed for looping buffers + buffer.hasPlayedOnce = true; + // Play position is only used for the initial time the buffer is played. Loops will start from the beginning of the buffer. + source.samplePosition = buffer.playPosition; } + + switch (buffer.format) { + case SampleFormat::PCM8: Helpers::warn("Unimplemented sample format!"); break; + case SampleFormat::PCM16: source.currentSamples = decodePCM16(data, buffer.sampleCount, source); break; + case SampleFormat::ADPCM: source.currentSamples = decodeADPCM(data, buffer.sampleCount, source); break; + + default: + Helpers::warn("Invalid DSP sample format"); + source.currentSamples = {}; + break; + } + + // If the buffer is a looping buffer, re-push it + if (buffer.looping) { + source.pushBuffer(buffer); + } + } + + void HLE_DSP::generateFrame(DSPSource& source) { + if (source.currentSamples.empty()) { + // There's no audio left to play, turn the voice off + if (source.buffers.empty()) { + source.enabled = false; + source.isBufferIDDirty = true; + source.previousBufferID = source.currentBufferID; + source.currentBufferID = 0; + + return; + } + + decodeBuffer(source); + } else { + constexpr uint maxSampleCount = Audio::samplesInFrame; + uint outputCount = 0; + + while (outputCount < maxSampleCount) { + if (source.currentSamples.empty()) { + if (source.buffers.empty()) { + break; + } else { + decodeBuffer(source); + } + } + + const uint sampleCount = std::min(maxSampleCount - outputCount, source.currentSamples.size()); + samplezorz.insert(samplezorz.end(), source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); + source.currentSamples.erase(source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); + + outputCount += sampleCount; + } + } + } + + HLE_DSP::SampleBuffer HLE_DSP::decodePCM16(const u8* data, usize sampleCount, Source& source) { + SampleBuffer decodedSamples(sampleCount); + const s16* data16 = reinterpret_cast(data); + + if (source.sourceType == SourceType::Stereo) { + for (usize i = 0; i < sampleCount; i++) { + s16 left = *data16++; + s16 right = *data16++; + + if (left != 0 || right != 0) { + Helpers::panic("panda..."); + } + + decodedSamples[i] = {left, right}; + } + } else { + // Mono + for (usize i = 0; i < sampleCount; i++) { + decodedSamples[i].fill(*data16++); + } + } + + return decodedSamples; } HLE_DSP::SampleBuffer HLE_DSP::decodeADPCM(const u8* data, usize sampleCount, Source& source) { @@ -413,6 +504,15 @@ namespace Audio { void DSPSource::reset() { enabled = false; + isBufferIDDirty = false; + + // Initialize these to some sane defaults + sampleFormat = SampleFormat::ADPCM; + sourceType = SourceType::Stereo; + + samplePosition = 0; + previousBufferID = 0; + currentBufferID = 0; syncCount = 0; buffers = {}; From fb8130a868897627f2d22ce270a454d434d67e8a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 30 Apr 2024 21:56:39 +0300 Subject: [PATCH 016/251] HLE DSP: Remove debug artifacts --- src/core/audio/hle_core.cpp | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index e92432b5..d6ba21ec 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -7,8 +7,6 @@ #include "services/dsp.hpp" -std::vector> samplezorz = {}; - namespace Audio { namespace DSPPipeType { enum : u32 { @@ -102,6 +100,7 @@ namespace Audio { dspService.triggerPipeEvent(DSPPipeType::Audio); } + // TODO: Should this be called if dspState != DSPState::On? outputFrame(); scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::cyclesPerFrame); } @@ -212,15 +211,6 @@ namespace Audio { // Generate audio if (source.enabled) { generateFrame(source); - - if (samplezorz.size() > 160 * 60 * 60 * 3) { - using namespace std; - ofstream fout("audio_data.bin", ios::out | ios::binary); - fout.write((char*)&samplezorz[0], samplezorz.size() * sizeof(Sample)); - fout.close(); - - Helpers::panic("Bwaa"); - } } // Update write region of shared memory @@ -394,7 +384,7 @@ namespace Audio { } const uint sampleCount = std::min(maxSampleCount - outputCount, source.currentSamples.size()); - samplezorz.insert(samplezorz.end(), source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); + // samples.insert(samples.end(), source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); source.currentSamples.erase(source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); outputCount += sampleCount; @@ -408,19 +398,15 @@ namespace Audio { if (source.sourceType == SourceType::Stereo) { for (usize i = 0; i < sampleCount; i++) { - s16 left = *data16++; - s16 right = *data16++; - - if (left != 0 || right != 0) { - Helpers::panic("panda..."); - } - + const s16 left = *data16++; + const s16 right = *data16++; decodedSamples[i] = {left, right}; } } else { // Mono for (usize i = 0; i < sampleCount; i++) { - decodedSamples[i].fill(*data16++); + const s16 sample = *data16++; + decodedSamples[i] = {sample, sample}; } } From 0490c6753fb4c56fc4b9835b504618e6d8101f18 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 1 May 2024 01:56:17 +0300 Subject: [PATCH 017/251] HLE DSP: Stub AAC --- CMakeLists.txt | 2 +- include/audio/aac.hpp | 71 +++++++++++++++++++++++++++++++++++++ include/audio/hle_core.hpp | 2 ++ src/core/audio/hle_core.cpp | 47 ++++++++++++++++++++++-- 4 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 include/audio/aac.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6d3901b6..48a2a0db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -241,7 +241,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/scheduler.hpp include/applets/error_applet.hpp include/audio/dsp_core.hpp include/audio/null_core.hpp include/audio/teakra_core.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp - include/audio/hle_core.hpp include/capstone.hpp + include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp ) cmrc_add_resource_library( diff --git a/include/audio/aac.hpp b/include/audio/aac.hpp new file mode 100644 index 00000000..c780e6d2 --- /dev/null +++ b/include/audio/aac.hpp @@ -0,0 +1,71 @@ +#pragma once +#include +#include + +#include "helpers.hpp" +#include "swap.hpp" + +namespace Audio::AAC { + namespace ResultCode { + enum : u32 { + Success = 0, + }; + } + + // Enum values from Citra and struct definitions based off Citra + namespace Command { + enum : u16 { + Init = 0, // Initialize encoder/decoder + EncodeDecode = 1, // Encode/Decode AAC + Shutdown = 2, // Shutdown encoder/decoder + LoadState = 3, + SaveState = 4, + }; + } + + namespace SampleRate { + enum : u32 { + Rate48000 = 0, + Rate44100 = 1, + Rate32000 = 2, + Rate24000 = 3, + Rate22050 = 4, + Rate16000 = 5, + Rate12000 = 6, + Rate11025 = 7, + Rate8000 = 8, + }; + } + + namespace Mode { + enum : u16 { + None = 0, + Decode = 1, + Encode = 2, + }; + } + + struct DecodeResponse { + u32_le sampleRate = SampleRate::Rate48000; + u32_le channelCount = 0; + u32_le size = 0; + u32_le unknown1 = 0; + u32_le unknown2 = 0; + u32_le sampleCount = 0; + }; + + struct Message { + u16_le mode = Mode::None; // Encode or decode AAC? + u16_le command = Command::Init; + u32_le resultCode = ResultCode::Success; + + // Info on the AAC request + union { + std::array commandData = {}; + DecodeResponse decodeResponse; + }; + }; + + static_assert(sizeof(Message) == 32); + static_assert(std::is_trivially_copyable()); +} // namespace Audio::AAC \ No newline at end of file diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index cee2b0c8..c57f221e 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -5,6 +5,7 @@ #include #include +#include "audio/aac.hpp" #include "audio/dsp_core.hpp" #include "audio/dsp_shared_mem.hpp" #include "memory.hpp" @@ -166,6 +167,7 @@ namespace Audio { } } + void handleAACRequest(const AAC::Message& request); void updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config, s16_le* adpcmCoefficients); void generateFrame(StereoFrame& frame); void generateFrame(DSPSource& source); diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index d6ba21ec..98d07ce6 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -149,12 +149,24 @@ namespace Audio { break; } - case DSPPipeType::Binary: - Helpers::warn("Unimplemented write to binary pipe! Size: %d\n", size); + case DSPPipeType::Binary: { + log("Unimplemented write to binary pipe! Size: %d\n", size); + + AAC::Message request; + if (size == sizeof(request)) { + std::array raw; + for (uint i = 0; i < size; i++) { + raw[i] = mem.read32(buffer + i); + } + + std::memcpy(&request, raw.data(), sizeof(request)); + handleAACRequest(request); + } // This pipe and interrupt are normally used for requests like AAC decode dspService.triggerPipeEvent(DSPPipeType::Binary); break; + } default: log("Audio::HLE_DSP: Wrote to unimplemented pipe %d\n", channel); break; } @@ -488,6 +500,37 @@ namespace Audio { return decodedSamples; } + void HLE_DSP::handleAACRequest(const AAC::Message& request) { + AAC::Message response = {}; + + switch (request.command) { + case AAC::Command::EncodeDecode: + // Dummy response to stop games from hanging + // TODO: Fix this when implementing AAC + response.resultCode = AAC::ResultCode::Success; + response.decodeResponse.channelCount = 2; + response.decodeResponse.sampleCount = 1024; + response.decodeResponse.size = 0; + response.decodeResponse.sampleRate = AAC::SampleRate::Rate48000; + break; + + case AAC::Command::Init: + case AAC::Command::Shutdown: + case AAC::Command::LoadState: + case AAC::Command::SaveState: + response = request; + response.resultCode = AAC::ResultCode::Success; + break; + + default: Helpers::warn("Unknown AAC command type"); break; + } + + // Copy response data to the binary pipe + auto& pipe = pipeData[DSPPipeType::Binary]; + pipe.resize(sizeof(response)); + std::memcpy(&pipe[0], &response, sizeof(response)); + } + void DSPSource::reset() { enabled = false; isBufferIDDirty = false; From ad380b8c5ae9bb8e9a2ce5bdd470a31f04385f20 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 1 May 2024 01:59:32 +0300 Subject: [PATCH 018/251] Warn on invalid AAC request --- src/core/audio/hle_core.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 98d07ce6..e38d4821 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -161,6 +161,8 @@ namespace Audio { std::memcpy(&request, raw.data(), sizeof(request)); handleAACRequest(request); + } else { + Helpers::warn("Invalid size for AAC request"); } // This pipe and interrupt are normally used for requests like AAC decode From e4b81d61a46816116808d868eb3d39b96313acb4 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 1 May 2024 16:10:51 +0300 Subject: [PATCH 019/251] HLE DSP: Fix AAC response stub --- include/audio/dsp_shared_mem.hpp | 4 ++-- src/core/audio/hle_core.cpp | 9 ++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/include/audio/dsp_shared_mem.hpp b/include/audio/dsp_shared_mem.hpp index 25806ea1..e776211d 100644 --- a/include/audio/dsp_shared_mem.hpp +++ b/include/audio/dsp_shared_mem.hpp @@ -294,12 +294,12 @@ namespace Audio::HLE { struct SourceStatus { struct Status { - u8 isEnabled; ///< Is this channel enabled? (Doesn't have to be playing anything.) + u8 enabled; ///< Is this channel enabled? (Doesn't have to be playing anything.) u8 currentBufferIDDirty; ///< Non-zero when current_buffer_id changes u16_le syncCount; ///< Is set by the DSP to the value of SourceConfiguration::sync_count u32_dsp samplePosition; ///< Number of samples into the current buffer u16_le currentBufferID; ///< Updated when a buffer finishes playing - u16_le lastBufferID; ///< Updated when all buffers in the queue finish playing + u16_le previousBufferID; ///< Updated when all buffers in the queue finish playing }; Status status[sourceCount]; diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index e38d4821..146c7bdf 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -229,11 +229,11 @@ namespace Audio { // Update write region of shared memory auto& status = write.sourceStatuses.status[i]; - status.isEnabled = source.enabled; + status.enabled = source.enabled; status.syncCount = source.syncCount; status.currentBufferIDDirty = source.isBufferIDDirty ? 1 : 0; status.currentBufferID = source.currentBufferID; - status.lastBufferID = source.previousBufferID; + status.previousBufferID = source.previousBufferID; // TODO: Properly update sample position status.samplePosition = source.samplePosition; @@ -503,7 +503,7 @@ namespace Audio { } void HLE_DSP::handleAACRequest(const AAC::Message& request) { - AAC::Message response = {}; + AAC::Message response; switch (request.command) { case AAC::Command::EncodeDecode: @@ -514,6 +514,9 @@ namespace Audio { response.decodeResponse.sampleCount = 1024; response.decodeResponse.size = 0; response.decodeResponse.sampleRate = AAC::SampleRate::Rate48000; + + response.command = request.command; + response.mode = request.mode; break; case AAC::Command::Init: From 6a424a7a66bac534800746cd90b2d7d26786bf86 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 1 May 2024 16:20:24 +0300 Subject: [PATCH 020/251] Fix CI --- include/audio/aac.hpp | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/include/audio/aac.hpp b/include/audio/aac.hpp index c780e6d2..afd2dbba 100644 --- a/include/audio/aac.hpp +++ b/include/audio/aac.hpp @@ -12,7 +12,7 @@ namespace Audio::AAC { }; } - // Enum values from Citra and struct definitions based off Citra + // Enum values and struct definitions based off Citra namespace Command { enum : u16 { Init = 0, // Initialize encoder/decoder @@ -46,12 +46,12 @@ namespace Audio::AAC { } struct DecodeResponse { - u32_le sampleRate = SampleRate::Rate48000; - u32_le channelCount = 0; - u32_le size = 0; - u32_le unknown1 = 0; - u32_le unknown2 = 0; - u32_le sampleCount = 0; + u32_le sampleRate; + u32_le channelCount; + u32_le size; + u32_le unknown1; + u32_le unknown2; + u32_le sampleCount; }; struct Message { @@ -61,11 +61,11 @@ namespace Audio::AAC { // Info on the AAC request union { - std::array commandData = {}; + std::array commandData{}; DecodeResponse decodeResponse; }; }; static_assert(sizeof(Message) == 32); static_assert(std::is_trivially_copyable()); -} // namespace Audio::AAC \ No newline at end of file +} // namespace Audio::AAC From 70f733ffb8437b51cfce6ad386a46d5bddfb6e7f Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 2 May 2024 00:22:13 +0300 Subject: [PATCH 021/251] GPU: Handle invalid floating point uniform writes --- include/PICA/shader.hpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/include/PICA/shader.hpp b/include/PICA/shader.hpp index 5b05e0b7..a9216b17 100644 --- a/include/PICA/shader.hpp +++ b/include/PICA/shader.hpp @@ -256,8 +256,10 @@ class PICAShader { void uploadFloatUniform(u32 word) { floatUniformBuffer[floatUniformWordCount++] = word; - if (floatUniformIndex >= 96) { - Helpers::panic("[PICA] Tried to write float uniform %d", floatUniformIndex); + // Check if the program tries to upload to a non-existent uniform, and empty the queue without writing in that case + if (floatUniformIndex >= 96) [[unlikely]] { + floatUniformWordCount = 0; + return; } if ((f32UniformTransfer && floatUniformWordCount >= 4) || (!f32UniformTransfer && floatUniformWordCount >= 3)) { From 81932421cfe08cf0994bfa3a5dccb909e562a4cd Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 2 May 2024 00:28:13 +0300 Subject: [PATCH 022/251] Optimize float uniform setting --- include/PICA/shader.hpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/include/PICA/shader.hpp b/include/PICA/shader.hpp index a9216b17..10f6ec88 100644 --- a/include/PICA/shader.hpp +++ b/include/PICA/shader.hpp @@ -256,16 +256,16 @@ class PICAShader { void uploadFloatUniform(u32 word) { floatUniformBuffer[floatUniformWordCount++] = word; - // Check if the program tries to upload to a non-existent uniform, and empty the queue without writing in that case - if (floatUniformIndex >= 96) [[unlikely]] { - floatUniformWordCount = 0; - return; - } if ((f32UniformTransfer && floatUniformWordCount >= 4) || (!f32UniformTransfer && floatUniformWordCount >= 3)) { - vec4f& uniform = floatUniforms[floatUniformIndex++]; floatUniformWordCount = 0; + // Check if the program tries to upload to a non-existent uniform, and empty the queue without writing in that case + if (floatUniformIndex >= 96) [[unlikely]] { + return; + } + vec4f& uniform = floatUniforms[floatUniformIndex++]; + if (f32UniformTransfer) { uniform[0] = f24::fromFloat32(*(float*)&floatUniformBuffer[3]); uniform[1] = f24::fromFloat32(*(float*)&floatUniformBuffer[2]); From 66bcf384f38d3c86052a45f824f9854359229c76 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 7 May 2024 23:08:24 +0300 Subject: [PATCH 023/251] Qt: Add file patcher --- .gitmodules | 3 + CMakeLists.txt | 5 +- include/panda_qt/ellided_label.hpp | 21 +++++ include/panda_qt/main_window.hpp | 5 +- include/panda_qt/patch_window.hpp | 21 +++++ src/panda_qt/ellided_label.cpp | 25 ++++++ src/panda_qt/main_window.cpp | 4 + src/panda_qt/patch_window.cpp | 123 +++++++++++++++++++++++++++++ third_party/hips | 1 + 9 files changed, 206 insertions(+), 2 deletions(-) create mode 100644 include/panda_qt/ellided_label.hpp create mode 100644 include/panda_qt/patch_window.hpp create mode 100644 src/panda_qt/ellided_label.cpp create mode 100644 src/panda_qt/patch_window.cpp create mode 160000 third_party/hips diff --git a/.gitmodules b/.gitmodules index 1f1d11fc..5a136acb 100644 --- a/.gitmodules +++ b/.gitmodules @@ -70,3 +70,6 @@ [submodule "third_party/capstone"] path = third_party/capstone url = https://github.com/capstone-engine/capstone +[submodule "third_party/hips"] + path = third_party/hips + url = https://github.com/wheremyfoodat/Hips diff --git a/CMakeLists.txt b/CMakeLists.txt index 48a2a0db..dca69d6b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ include_directories(${PROJECT_SOURCE_DIR}/include/kernel) include_directories(${FMT_INCLUDE_DIR}) include_directories(third_party/boost/) include_directories(third_party/elfio/) +include_directories(third_party/hips/include/) include_directories(third_party/imgui/) include_directories(third_party/dynarmic/src) include_directories(third_party/cryptopp/) @@ -448,9 +449,11 @@ if(NOT BUILD_HYDRA_CORE) set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp src/panda_qt/mappings.cpp - ) + src/panda_qt/patch_window.cpp src/panda_qt/ellided_label.cpp + ) set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp + include/panda_qt/patch_window.hpp include/panda_qt/ellided_label.hpp ) source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) diff --git a/include/panda_qt/ellided_label.hpp b/include/panda_qt/ellided_label.hpp new file mode 100644 index 00000000..19fd8c74 --- /dev/null +++ b/include/panda_qt/ellided_label.hpp @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include +#include + +class EllidedLabel : public QLabel { + Q_OBJECT + public: + explicit EllidedLabel(Qt::TextElideMode elideMode = Qt::ElideLeft, QWidget* parent = nullptr); + explicit EllidedLabel(QString text, Qt::TextElideMode elideMode = Qt::ElideLeft, QWidget* parent = nullptr); + void setText(QString text); + + protected: + void resizeEvent(QResizeEvent* event); + + private: + void updateText(); + QString m_text; + Qt::TextElideMode m_elideMode; +}; \ No newline at end of file diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index 7e93bdf6..72725257 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -17,6 +17,7 @@ #include "panda_qt/about_window.hpp" #include "panda_qt/cheats_window.hpp" #include "panda_qt/config_window.hpp" +#include "panda_qt/patch_window.hpp" #include "panda_qt/screen.hpp" #include "panda_qt/text_editor.hpp" #include "services/hid.hpp" @@ -90,13 +91,14 @@ class MainWindow : public QMainWindow { std::mutex messageQueueMutex; std::vector messageQueue; + QMenuBar* menuBar = nullptr; InputMappings keyboardMappings; ScreenWidget screen; AboutWindow* aboutWindow; ConfigWindow* configWindow; CheatsWindow* cheatsEditor; TextEditorWindow* luaEditor; - QMenuBar* menuBar = nullptr; + PatchWindow* patchWindow; // We use SDL's game controller API since it's the sanest API that supports as many controllers as possible SDL_GameController* gameController = nullptr; @@ -110,6 +112,7 @@ class MainWindow : public QMainWindow { void dumpRomFS(); void openLuaEditor(); void openCheatsEditor(); + void openPatchWindow(); void showAboutMenu(); void initControllers(); void pollControllers(); diff --git a/include/panda_qt/patch_window.hpp b/include/panda_qt/patch_window.hpp new file mode 100644 index 00000000..652c9a23 --- /dev/null +++ b/include/panda_qt/patch_window.hpp @@ -0,0 +1,21 @@ +#pragma once +#include +#include +#include + +#include "panda_qt/ellided_label.hpp" + +class PatchWindow final : public QWidget { + Q_OBJECT + + public: + PatchWindow(QWidget* parent = nullptr); + ~PatchWindow() = default; + + private: + std::filesystem::path inputPath = ""; + std::filesystem::path patchPath = ""; + + EllidedLabel* inputPathLabel = nullptr; + EllidedLabel* patchPathLabel = nullptr; +}; diff --git a/src/panda_qt/ellided_label.cpp b/src/panda_qt/ellided_label.cpp new file mode 100644 index 00000000..68c0da76 --- /dev/null +++ b/src/panda_qt/ellided_label.cpp @@ -0,0 +1,25 @@ +#include "panda_qt/ellided_label.hpp" + +// Based on https://stackoverflow.com/questions/7381100/text-overflow-for-a-qlabel-s-text-rendering-in-qt +EllidedLabel::EllidedLabel(Qt::TextElideMode elideMode, QWidget* parent) : EllidedLabel("", elideMode, parent) {} + +EllidedLabel::EllidedLabel(QString text, Qt::TextElideMode elideMode, QWidget* parent) : QLabel(parent) { + m_elideMode = elideMode; + setText(text); +} + +void EllidedLabel::setText(QString text) { + m_text = text; + updateText(); +} + +void EllidedLabel::resizeEvent(QResizeEvent* event) { + QLabel::resizeEvent(event); + updateText(); +} + +void EllidedLabel::updateText() { + QFontMetrics metrics(font()); + QString elided = metrics.elidedText(m_text, m_elideMode, width()); + QLabel::setText(elided); +} \ No newline at end of file diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index da9d2706..54e4fabe 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -54,11 +54,13 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) auto dumpRomFSAction = toolsMenu->addAction(tr("Dump RomFS")); auto luaEditorAction = toolsMenu->addAction(tr("Open Lua Editor")); auto cheatsEditorAction = toolsMenu->addAction(tr("Open Cheats Editor")); + auto patchWindowAction = toolsMenu->addAction(tr("Open Patch Window")); auto dumpDspFirmware = toolsMenu->addAction(tr("Dump loaded DSP firmware")); connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS); connect(luaEditorAction, &QAction::triggered, this, &MainWindow::openLuaEditor); connect(cheatsEditorAction, &QAction::triggered, this, &MainWindow::openCheatsEditor); + connect(patchWindowAction, &QAction::triggered, this, &MainWindow::openPatchWindow); connect(dumpDspFirmware, &QAction::triggered, this, &MainWindow::dumpDspFirmware); auto aboutAction = aboutMenu->addAction(tr("About Panda3DS")); @@ -71,6 +73,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) aboutWindow = new AboutWindow(nullptr); configWindow = new ConfigWindow(this); cheatsEditor = new CheatsWindow(emu, {}, this); + patchWindow = new PatchWindow(this); luaEditor = new TextEditorWindow(this, "script.lua", ""); auto args = QCoreApplication::arguments(); @@ -293,6 +296,7 @@ void MainWindow::showAboutMenu() { void MainWindow::openLuaEditor() { luaEditor->show(); } void MainWindow::openCheatsEditor() { cheatsEditor->show(); } +void MainWindow::openPatchWindow() { patchWindow->show(); } void MainWindow::dispatchMessage(const EmulatorMessage& message) { switch (message.type) { diff --git a/src/panda_qt/patch_window.cpp b/src/panda_qt/patch_window.cpp new file mode 100644 index 00000000..98983865 --- /dev/null +++ b/src/panda_qt/patch_window.cpp @@ -0,0 +1,123 @@ +#include "panda_qt/patch_window.hpp" + +#include +#include +#include +#include +#include + +#include "hips.hpp" +#include "io_file.hpp" + +PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { + QVBoxLayout* layout = new QVBoxLayout; + layout->setContentsMargins(6, 6, 6, 6); + setLayout(layout); + + QWidget* inputBox = new QWidget; + QHBoxLayout* inputLayout = new QHBoxLayout; + QLabel* inputText = new QLabel(tr("Select input file")); + QPushButton* inputButton = new QPushButton(tr("Select")); + inputPathLabel = new EllidedLabel(""); + inputPathLabel->setFixedWidth(200); + + inputLayout->addWidget(inputText); + inputLayout->addWidget(inputButton); + inputLayout->addWidget(inputPathLabel); + inputBox->setLayout(inputLayout); + + QWidget* patchBox = new QWidget; + QHBoxLayout* patchLayout = new QHBoxLayout; + QLabel* patchText = new QLabel(tr("Select patch file")); + QPushButton* patchButton = new QPushButton(tr("Select")); + patchPathLabel = new EllidedLabel(""); + patchPathLabel->setFixedWidth(200); + + patchLayout->addWidget(patchText); + patchLayout->addWidget(patchButton); + patchLayout->addWidget(patchPathLabel); + patchBox->setLayout(patchLayout); + + QWidget* actionBox = new QWidget; + QHBoxLayout* actionLayout = new QHBoxLayout; + QPushButton* applyPatchButton = new QPushButton(tr("Apply patch")); + actionLayout->addWidget(applyPatchButton); + actionBox->setLayout(actionLayout); + + layout->addWidget(inputBox); + layout->addWidget(patchBox); + layout->addWidget(actionBox); + + connect(inputButton, &QPushButton::clicked, this, [this]() { + auto path = QFileDialog::getOpenFileName(this, tr("Select file to patch"), "", tr("All files (*.*)")); + inputPath = std::filesystem::path(path.toStdU16String()); + + inputPathLabel->setText(path); + }); + + connect(patchButton, &QPushButton::clicked, this, [this]() { + auto path = QFileDialog::getOpenFileName(this, tr("Select patch file"), "", tr("Patch files (*.ips *.ups *.bps)")); + patchPath = std::filesystem::path(path.toStdU16String()); + + patchPathLabel->setText(path); + }); + + connect(applyPatchButton, &QPushButton::clicked, this, [this]() { + if (inputPath.empty() || patchPath.empty()) { + printf("Pls set paths properly"); + return; + } + + auto path = QFileDialog::getSaveFileName(this, tr("Select file"), QString::fromStdU16String(inputPath.u16string()), tr("All files (*.*)")); + std::filesystem::path outputPath = std::filesystem::path(path.toStdU16String()); + + if (outputPath.empty()) { + printf("Pls set paths properly"); + return; + } + + Hips::PatchType patchType; + auto extension = patchPath.extension(); + + // Figure out what sort of patch we're dealing with + if (extension == ".ips") { + patchType = Hips::PatchType::IPS; + } else if (extension == ".ups") { + patchType = Hips::PatchType::UPS; + } else if (extension == ".bps") { + patchType = Hips::PatchType::BPS; + } else { + printf("Unknown patch format\n"); + return; + } + + // Read input and patch files into buffers + IOFile input(inputPath, "rb"); + IOFile patch(patchPath, "rb"); + + if (!input.isOpen() || !patch.isOpen()) { + printf("Failed to open input or patch file.\n"); + return; + } + + // Read the files into arrays + const auto inputSize = *input.size(); + const auto patchSize = *patch.size(); + + std::unique_ptr inputData(new uint8_t[inputSize]); + std::unique_ptr patchData(new uint8_t[patchSize]); + + input.rewind(); + patch.rewind(); + input.readBytes(inputData.get(), inputSize); + patch.readBytes(patchData.get(), patchSize); + + auto [bytes, result] = Hips::patch(inputData.get(), inputSize, patchData.get(), patchSize, patchType); + + // Write patched file + if (!bytes.empty()) { + IOFile output(outputPath, "wb"); + output.writeBytes(bytes.data(), bytes.size()); + } + }); +} \ No newline at end of file diff --git a/third_party/hips b/third_party/hips new file mode 160000 index 00000000..bbe8faf1 --- /dev/null +++ b/third_party/hips @@ -0,0 +1 @@ +Subproject commit bbe8faf149c4e10aaa45e2454fdb386e4cabf0cb From 332fbcfff184b98f8e6a521e7b8e834d89414177 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 7 May 2024 23:55:32 +0300 Subject: [PATCH 024/251] Qt: Add patching errors --- CMakeLists.txt | 2 +- docs/img/rpog_icon.png | Bin 0 -> 26925 bytes include/panda_qt/patch_window.hpp | 10 +++++++ src/panda_qt/patch_window.cpp | 45 ++++++++++++++++++++++++++---- 4 files changed, 51 insertions(+), 6 deletions(-) create mode 100644 docs/img/rpog_icon.png diff --git a/CMakeLists.txt b/CMakeLists.txt index dca69d6b..88ad6aeb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -486,7 +486,7 @@ if(NOT BUILD_HYDRA_CORE) qt_add_resources(AlberCore "app_images" PREFIX "/" FILES - docs/img/rsob_icon.png docs/img/rstarstruck_icon.png + docs/img/rsob_icon.png docs/img/rstarstruck_icon.png docs/img/rpog_icon.png ) else() set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp src/panda_sdl/mappings.cpp) diff --git a/docs/img/rpog_icon.png b/docs/img/rpog_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b5426aed8a19dd97e2727087a0e44494574fab2f GIT binary patch literal 26925 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 literal 0 HcmV?d00001 diff --git a/include/panda_qt/patch_window.hpp b/include/panda_qt/patch_window.hpp index 652c9a23..06676a66 100644 --- a/include/panda_qt/patch_window.hpp +++ b/include/panda_qt/patch_window.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include @@ -13,6 +14,15 @@ class PatchWindow final : public QWidget { ~PatchWindow() = default; private: + // Show a message box + // Title: Title of the message box to display + // Message: Message to display + // Icon: The type of icon (error, warning, information, etc) to display + // IconPath: If non-null, then a path to an icon in our assets to display on the OK button + void displayMessage( + const QString& title, const QString& message, QMessageBox::Icon icon = QMessageBox::Icon::Warning, const char* iconPath = nullptr + ); + std::filesystem::path inputPath = ""; std::filesystem::path patchPath = ""; diff --git a/src/panda_qt/patch_window.cpp b/src/panda_qt/patch_window.cpp index 98983865..de5cd277 100644 --- a/src/panda_qt/patch_window.cpp +++ b/src/panda_qt/patch_window.cpp @@ -1,7 +1,9 @@ #include "panda_qt/patch_window.hpp" +#include #include #include +#include #include #include #include @@ -64,15 +66,20 @@ PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { connect(applyPatchButton, &QPushButton::clicked, this, [this]() { if (inputPath.empty() || patchPath.empty()) { - printf("Pls set paths properly"); + displayMessage(tr("Paths not provided correctly"), tr("Please provide paths for both the input file and the patch file")); return; } - auto path = QFileDialog::getSaveFileName(this, tr("Select file"), QString::fromStdU16String(inputPath.u16string()), tr("All files (*.*)")); + // std::filesystem::path only has += and not + for reasons unknown to humanity + auto defaultPath = inputPath.parent_path() / inputPath.stem(); + defaultPath += "-patched"; + defaultPath += inputPath.extension(); + + auto path = QFileDialog::getSaveFileName(this, tr("Select file"), QString::fromStdU16String(defaultPath.u16string()), tr("All files (*.*)")); std::filesystem::path outputPath = std::filesystem::path(path.toStdU16String()); if (outputPath.empty()) { - printf("Pls set paths properly"); + displayMessage(tr("No output path"), tr("No path was provided for the output file, no patching was done")); return; } @@ -87,7 +94,7 @@ PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { } else if (extension == ".bps") { patchType = Hips::PatchType::BPS; } else { - printf("Unknown patch format\n"); + displayMessage(tr("Unknown patch format"), tr("Unknown format for patch file. Currently IPS, UPS and BPS are supported")); return; } @@ -96,7 +103,7 @@ PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { IOFile patch(patchPath, "rb"); if (!input.isOpen() || !patch.isOpen()) { - printf("Failed to open input or patch file.\n"); + displayMessage(tr("Failed to open input files"), tr("Make sure they're in a directory Panda3DS has access to")); return; } @@ -119,5 +126,33 @@ PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { IOFile output(outputPath, "wb"); output.writeBytes(bytes.data(), bytes.size()); } + + switch (result) { + case Hips::Result::Success: + displayMessage( + tr("Patching Success"), tr("Your file was patched successfully."), QMessageBox::Icon::Information, ":/docs/img/rpog_icon.png" + ); + break; + + case Hips::Result::ChecksumMismatch: + displayMessage( + tr("Checksum mismatch"), + tr("Patch was applied successfully but a checksum mismatch was detected. The input or output files might not be correct") + ); + break; + + default: displayMessage(tr("Patching error"), tr("An error occured while patching")); break; + } }); +} + +void PatchWindow::PatchWindow::displayMessage(const QString& title, const QString& message, QMessageBox::Icon icon, const char* iconPath) { + QMessageBox messageBox(icon, title, message); + QAbstractButton* button = messageBox.addButton(tr("OK"), QMessageBox::ButtonRole::YesRole); + + if (iconPath != nullptr) { + button->setIcon(QIcon(iconPath)); + } + + messageBox.exec(); } \ No newline at end of file From aa7a6bfe7a17219a42b0830c8c646484eafa7852 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 8 May 2024 00:20:39 +0000 Subject: [PATCH 025/251] s/ellided/elided (#510) * s/ellided/elided * Fix header name --- CMakeLists.txt | 4 +-- .../{ellided_label.hpp => elided_label.hpp} | 6 ++--- include/panda_qt/patch_window.hpp | 2 +- src/panda_qt/elided_label.cpp | 25 +++++++++++++++++++ src/panda_qt/ellided_label.cpp | 25 ------------------- src/panda_qt/patch_window.cpp | 4 +-- 6 files changed, 33 insertions(+), 33 deletions(-) rename include/panda_qt/{ellided_label.hpp => elided_label.hpp} (53%) create mode 100644 src/panda_qt/elided_label.cpp delete mode 100644 src/panda_qt/ellided_label.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 88ad6aeb..748c298b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -449,11 +449,11 @@ if(NOT BUILD_HYDRA_CORE) set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp src/panda_qt/mappings.cpp - src/panda_qt/patch_window.cpp src/panda_qt/ellided_label.cpp + src/panda_qt/patch_window.cpp src/panda_qt/elided_label.cpp ) set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp - include/panda_qt/patch_window.hpp include/panda_qt/ellided_label.hpp + include/panda_qt/patch_window.hpp include/panda_qt/elided_label.hpp ) source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) diff --git a/include/panda_qt/ellided_label.hpp b/include/panda_qt/elided_label.hpp similarity index 53% rename from include/panda_qt/ellided_label.hpp rename to include/panda_qt/elided_label.hpp index 19fd8c74..9d937f9b 100644 --- a/include/panda_qt/ellided_label.hpp +++ b/include/panda_qt/elided_label.hpp @@ -4,11 +4,11 @@ #include #include -class EllidedLabel : public QLabel { +class ElidedLabel : public QLabel { Q_OBJECT public: - explicit EllidedLabel(Qt::TextElideMode elideMode = Qt::ElideLeft, QWidget* parent = nullptr); - explicit EllidedLabel(QString text, Qt::TextElideMode elideMode = Qt::ElideLeft, QWidget* parent = nullptr); + explicit ElidedLabel(Qt::TextElideMode elideMode = Qt::ElideLeft, QWidget* parent = nullptr); + explicit ElidedLabel(QString text, Qt::TextElideMode elideMode = Qt::ElideLeft, QWidget* parent = nullptr); void setText(QString text); protected: diff --git a/include/panda_qt/patch_window.hpp b/include/panda_qt/patch_window.hpp index 06676a66..ccffae4f 100644 --- a/include/panda_qt/patch_window.hpp +++ b/include/panda_qt/patch_window.hpp @@ -4,7 +4,7 @@ #include #include -#include "panda_qt/ellided_label.hpp" +#include "panda_qt/elided_label.hpp" class PatchWindow final : public QWidget { Q_OBJECT diff --git a/src/panda_qt/elided_label.cpp b/src/panda_qt/elided_label.cpp new file mode 100644 index 00000000..f15cf11d --- /dev/null +++ b/src/panda_qt/elided_label.cpp @@ -0,0 +1,25 @@ +#include "panda_qt/elided_label.hpp" + +// Based on https://stackoverflow.com/questions/7381100/text-overflow-for-a-qlabel-s-text-rendering-in-qt +ElidedLabel::ElidedLabel(Qt::TextElideMode elideMode, QWidget* parent) : ElidedLabel("", elideMode, parent) {} + +ElidedLabel::ElidedLabel(QString text, Qt::TextElideMode elideMode, QWidget* parent) : QLabel(parent) { + m_elideMode = elideMode; + setText(text); +} + +void ElidedLabel::setText(QString text) { + m_text = text; + updateText(); +} + +void ElidedLabel::resizeEvent(QResizeEvent* event) { + QLabel::resizeEvent(event); + updateText(); +} + +void ElidedLabel::updateText() { + QFontMetrics metrics(font()); + QString elided = metrics.elidedText(m_text, m_elideMode, width()); + QLabel::setText(elided); +} \ No newline at end of file diff --git a/src/panda_qt/ellided_label.cpp b/src/panda_qt/ellided_label.cpp deleted file mode 100644 index 68c0da76..00000000 --- a/src/panda_qt/ellided_label.cpp +++ /dev/null @@ -1,25 +0,0 @@ -#include "panda_qt/ellided_label.hpp" - -// Based on https://stackoverflow.com/questions/7381100/text-overflow-for-a-qlabel-s-text-rendering-in-qt -EllidedLabel::EllidedLabel(Qt::TextElideMode elideMode, QWidget* parent) : EllidedLabel("", elideMode, parent) {} - -EllidedLabel::EllidedLabel(QString text, Qt::TextElideMode elideMode, QWidget* parent) : QLabel(parent) { - m_elideMode = elideMode; - setText(text); -} - -void EllidedLabel::setText(QString text) { - m_text = text; - updateText(); -} - -void EllidedLabel::resizeEvent(QResizeEvent* event) { - QLabel::resizeEvent(event); - updateText(); -} - -void EllidedLabel::updateText() { - QFontMetrics metrics(font()); - QString elided = metrics.elidedText(m_text, m_elideMode, width()); - QLabel::setText(elided); -} \ No newline at end of file diff --git a/src/panda_qt/patch_window.cpp b/src/panda_qt/patch_window.cpp index de5cd277..189288eb 100644 --- a/src/panda_qt/patch_window.cpp +++ b/src/panda_qt/patch_window.cpp @@ -20,7 +20,7 @@ PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { QHBoxLayout* inputLayout = new QHBoxLayout; QLabel* inputText = new QLabel(tr("Select input file")); QPushButton* inputButton = new QPushButton(tr("Select")); - inputPathLabel = new EllidedLabel(""); + inputPathLabel = new ElidedLabel(""); inputPathLabel->setFixedWidth(200); inputLayout->addWidget(inputText); @@ -32,7 +32,7 @@ PatchWindow::PatchWindow(QWidget* parent) : QWidget(parent, Qt::Window) { QHBoxLayout* patchLayout = new QHBoxLayout; QLabel* patchText = new QLabel(tr("Select patch file")); QPushButton* patchButton = new QPushButton(tr("Select")); - patchPathLabel = new EllidedLabel(""); + patchPathLabel = new ElidedLabel(""); patchPathLabel->setFixedWidth(200); patchLayout->addWidget(patchText); From 9a50a57d327471a5a20a954285466dc00115d3ff Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 10 May 2024 02:13:58 +0300 Subject: [PATCH 026/251] Fix CI --- include/panda_qt/patch_window.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/panda_qt/patch_window.hpp b/include/panda_qt/patch_window.hpp index ccffae4f..a6e1a129 100644 --- a/include/panda_qt/patch_window.hpp +++ b/include/panda_qt/patch_window.hpp @@ -26,6 +26,6 @@ class PatchWindow final : public QWidget { std::filesystem::path inputPath = ""; std::filesystem::path patchPath = ""; - EllidedLabel* inputPathLabel = nullptr; - EllidedLabel* patchPathLabel = nullptr; + ElidedLabel* inputPathLabel = nullptr; + ElidedLabel* patchPathLabel = nullptr; }; From 2f9d5e30b409d0498c8a235b09b2a15181d43a75 Mon Sep 17 00:00:00 2001 From: NerduMiner Date: Sat, 11 May 2024 15:04:53 -0400 Subject: [PATCH 027/251] Index with iterator value in CAMService::startCapture rather than getSingleIndex() The port may have a value of 3 in this function, which will cause a panic. getPortIndices() handles this case for us already, so the iterator vale is safe to use --- src/core/services/cam.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/services/cam.cpp b/src/core/services/cam.cpp index b3dfd1dc..d9c005e7 100644 --- a/src/core/services/cam.cpp +++ b/src/core/services/cam.cpp @@ -343,7 +343,7 @@ void CAMService::startCapture(u32 messagePointer) { if (port.isValid()) { for (int i : port.getPortIndices()) { - auto& event = ports[port.getSingleIndex()].receiveEvent; + auto& event = ports[i].receiveEvent; // Until we properly implement cameras, immediately signal the receive event if (event.has_value()) { From 842943fa4cb674bc2b5a652d419f8e4acd889e90 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 13 May 2024 00:51:40 +0300 Subject: [PATCH 028/251] GLSL shader gen: Add alpha test (...half of it I guess) --- CMakeLists.txt | 2 +- include/PICA/pica_frag_config.hpp | 53 ++++++++++++++++++++++++++++ include/PICA/regs.hpp | 11 ++++++ include/PICA/shader_gen.hpp | 2 ++ include/renderer_gl/renderer_gl.hpp | 27 +------------- src/core/PICA/shader_gen_glsl.cpp | 24 +++++++++++++ src/core/renderer_gl/renderer_gl.cpp | 27 ++++++++------ 7 files changed, 108 insertions(+), 38 deletions(-) create mode 100644 include/PICA/pica_frag_config.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 0bdb8abb..c6b12188 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -242,7 +242,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/PICA/dynapica/shader_rec_emitter_arm64.hpp include/scheduler.hpp include/applets/error_applet.hpp include/PICA/shader_gen.hpp include/audio/dsp_core.hpp include/audio/null_core.hpp include/audio/teakra_core.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp - include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp + include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp include/PICA/pica_frag_config.hpp ) cmrc_add_resource_library( diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp new file mode 100644 index 00000000..c4d46b11 --- /dev/null +++ b/include/PICA/pica_frag_config.hpp @@ -0,0 +1,53 @@ +#pragma once +#include +#include +#include +#include + +#include "PICA/pica_hash.hpp" +#include "PICA/regs.hpp" +#include "bitfield.hpp" +#include "helpers.hpp" + +namespace PICA { + struct OutputConfig { + union { + u32 raw; + // Merge the enable + compare function into 1 field to avoid duplicate shaders + // enable == off means a CompareFunction of Always + BitField<0, 3, CompareFunction> alphaTestFunction; + }; + }; + + struct TextureConfig { + u32 texUnitConfig; + u32 texEnvUpdateBuffer; + + // TODO: This should probably be a uniform + u32 texEnvBufferColor; + + // There's 6 TEV stages, and each one is configured via 5 word-sized registers + std::array tevConfigs; + }; + + struct FragmentConfig { + OutputConfig outConfig; + TextureConfig texConfig; + + bool operator==(const FragmentConfig& config) const { + // Hash function and equality operator required by std::unordered_map + return std::memcmp(this, &config, sizeof(FragmentConfig)) == 0; + } + }; + + static_assert( + std::has_unique_object_representations() && std::has_unique_object_representations() && + std::has_unique_object_representations() + ); +} // namespace PICA + +// Override std::hash for our fragment config class +template <> +struct std::hash { + std::size_t operator()(const PICA::FragmentConfig& config) const noexcept { return PICAHash::computeHash((const char*)&config, sizeof(config)); } +}; \ No newline at end of file diff --git a/include/PICA/regs.hpp b/include/PICA/regs.hpp index 5b9e1830..74f8c7d5 100644 --- a/include/PICA/regs.hpp +++ b/include/PICA/regs.hpp @@ -345,6 +345,17 @@ namespace PICA { GeometryPrimitive = 3, }; + enum class CompareFunction : u32 { + Never = 0, + Always = 1, + Equal = 2, + NotEqual = 3, + Less = 4, + LessOrEqual = 5, + Greater = 6, + GreaterOrEqual = 7, + }; + struct TexEnvConfig { enum class Source : u8 { PrimaryColor = 0x0, diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index 23a87120..e8e8ca20 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -24,6 +24,8 @@ namespace PICA::ShaderGen { void getColorOperation(std::string& shader, PICA::TexEnvConfig::Operation op); void getAlphaOperation(std::string& shader, PICA::TexEnvConfig::Operation op); + void applyAlphaTest(std::string& shader, const PICARegs& regs); + u32 textureConfig = 0; public: diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index e8eaeacb..53ca9975 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -7,6 +7,7 @@ #include #include "PICA/float_types.hpp" +#include "PICA/pica_frag_config.hpp" #include "PICA/pica_hash.hpp" #include "PICA/pica_vertex.hpp" #include "PICA/regs.hpp" @@ -21,32 +22,6 @@ // More circular dependencies! class GPU; -namespace PICA { - struct FragmentConfig { - u32 texUnitConfig; - u32 texEnvUpdateBuffer; - - // TODO: This should probably be a uniform - u32 texEnvBufferColor; - - // There's 6 TEV stages, and each one is configured via 5 word-sized registers - std::array tevConfigs; - - // Hash function and equality operator required by std::unordered_map - bool operator==(const FragmentConfig& config) const { - return std::memcmp(this, &config, sizeof(FragmentConfig)) == 0; - } - }; -} // namespace PICA - -// Override std::hash for our fragment config class -template <> -struct std::hash { - std::size_t operator()(const PICA::FragmentConfig& config) const noexcept { - return PICAHash::computeHash((const char*)&config, sizeof(config)); - } -}; - class RendererGL final : public Renderer { GLStateManager gl = {}; diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index c3056815..50be94f0 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -144,6 +144,8 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { compileTEV(ret, i, regs); } + applyAlphaTest(ret, regs); + ret += "fragColor = combinerOutput;\n"; ret += "}"; // End of main function ret += "\n\n\n\n\n\n\n\n\n\n\n\n\n"; @@ -353,3 +355,25 @@ void FragmentGenerator::getAlphaOperation(std::string& shader, TexEnvConfig::Ope break; } } + +void FragmentGenerator::applyAlphaTest(std::string& shader, const PICARegs& regs) { + const u32 alphaConfig = regs[InternalRegs::AlphaTestConfig]; + // Alpha test disabled + if (Helpers::getBit<0>(alphaConfig) == 0) { + return; + } + + const auto function = static_cast(Helpers::getBits<4, 3>(alphaConfig)); + + shader += "if ("; + switch (function) { + case CompareFunction::Never: shader += "true"; break; + case CompareFunction::Always: shader += "false"; break; + default: + Helpers::warn("Unimplemented alpha test function"); + shader += "false"; + break; + } + + shader += ") { discard; }\n"; +} diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 5d3ed1b1..cfd197f8 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -796,22 +796,27 @@ std::optional RendererGL::getColourBuffer(u32 addr, PICA::ColorFmt OpenGL::Program& RendererGL::getSpecializedShader() { PICA::FragmentConfig fsConfig; - fsConfig.texUnitConfig = regs[InternalRegs::TexUnitCfg]; - fsConfig.texEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; - fsConfig.texEnvBufferColor = regs[InternalRegs::TexEnvBufferColor]; + auto& outConfig = fsConfig.outConfig; + auto& texConfig = fsConfig.texConfig; + + auto alphaTestConfig = regs[InternalRegs::AlphaTestConfig]; + auto alphaTestFunction = Helpers::getBits<4, 3>(alphaTestConfig); + outConfig.alphaTestFunction = (alphaTestConfig & 1) ? static_cast(alphaTestFunction) : PICA::CompareFunction::Always; + + texConfig.texUnitConfig = regs[InternalRegs::TexUnitCfg]; + texConfig.texEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; + texConfig.texEnvBufferColor = 0; // Set up TEV stages - std::memcpy(&fsConfig.tevConfigs[0 * 5], ®s[InternalRegs::TexEnv0Source], 5 * sizeof(u32)); - std::memcpy(&fsConfig.tevConfigs[1 * 5], ®s[InternalRegs::TexEnv1Source], 5 * sizeof(u32)); - std::memcpy(&fsConfig.tevConfigs[2 * 5], ®s[InternalRegs::TexEnv2Source], 5 * sizeof(u32)); - std::memcpy(&fsConfig.tevConfigs[3 * 5], ®s[InternalRegs::TexEnv3Source], 5 * sizeof(u32)); - std::memcpy(&fsConfig.tevConfigs[4 * 5], ®s[InternalRegs::TexEnv4Source], 5 * sizeof(u32)); - std::memcpy(&fsConfig.tevConfigs[5 * 5], ®s[InternalRegs::TexEnv5Source], 5 * sizeof(u32)); + std::memcpy(&texConfig.tevConfigs[0 * 5], ®s[InternalRegs::TexEnv0Source], 5 * sizeof(u32)); + std::memcpy(&texConfig.tevConfigs[1 * 5], ®s[InternalRegs::TexEnv1Source], 5 * sizeof(u32)); + std::memcpy(&texConfig.tevConfigs[2 * 5], ®s[InternalRegs::TexEnv2Source], 5 * sizeof(u32)); + std::memcpy(&texConfig.tevConfigs[3 * 5], ®s[InternalRegs::TexEnv3Source], 5 * sizeof(u32)); + std::memcpy(&texConfig.tevConfigs[4 * 5], ®s[InternalRegs::TexEnv4Source], 5 * sizeof(u32)); + std::memcpy(&texConfig.tevConfigs[5 * 5], ®s[InternalRegs::TexEnv5Source], 5 * sizeof(u32)); OpenGL::Program& program = shaderCache[fsConfig]; if (!program.exists()) { - printf("Creating specialized shader\n"); - std::string vs = fragShaderGen.getVertexShader(regs); std::string fs = fragShaderGen.generate(regs); std::cout << vs << "\n\n" << fs << "\n"; From 85a17c3fcd507083192da82534b825bc90cbce44 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 13 May 2024 01:10:44 +0300 Subject: [PATCH 029/251] Add UBO support to opengl.hpp --- third_party/opengl/opengl.hpp | 94 +++++++++++++++++++++++++---------- 1 file changed, 68 insertions(+), 26 deletions(-) diff --git a/third_party/opengl/opengl.hpp b/third_party/opengl/opengl.hpp index f368f573..9997e63b 100644 --- a/third_party/opengl/opengl.hpp +++ b/third_party/opengl/opengl.hpp @@ -430,36 +430,36 @@ namespace OpenGL { glDispatchCompute(groupsX, groupsY, groupsZ); } - struct VertexBuffer { - GLuint m_handle = 0; + struct VertexBuffer { + GLuint m_handle = 0; - void create() { - if (m_handle == 0) { - glGenBuffers(1, &m_handle); - } - } + void create() { + if (m_handle == 0) { + glGenBuffers(1, &m_handle); + } + } - void createFixedSize(GLsizei size, GLenum usage = GL_DYNAMIC_DRAW) { - create(); - bind(); - glBufferData(GL_ARRAY_BUFFER, size, nullptr, usage); - } + void createFixedSize(GLsizei size, GLenum usage = GL_DYNAMIC_DRAW) { + create(); + bind(); + glBufferData(GL_ARRAY_BUFFER, size, nullptr, usage); + } - VertexBuffer(bool shouldCreate = false) { - if (shouldCreate) { - create(); - } - } + VertexBuffer(bool shouldCreate = false) { + if (shouldCreate) { + create(); + } + } #ifdef OPENGL_DESTRUCTORS - ~VertexBuffer() { free(); } -#endif - GLuint handle() const { return m_handle; } - bool exists() const { return m_handle != 0; } - void bind() const { glBindBuffer(GL_ARRAY_BUFFER, m_handle); } - void free() { glDeleteBuffers(1, &m_handle); } + ~VertexBuffer() { free(); } +#endif + GLuint handle() const { return m_handle; } + bool exists() const { return m_handle != 0; } + void bind() const { glBindBuffer(GL_ARRAY_BUFFER, m_handle); } + void free() { glDeleteBuffers(1, &m_handle); } - // Reallocates the buffer on every call. Prefer the sub version if possible. + // Reallocates the buffer on every call. Prefer the sub version if possible. template void bufferVerts(VertType* vertices, int vertCount, GLenum usage = GL_DYNAMIC_DRAW) { glBufferData(GL_ARRAY_BUFFER, sizeof(VertType) * vertCount, vertices, usage); @@ -471,7 +471,7 @@ namespace OpenGL { glBufferSubData(GL_ARRAY_BUFFER, offset, sizeof(VertType) * vertCount, vertices); } - // If C++20 is available, add overloads that take std::span instead of raw pointers + // If C++20 is available, add overloads that take std::span instead of raw pointers #ifdef OPENGL_HAVE_CPP20 template void bufferVerts(std::span vertices, GLenum usage = GL_DYNAMIC_DRAW) { @@ -485,6 +485,48 @@ namespace OpenGL { #endif }; + struct UniformBuffer { + GLuint m_handle = 0; + + void create() { + if (m_handle == 0) { + glGenBuffers(1, &m_handle); + } + } + + void createFixedSize(GLsizei size, GLenum usage = GL_DYNAMIC_DRAW) { + create(); + bind(); + glBufferData(GL_UNIFORM_BUFFER, size, nullptr, usage); + } + + UniformBuffer(bool shouldCreate = false) { + if (shouldCreate) { + create(); + } + } + +#ifdef OPENGL_DESTRUCTORS + ~UniformBuffer() { free(); } +#endif + GLuint handle() const { return m_handle; } + bool exists() const { return m_handle != 0; } + void bind() const { glBindBuffer(GL_UNIFORM_BUFFER, m_handle); } + void free() { glDeleteBuffers(1, &m_handle); } + + // Reallocates the buffer on every call. Prefer the sub version if possible. + template + void buffer(const UniformType& uniformData, GLenum usage = GL_DYNAMIC_DRAW) { + glBufferData(GL_UNIFORM_BUFFER, sizeof(uniformData), &uniformData, usage); + } + + // Only use if you used createFixedSize + template + void bufferSub(const UniformType& uniformData, int vertCount, GLintptr offset = 0) { + glBufferSubData(GL_UNIFORM_BUFFER, offset, sizeof(uniformData), &uniformData); + } + }; + enum DepthFunc { Never = GL_NEVER, // Depth test never passes Always = GL_ALWAYS, // Depth test always passes @@ -693,4 +735,4 @@ namespace OpenGL { using Rect = Rectangle; -} // end namespace OpenGL \ No newline at end of file +} // end namespace OpenGL From 12d25fe20d269c7afb12ebd80724ee1ade2c8b87 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 8 Jun 2024 15:04:36 +0000 Subject: [PATCH 030/251] CMake: Remove Vulkan version requirement --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 748c298b..3492bf59 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -338,7 +338,7 @@ endif() if(ENABLE_VULKAN) find_package( - Vulkan 1.3.206 REQUIRED + Vulkan REQUIRED COMPONENTS glslangValidator ) From 29d9ed7224024f3ace7bf3c3a12d79d467d54be8 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 25 Jun 2024 22:11:48 +0000 Subject: [PATCH 031/251] Try to fix Vulkan on Windows CI part 2 (#521) * Try to fix Vulkan SDK on Windows CI * Try to fix Vulkan SDK on Windows CI * Update CMakeLists.txt * Update CMakeLists.txt * Try to fix Vulkan SDK on Windows CI * Add trace to Windows build * Update Windows_Build.yml * Update Windows_Build.yml * Update CMakeLists.txt * Update CMakeLists.txt * Update CMakeLists.txt * Update CMakeLists.txt * Update CMakeLists.txt * Update Windows_Build.yml --- CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3492bf59..80114bfa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -339,7 +339,7 @@ endif() if(ENABLE_VULKAN) find_package( Vulkan REQUIRED - COMPONENTS glslangValidator + COMPONENTS glslang ) set(RENDERER_VK_INCLUDE_FILES include/renderer_vk/renderer_vk.hpp @@ -382,7 +382,7 @@ if(ENABLE_VULKAN) add_custom_command( OUTPUT ${HOST_SHADER_SPIRV} COMMAND ${CMAKE_COMMAND} -E make_directory "${PROJECT_BINARY_DIR}/host_shaders/" - COMMAND Vulkan::glslangValidator ${RENDERER_VK_HOST_SHADERS_FLAGS} -V "${PROJECT_SOURCE_DIR}/${HOST_SHADER_SOURCE}" -o ${HOST_SHADER_SPIRV} + COMMAND glslang ${RENDERER_VK_HOST_SHADERS_FLAGS} -V "${PROJECT_SOURCE_DIR}/${HOST_SHADER_SOURCE}" -o ${HOST_SHADER_SPIRV} DEPENDS ${HOST_SHADER_SOURCE} ) list( APPEND RENDERER_VK_HOST_SHADERS_SPIRV ${HOST_SHADER_SPIRV} ) From 1c9a3ac3d3d9414a7e6b270132fe7aacb786a651 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:30:38 +0300 Subject: [PATCH 032/251] Add Y2R event delay --- include/kernel/kernel.hpp | 2 ++ include/scheduler.hpp | 3 ++- include/services/service_manager.hpp | 1 + include/services/y2r.hpp | 6 +++++- src/core/kernel/kernel.cpp | 2 ++ src/core/services/y2r.cpp | 32 ++++++++++++++++++++++------ src/emulator.cpp | 2 ++ 7 files changed, 40 insertions(+), 8 deletions(-) diff --git a/include/kernel/kernel.hpp b/include/kernel/kernel.hpp index fc7fe3f3..e0c0651b 100644 --- a/include/kernel/kernel.hpp +++ b/include/kernel/kernel.hpp @@ -15,6 +15,7 @@ #include "services/service_manager.hpp" class CPU; +struct Scheduler; class Kernel { std::span regs; @@ -243,6 +244,7 @@ public: } ServiceManager& getServiceManager() { return serviceManager; } + Scheduler& getScheduler(); void sendGPUInterrupt(GPUInterrupt type) { serviceManager.sendGPUInterrupt(type); } void clearInstructionCache(); diff --git a/include/scheduler.hpp b/include/scheduler.hpp index 97c50afc..cfc4d5e8 100644 --- a/include/scheduler.hpp +++ b/include/scheduler.hpp @@ -11,7 +11,8 @@ struct Scheduler { VBlank = 0, // End of frame event UpdateTimers = 1, // Update kernel timer objects RunDSP = 2, // Make the emulated DSP run for one audio frame - Panic = 3, // Dummy event that is always pending and should never be triggered (Timestamp = UINT64_MAX) + SignalY2R = 3, // Signal that a Y2R conversion has finished + Panic = 4, // Dummy event that is always pending and should never be triggered (Timestamp = UINT64_MAX) TotalNumberOfEvents // How many event types do we have in total? }; static constexpr usize totalNumberOfEvents = static_cast(EventType::TotalNumberOfEvents); diff --git a/include/services/service_manager.hpp b/include/services/service_manager.hpp index 8d1cf381..6679f98d 100644 --- a/include/services/service_manager.hpp +++ b/include/services/service_manager.hpp @@ -109,4 +109,5 @@ class ServiceManager { HIDService& getHID() { return hid; } NFCService& getNFC() { return nfc; } DSPService& getDSP() { return dsp; } + Y2RService& getY2R() { return y2r; } }; diff --git a/include/services/y2r.hpp b/include/services/y2r.hpp index 0cc1d587..4aa96d7b 100644 --- a/include/services/y2r.hpp +++ b/include/services/y2r.hpp @@ -113,8 +113,12 @@ class Y2RService { void startConversion(u32 messagePointer); void stopConversion(u32 messagePointer); -public: + bool isBusy; + + public: Y2RService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} void reset(); void handleSyncRequest(u32 messagePointer); + + void signalConversionDone(); }; \ No newline at end of file diff --git a/src/core/kernel/kernel.cpp b/src/core/kernel/kernel.cpp index 392b87fd..0d1efc15 100644 --- a/src/core/kernel/kernel.cpp +++ b/src/core/kernel/kernel.cpp @@ -399,3 +399,5 @@ std::string Kernel::getProcessName(u32 pid) { Helpers::panic("Attempted to name non-current process"); } } + +Scheduler& Kernel::getScheduler() { return cpu.getScheduler(); } diff --git a/src/core/services/y2r.cpp b/src/core/services/y2r.cpp index a796631c..ae0961cf 100644 --- a/src/core/services/y2r.cpp +++ b/src/core/services/y2r.cpp @@ -61,6 +61,7 @@ void Y2RService::reset() { inputLineWidth = 420; conversionCoefficients.fill(0); + isBusy = false; } void Y2RService::handleSyncRequest(u32 messagePointer) { @@ -156,6 +157,11 @@ void Y2RService::setTransferEndInterrupt(u32 messagePointer) { void Y2RService::stopConversion(u32 messagePointer) { log("Y2R::StopConversion\n"); + if (isBusy) { + isBusy = false; + kernel.getScheduler().removeEvent(Scheduler::EventType::SignalY2R); + } + mem.write32(messagePointer, IPC::responseHeader(0x27, 1, 0)); mem.write32(messagePointer + 4, Result::Success); } @@ -167,7 +173,7 @@ void Y2RService::isBusyConversion(u32 messagePointer) { mem.write32(messagePointer, IPC::responseHeader(0x28, 2, 0)); mem.write32(messagePointer + 4, Result::Success); - mem.write32(messagePointer + 8, static_cast(BusyStatus::NotBusy)); + mem.write32(messagePointer + 8, static_cast(isBusy ? BusyStatus::Busy : BusyStatus::NotBusy)); } void Y2RService::setBlockAlignment(u32 messagePointer) { @@ -434,11 +440,14 @@ void Y2RService::startConversion(u32 messagePointer) { mem.write32(messagePointer, IPC::responseHeader(0x26, 1, 0)); mem.write32(messagePointer + 4, Result::Success); - // Make Y2R conversion end instantly. - // Signal the transfer end event if it's been created. TODO: Is this affected by SetTransferEndInterrupt? - if (transferEndEvent.has_value()) { - kernel.signalEvent(transferEndEvent.value()); - } + // Schedule Y2R conversion end event. + static constexpr u64 delayTicks = 60'000; + isBusy = true; + + // Remove any potential pending Y2R event and schedule a new one + Scheduler& scheduler = kernel.getScheduler(); + scheduler.removeEvent(Scheduler::EventType::SignalY2R); + scheduler.addEvent(Scheduler::EventType::SignalY2R, scheduler.currentTimestamp + delayTicks); } void Y2RService::isFinishedSendingYUV(u32 messagePointer) { @@ -484,4 +493,15 @@ void Y2RService::isFinishedReceiving(u32 messagePointer) { mem.write32(messagePointer, IPC::responseHeader(0x17, 2, 0)); mem.write32(messagePointer + 4, Result::Success); mem.write32(messagePointer + 8, finished ? 1 : 0); +} + +void Y2RService::signalConversionDone() { + if (isBusy) { + isBusy = false; + + // Signal the transfer end event if it's been created. TODO: Is this affected by SetTransferEndInterrupt? + if (transferEndEvent.has_value()) { + kernel.signalEvent(transferEndEvent.value()); + } + } } \ No newline at end of file diff --git a/src/emulator.cpp b/src/emulator.cpp index 16c3bffd..af156eeb 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -169,6 +169,8 @@ void Emulator::pollScheduler() { break; } + case Scheduler::EventType::SignalY2R: kernel.getServiceManager().getY2R().signalConversionDone(); break; + default: { Helpers::panic("Scheduler: Unimplemented event type received: %d\n", static_cast(eventType)); break; From d4cf54d56cafaf1ae06d26c48e9a4f0ca1596401 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 2 Jul 2024 15:57:39 +0300 Subject: [PATCH 033/251] Tweak Y2R timings --- src/core/services/y2r.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/services/y2r.cpp b/src/core/services/y2r.cpp index ae0961cf..1c7b33cd 100644 --- a/src/core/services/y2r.cpp +++ b/src/core/services/y2r.cpp @@ -441,7 +441,8 @@ void Y2RService::startConversion(u32 messagePointer) { mem.write32(messagePointer + 4, Result::Success); // Schedule Y2R conversion end event. - static constexpr u64 delayTicks = 60'000; + // The tick value is tweaked based on the minimum delay needed to get FIFA 15 to not hang due to a race condition on its title screen + static constexpr u64 delayTicks = 1'350'000; isBusy = true; // Remove any potential pending Y2R event and schedule a new one From 800c11ff62a4893dc07e6c0b3eb760394befa9b4 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:19:20 +0300 Subject: [PATCH 034/251] HLE DSP: Add PCM8 audio decoding --- include/audio/hle_core.hpp | 1 + src/core/audio/hle_core.cpp | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index c57f221e..b59dc811 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -176,6 +176,7 @@ namespace Audio { // Decode an entire buffer worth of audio void decodeBuffer(DSPSource& source); + SampleBuffer decodePCM8(const u8* data, usize sampleCount, Source& source); SampleBuffer decodePCM16(const u8* data, usize sampleCount, Source& source); SampleBuffer decodeADPCM(const u8* data, usize sampleCount, Source& source); diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 146c7bdf..12c8f4c8 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -355,7 +355,7 @@ namespace Audio { } switch (buffer.format) { - case SampleFormat::PCM8: Helpers::warn("Unimplemented sample format!"); break; + case SampleFormat::PCM8: source.currentSamples = decodePCM8(data, buffer.sampleCount, source); break; case SampleFormat::PCM16: source.currentSamples = decodePCM16(data, buffer.sampleCount, source); break; case SampleFormat::ADPCM: source.currentSamples = decodeADPCM(data, buffer.sampleCount, source); break; @@ -406,6 +406,26 @@ namespace Audio { } } + HLE_DSP::SampleBuffer HLE_DSP::decodePCM8(const u8* data, usize sampleCount, Source& source) { + SampleBuffer decodedSamples(sampleCount); + + if (source.sourceType == SourceType::Stereo) { + for (usize i = 0; i < sampleCount; i++) { + const s16 left = s16(u16(*data++) << 8); + const s16 right = s16(u16(*data++) << 8); + decodedSamples[i] = {left, right}; + } + } else { + // Mono + for (usize i = 0; i < sampleCount; i++) { + const s16 sample = s16(u16(*data++) << 8); + decodedSamples[i] = {sample, sample}; + } + } + + return decodedSamples; + } + HLE_DSP::SampleBuffer HLE_DSP::decodePCM16(const u8* data, usize sampleCount, Source& source) { SampleBuffer decodedSamples(sampleCount); const s16* data16 = reinterpret_cast(data); From de9375122b012ab357a0bf54064422f6e2025c0a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:30:51 +0300 Subject: [PATCH 035/251] Add SDMC::DeleteFile --- src/core/fs/archive_sdmc.cpp | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/core/fs/archive_sdmc.cpp b/src/core/fs/archive_sdmc.cpp index 6c34de7a..8fda1320 100644 --- a/src/core/fs/archive_sdmc.cpp +++ b/src/core/fs/archive_sdmc.cpp @@ -39,7 +39,35 @@ HorizonResult SDMCArchive::createFile(const FSPath& path, u64 size) { } HorizonResult SDMCArchive::deleteFile(const FSPath& path) { - Helpers::panic("[SDMC] Unimplemented DeleteFile"); + if (path.type == PathType::UTF16) { + if (!isPathSafe(path)) { + Helpers::panic("Unsafe path in SDMC::DeleteFile"); + } + + fs::path p = IOFile::getAppData() / "SDMC"; + p += fs::path(path.utf16_string).make_preferred(); + + if (fs::is_directory(p)) { + Helpers::panic("SDMC::DeleteFile: Tried to delete directory"); + } + + if (!fs::is_regular_file(p)) { + return Result::FS::FileNotFoundAlt; + } + + std::error_code ec; + bool success = fs::remove(p, ec); + + // It might still be possible for fs::remove to fail, if there's eg an open handle to a file being deleted + // In this case, print a warning, but still return success for now + if (!success) { + Helpers::warn("SDMC::DeleteFile: fs::remove failed\n"); + } + + return Result::Success; + } + + Helpers::panic("SaveDataArchive::DeleteFile: Unknown path type"); return Result::Success; } From 0fe62f9b46153e0d6f72571650454814ae0e7cf1 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 2 Jul 2024 17:32:17 +0300 Subject: [PATCH 036/251] Correct archive names --- src/core/fs/archive_sdmc.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/fs/archive_sdmc.cpp b/src/core/fs/archive_sdmc.cpp index 8fda1320..97b02b9e 100644 --- a/src/core/fs/archive_sdmc.cpp +++ b/src/core/fs/archive_sdmc.cpp @@ -67,7 +67,7 @@ HorizonResult SDMCArchive::deleteFile(const FSPath& path) { return Result::Success; } - Helpers::panic("SaveDataArchive::DeleteFile: Unknown path type"); + Helpers::panic("SDMCArchive::DeleteFile: Unknown path type"); return Result::Success; } @@ -173,7 +173,7 @@ Rust::Result SDMCArchive::openDirectory(const F if (path.type == PathType::UTF16) { if (!isPathSafe(path)) { - Helpers::panic("Unsafe path in SaveData::OpenDirectory"); + Helpers::panic("Unsafe path in SDMC::OpenDirectory"); } fs::path p = IOFile::getAppData() / "SDMC"; From 0e4079f30457a28f3ba5fe60fb775cd089e781cd Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:23:43 +0300 Subject: [PATCH 037/251] a64 shader recompiler: Add DPH/DPHI --- .../dynapica/shader_rec_emitter_arm64.cpp | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp b/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp index d6358070..15200e76 100644 --- a/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp +++ b/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp @@ -144,8 +144,8 @@ void ShaderEmitter::compileInstruction(const PICAShader& shaderUnit) { case ShaderOpcodes::CMP2: recCMP(shaderUnit, instruction); break; case ShaderOpcodes::DP3: recDP3(shaderUnit, instruction); break; case ShaderOpcodes::DP4: recDP4(shaderUnit, instruction); break; - // case ShaderOpcodes::DPH: - // case ShaderOpcodes::DPHI: recDPH(shaderUnit, instruction); break; + case ShaderOpcodes::DPH: + case ShaderOpcodes::DPHI: recDPH(shaderUnit, instruction); break; case ShaderOpcodes::END: recEND(shaderUnit, instruction); break; case ShaderOpcodes::EX2: recEX2(shaderUnit, instruction); break; case ShaderOpcodes::FLR: recFLR(shaderUnit, instruction); break; @@ -533,6 +533,39 @@ void ShaderEmitter::recDP4(const PICAShader& shader, u32 instruction) { storeRegister(src1Vec, shader, dest, operandDescriptor); } +void ShaderEmitter::recDPH(const PICAShader& shader, u32 instruction) { + const bool isDPHI = (instruction >> 26) == ShaderOpcodes::DPHI; + + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const u32 src1 = isDPHI ? getBits<14, 5>(instruction) : getBits<12, 7>(instruction); + const u32 src2 = isDPHI ? getBits<7, 7>(instruction) : getBits<7, 5>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + const u32 writeMask = getBits<0, 4>(operandDescriptor); + + // TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA) + loadRegister<1>(src1Vec, shader, src1, isDPHI ? 0 : idx, operandDescriptor); + loadRegister<2>(src2Vec, shader, src2, isDPHI ? idx : 0, operandDescriptor); + // // Attach 1.0 to the w component of src1 + MOV(src1Vec.Selem()[3], onesVector.Selem()[0]); + + // Now perform a DP4 + // Do a piecewise multiplication of the vectors first + if constexpr (useSafeMUL) { + emitSafeMUL(src1Vec, src2Vec, scratch1Vec); + } else { + FMUL(src1Vec.S4(), src1Vec.S4(), src2Vec.S4()); + } + FADDP(src1Vec.S4(), src1Vec.S4(), src1Vec.S4()); // Now add the adjacent components together + FADDP(src1Vec.toS(), src1Vec.toD().S2()); // Again for the bottom 2 lanes. Now the bottom lane contains the dot product + + if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x + DUP(src1Vec.S4(), src1Vec.Selem()[0]); // src1Vec = src1Vec.xxxx + } + + storeRegister(src1Vec, shader, dest, operandDescriptor); +} + oaknut::Label ShaderEmitter::emitLog2Func() { oaknut::Label funcStart; From 31902e92a98ce3b68dc2ac6a153d3d27f865cf3a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 3 Jul 2024 16:27:17 +0300 Subject: [PATCH 038/251] Enable shader JIT by default on arm64 desktop + Android --- include/config.hpp | 2 +- .../java/com/panda3ds/pandroid/data/config/GlobalConfig.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/config.hpp b/include/config.hpp index 2333c682..339e651c 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -7,7 +7,7 @@ // Remember to initialize every field here to its default value otherwise bad things will happen struct EmulatorConfig { // Only enable the shader JIT by default on platforms where it's completely tested -#ifdef PANDA3DS_X64_HOST +#if defined(PANDA3DS_X64_HOST) || defined(PANDA3DS_ARM64_HOST) static constexpr bool shaderJitDefault = true; #else static constexpr bool shaderJitDefault = false; diff --git a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java index 21645b7e..448d561a 100644 --- a/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java +++ b/src/pandroid/app/src/main/java/com/panda3ds/pandroid/data/config/GlobalConfig.java @@ -21,7 +21,7 @@ public class GlobalConfig { public static DataModel data; - public static final Key KEY_SHADER_JIT = new Key<>("emu.shader_jit", false); + public static final Key KEY_SHADER_JIT = new Key<>("emu.shader_jit", true); public static final Key KEY_PICTURE_IN_PICTURE = new Key<>("app.behavior.pictureInPicture", false); public static final Key KEY_SHOW_PERFORMANCE_OVERLAY = new Key<>("dev.performanceOverlay", false); public static final Key KEY_LOGGER_SERVICE = new Key<>("dev.loggerService", false); From d47e964c8022c4d21b11e2c81c9947fc3172137f Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Thu, 20 Jun 2024 11:18:31 +0300 Subject: [PATCH 039/251] Libretro: Initial implementation --- CMakeLists.txt | 14 +- src/libretro_core.cpp | 359 ++ third_party/libretro/include/libretro.h | 4405 +++++++++++++++++++++++ 3 files changed, 4777 insertions(+), 1 deletion(-) create mode 100644 src/libretro_core.cpp create mode 100644 third_party/libretro/include/libretro.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 80114bfa..2897560b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -40,11 +40,19 @@ option(ENABLE_DISCORD_RPC "Compile with Discord RPC support (disabled by default option(ENABLE_LUAJIT "Enable scripting with the Lua programming language" ON) option(ENABLE_QT_GUI "Enable the Qt GUI. If not selected then the emulator uses a minimal SDL-based UI instead" OFF) option(BUILD_HYDRA_CORE "Build a Hydra core" OFF) +option(BUILD_LIBRETRO_CORE "Build a Libretro core" OFF) if(BUILD_HYDRA_CORE) set(CMAKE_POSITION_INDEPENDENT_CODE ON) endif() +if(BUILD_LIBRETRO_CORE) + set(CMAKE_POSITION_INDEPENDENT_CODE ON) + set(ENABLE_DISCORD_RPC OFF) + set(ENABLE_LUAJIT OFF) + add_definitions(-D__LIBRETRO__) +endif() + add_library(AlberCore STATIC) include_directories(${PROJECT_SOURCE_DIR}/include/) @@ -438,7 +446,7 @@ else() target_compile_definitions(AlberCore PUBLIC "PANDA3DS_FRONTEND_SDL=1") endif() -if(NOT BUILD_HYDRA_CORE) +if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE) add_executable(Alber) if(ENABLE_QT_GUI) @@ -500,6 +508,10 @@ elseif(BUILD_HYDRA_CORE) include_directories(third_party/hydra_core/include) add_library(Alber SHARED src/hydra_core.cpp) target_link_libraries(Alber PUBLIC AlberCore) +elseif(BUILD_LIBRETRO_CORE) + include_directories(third_party/libretro/include) + add_library(panda3ds_libretro SHARED src/libretro_core.cpp) + target_link_libraries(panda3ds_libretro PUBLIC AlberCore) endif() if(ENABLE_LTO OR ENABLE_USER_BUILD) diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp new file mode 100644 index 00000000..ff57f0c8 --- /dev/null +++ b/src/libretro_core.cpp @@ -0,0 +1,359 @@ +#include +#include + +#include + +#include +#include + +static retro_environment_t environ_cb; +static retro_video_refresh_t video_cb; +static retro_audio_sample_batch_t audio_batch_cb; +static retro_input_poll_t input_poll_cb; +static retro_input_state_t input_state_cb; + +static struct retro_hw_render_callback hw_render; + +std::unique_ptr emulator; +RendererGL* renderer; + +static void* GetProcAddress(const char* name) { + return (void*)hw_render.get_proc_address(name); +} + +static void VideoResetContext(void) { +#ifdef USING_GLES + if (!gladLoadGLES2Loader(reinterpret_cast(GetProcAddress))) { + Helpers::panic("OpenGL ES init failed"); + } +#else + if (!gladLoadGLLoader(reinterpret_cast(GetProcAddress))) { + Helpers::panic("OpenGL init failed"); + } +#endif + + emulator->initGraphicsContext(nullptr); +} + +static void VideoDestroyContext(void) { + emulator->deinitGraphicsContext(); +} + +static bool SetHWRender(retro_hw_context_type type) { + hw_render.context_type = type; + hw_render.context_reset = VideoResetContext; + hw_render.context_destroy = VideoDestroyContext; + hw_render.bottom_left_origin = true; + + switch (type) { + case RETRO_HW_CONTEXT_OPENGL_CORE: + hw_render.version_major = 3; + hw_render.version_minor = 3; + + if (environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render)) { + return true; + } + break; + case RETRO_HW_CONTEXT_OPENGLES3: + case RETRO_HW_CONTEXT_OPENGL: + hw_render.version_major = 3; + hw_render.version_minor = 0; + + if (environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render)) { + return true; + } + break; + default: + break; + } + + return false; +} + +static void VideoInit(void) { + retro_hw_context_type preferred = RETRO_HW_CONTEXT_NONE; + environ_cb(RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER, &preferred); + + if (preferred && SetHWRender(preferred)) + return; + if (SetHWRender(RETRO_HW_CONTEXT_OPENGL_CORE)) + return; + if (SetHWRender(RETRO_HW_CONTEXT_OPENGL)) + return; + if (SetHWRender(RETRO_HW_CONTEXT_OPENGLES3)) + return; + + hw_render.context_type = RETRO_HW_CONTEXT_NONE; +} + +static bool GetButtonState(unsigned id) { + return input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, id); +} + +static float GetAxisState(unsigned index, unsigned id) { + return input_state_cb(0, RETRO_DEVICE_ANALOG, index, id); +} + +static void InputInit(void) { + static const struct retro_controller_description controllers[] = { + { "Nintendo 3DS", RETRO_DEVICE_JOYPAD }, + { NULL, 0 }, + }; + + static const struct retro_controller_info ports[] = { + { controllers, 1 }, + { NULL, 0 }, + }; + + environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports); + + struct retro_input_descriptor desc[] = { + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R, "R" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L, "L" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X, "X" }, + { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y, "Y" }, + { 0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X, "Circle Pad X" }, + { 0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y, "Circle Pad Y" }, + { 0 }, + }; + + environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, &desc); +} + +static std::string FetchVariable(std::string key, std::string def) { + struct retro_variable var = { nullptr }; + var.key = key.c_str(); + + if (!environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) || var.value == nullptr) { + Helpers::warn("Fetching variable %s failed.", key); + return def; + } + + return std::string(var.value); +} + +static bool FetchVariableBool(std::string key, bool def) { + return FetchVariable(key, def ? "enabled" : "disabled") == "enabled"; +} + +static void ConfigInit() { + static const retro_variable values[] = { + { "panda3ds_use_shader_jit", "Enable shader JIT; enabled|disabled" }, + { "panda3ds_use_vsync", "Enable VSync; enabled|disabled" }, + { "panda3ds_dsp_emulation", "DSP emulation; Null|HLE|LLE" }, + { "panda3ds_use_audio", "Enable audio; disabled|enabled" }, + { "panda3ds_use_virtual_sd", "Enable virtual SD card; enabled|disabled" }, + { "panda3ds_write_protect_virtual_sd", "Write protect virtual SD card; disabled|enabled" }, + { "panda3ds_battery_level", "Battery percentage; 5|10|20|30|50|70|90|100" }, + { "panda3ds_use_charger", "Charger plugged; enabled|disabled" }, + { nullptr, nullptr } + }; + + environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, (void*)values); +} + +static void ConfigUpdate() { + EmulatorConfig& config = emulator->getConfig(); + + config.rendererType = RendererType::OpenGL; + config.vsyncEnabled = FetchVariableBool("panda3ds_use_vsync", true); + config.shaderJitEnabled = FetchVariableBool("panda3ds_use_shader_jit", true); + config.chargerPlugged = FetchVariableBool("panda3ds_use_charger", true); + config.batteryPercentage = std::clamp(std::stoi(FetchVariable("panda3ds_battery_level", "5")), 0, 100); + config.dspType = Audio::DSPCore::typeFromString(FetchVariable("panda3ds_dsp_emulation", "null")); + config.audioEnabled = FetchVariableBool("panda3ds_use_audio", false); + config.sdCardInserted = FetchVariableBool("panda3ds_use_virtual_sd", true); + config.sdWriteProtected = FetchVariableBool("panda3ds_write_protect_virtual_sd", false); + config.discordRpcEnabled = false; +} + +void retro_get_system_info(retro_system_info* info) { + info->need_fullpath = true; + info->valid_extensions = "3ds|3dsx|elf|axf|cci|cxi|app"; + info->library_version = "0.8"; + info->library_name = "Panda3DS"; + info->block_extract = true; +} + +void retro_get_system_av_info(retro_system_av_info* info) { + info->geometry.base_width = emulator->width; + info->geometry.base_height = emulator->height; + + info->geometry.max_width = info->geometry.base_width; + info->geometry.max_height = info->geometry.base_height; + + info->geometry.aspect_ratio = 5.0 / 6.0; + info->timing.fps = 60.0; + info->timing.sample_rate = 32000; +} + +void retro_set_environment(retro_environment_t cb) { + environ_cb = cb; +} + +void retro_set_video_refresh(retro_video_refresh_t cb) { + video_cb = cb; +} + +void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) { + audio_batch_cb = cb; +} + +void retro_set_audio_sample(retro_audio_sample_t cb) { +} + +void retro_set_input_poll(retro_input_poll_t cb) { + input_poll_cb = cb; +} + +void retro_set_input_state(retro_input_state_t cb) { + input_state_cb = cb; +} + +void retro_init(void) { + enum retro_pixel_format xrgb888 = RETRO_PIXEL_FORMAT_XRGB8888; + environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &xrgb888); + + emulator = std::make_unique(); +} + +void retro_deinit(void) { + emulator = nullptr; +} + +bool retro_load_game(const struct retro_game_info* game) { + ConfigInit(); + ConfigUpdate(); + + if (emulator->getRendererType() != RendererType::OpenGL) { + throw std::runtime_error("Libretro: Renderer is not OpenGL"); + } + + renderer = static_cast(emulator->getRenderer()); + emulator->setOutputSize(emulator->width, emulator->height); + + InputInit(); + VideoInit(); + + return emulator->loadROM(game->path); +} + +bool retro_load_game_special(unsigned type, const struct retro_game_info* info, size_t num) { + return false; +} + +void retro_unload_game(void) { + renderer->setFBO(0); + renderer = nullptr; +} + +void retro_reset(void) { + emulator->reset(Emulator::ReloadOption::Reload); +} + +void retro_run(void) { + renderer->setFBO(hw_render.get_current_framebuffer()); + renderer->resetStateManager(); + + input_poll_cb(); + + HIDService& hid = emulator->getServiceManager().getHID(); + + hid.setKey(HID::Keys::A, GetButtonState(RETRO_DEVICE_ID_JOYPAD_A)); + hid.setKey(HID::Keys::B, GetButtonState(RETRO_DEVICE_ID_JOYPAD_B)); + hid.setKey(HID::Keys::X, GetButtonState(RETRO_DEVICE_ID_JOYPAD_X)); + hid.setKey(HID::Keys::Y, GetButtonState(RETRO_DEVICE_ID_JOYPAD_Y)); + hid.setKey(HID::Keys::L, GetButtonState(RETRO_DEVICE_ID_JOYPAD_L)); + hid.setKey(HID::Keys::R, GetButtonState(RETRO_DEVICE_ID_JOYPAD_R)); + hid.setKey(HID::Keys::Start, GetButtonState(RETRO_DEVICE_ID_JOYPAD_START)); + hid.setKey(HID::Keys::Select, GetButtonState(RETRO_DEVICE_ID_JOYPAD_SELECT)); + hid.setKey(HID::Keys::Up, GetButtonState(RETRO_DEVICE_ID_JOYPAD_UP)); + hid.setKey(HID::Keys::Down, GetButtonState(RETRO_DEVICE_ID_JOYPAD_DOWN)); + hid.setKey(HID::Keys::Left, GetButtonState(RETRO_DEVICE_ID_JOYPAD_LEFT)); + hid.setKey(HID::Keys::Right, GetButtonState(RETRO_DEVICE_ID_JOYPAD_RIGHT)); + + float x_left = GetAxisState(RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X); + float y_left = GetAxisState(RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y); + + hid.setCirclepadX(x_left == 0 ? 0 : x_left < 0 ? -0x9C : 0x9C); + hid.setCirclepadY(y_left == 0 ? 0 : y_left > 0 ? -0x9C : 0x9C); + + bool touch = input_state_cb(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT); + auto pos_x = input_state_cb(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X); + auto pos_y = input_state_cb(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y); + + auto new_x = static_cast((pos_x + 0x7fff) / (float)(0x7fff * 2) * emulator->width); + auto new_y = static_cast((pos_y + 0x7fff) / (float)(0x7fff * 2) * emulator->height); + + auto off_x = 40; + auto off_y = emulator->height / 2; + + bool scr_x = new_x >= off_x && new_x < emulator->width - off_x; + bool scr_y = new_y >= off_y && new_y <= emulator->height; + + if (touch && scr_y && scr_x) { + u16 x = static_cast(new_x - off_x); + u16 y = static_cast(new_y - off_y); + + hid.setTouchScreenPress(x, y); + } else { + hid.releaseTouchScreen(); + } + + hid.updateInputs(emulator->getTicks()); + + emulator->runFrame(); + video_cb(RETRO_HW_FRAME_BUFFER_VALID, emulator->width, emulator->height, 0); +} + +void retro_set_controller_port_device(unsigned port, unsigned device) { +} + +size_t retro_serialize_size(void) { + size_t size = 0; + return size; +} + +bool retro_serialize(void* data, size_t size) { + return false; +} + +bool retro_unserialize(const void* data, size_t size) { + return false; +} + +unsigned retro_get_region(void) { + return RETRO_REGION_NTSC; +} + +unsigned retro_api_version() { + return RETRO_API_VERSION; +} + +size_t retro_get_memory_size(unsigned id) { + if (id == RETRO_MEMORY_SYSTEM_RAM) { + return 0; + } + return 0; +} + +void* retro_get_memory_data(unsigned id) { + if (id == RETRO_MEMORY_SYSTEM_RAM) { + return 0; + } + return NULL; +} + +void retro_cheat_set(unsigned index, bool enabled, const char* code) { +} + +void retro_cheat_reset(void) { +} diff --git a/third_party/libretro/include/libretro.h b/third_party/libretro/include/libretro.h new file mode 100644 index 00000000..96d07df4 --- /dev/null +++ b/third_party/libretro/include/libretro.h @@ -0,0 +1,4405 @@ +/* Copyright (C) 2010-2020 The RetroArch team + * + * --------------------------------------------------------------------------------------- + * The following license statement only applies to this libretro API header (libretro.h). + * --------------------------------------------------------------------------------------- + * + * Permission is hereby granted, free of charge, + * to any person obtaining a copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, + * INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, + * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +#ifndef LIBRETRO_H__ +#define LIBRETRO_H__ + +#include +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef __cplusplus +#if defined(_MSC_VER) && _MSC_VER < 1800 && !defined(SN_TARGET_PS3) +/* Hack applied for MSVC when compiling in C89 mode + * as it isn't C99-compliant. */ +#define bool unsigned char +#define true 1 +#define false 0 +#else +#include +#endif +#endif + +#ifndef RETRO_CALLCONV +# if defined(__GNUC__) && defined(__i386__) && !defined(__x86_64__) +# define RETRO_CALLCONV __attribute__((cdecl)) +# elif defined(_MSC_VER) && defined(_M_X86) && !defined(_M_X64) +# define RETRO_CALLCONV __cdecl +# else +# define RETRO_CALLCONV /* all other platforms only have one calling convention each */ +# endif +#endif + +#ifndef RETRO_API +# if defined(_WIN32) || defined(__CYGWIN__) || defined(__MINGW32__) +# ifdef RETRO_IMPORT_SYMBOLS +# ifdef __GNUC__ +# define RETRO_API RETRO_CALLCONV __attribute__((__dllimport__)) +# else +# define RETRO_API RETRO_CALLCONV __declspec(dllimport) +# endif +# else +# ifdef __GNUC__ +# define RETRO_API RETRO_CALLCONV __attribute__((__dllexport__)) +# else +# define RETRO_API RETRO_CALLCONV __declspec(dllexport) +# endif +# endif +# else +# if defined(__GNUC__) && __GNUC__ >= 4 +# define RETRO_API RETRO_CALLCONV __attribute__((__visibility__("default"))) +# else +# define RETRO_API RETRO_CALLCONV +# endif +# endif +#endif + +/* Used for checking API/ABI mismatches that can break libretro + * implementations. + * It is not incremented for compatible changes to the API. + */ +#define RETRO_API_VERSION 1 + +/* + * Libretro's fundamental device abstractions. + * + * Libretro's input system consists of some standardized device types, + * such as a joypad (with/without analog), mouse, keyboard, lightgun + * and a pointer. + * + * The functionality of these devices are fixed, and individual cores + * map their own concept of a controller to libretro's abstractions. + * This makes it possible for frontends to map the abstract types to a + * real input device, and not having to worry about binding input + * correctly to arbitrary controller layouts. + */ + +#define RETRO_DEVICE_TYPE_SHIFT 8 +#define RETRO_DEVICE_MASK ((1 << RETRO_DEVICE_TYPE_SHIFT) - 1) +#define RETRO_DEVICE_SUBCLASS(base, id) (((id + 1) << RETRO_DEVICE_TYPE_SHIFT) | base) + +/* Input disabled. */ +#define RETRO_DEVICE_NONE 0 + +/* The JOYPAD is called RetroPad. It is essentially a Super Nintendo + * controller, but with additional L2/R2/L3/R3 buttons, similar to a + * PS1 DualShock. */ +#define RETRO_DEVICE_JOYPAD 1 + +/* The mouse is a simple mouse, similar to Super Nintendo's mouse. + * X and Y coordinates are reported relatively to last poll (poll callback). + * It is up to the libretro implementation to keep track of where the mouse + * pointer is supposed to be on the screen. + * The frontend must make sure not to interfere with its own hardware + * mouse pointer. + */ +#define RETRO_DEVICE_MOUSE 2 + +/* KEYBOARD device lets one poll for raw key pressed. + * It is poll based, so input callback will return with the current + * pressed state. + * For event/text based keyboard input, see + * RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK. + */ +#define RETRO_DEVICE_KEYBOARD 3 + +/* LIGHTGUN device is similar to Guncon-2 for PlayStation 2. + * It reports X/Y coordinates in screen space (similar to the pointer) + * in the range [-0x8000, 0x7fff] in both axes, with zero being center and + * -0x8000 being out of bounds. + * As well as reporting on/off screen state. It features a trigger, + * start/select buttons, auxiliary action buttons and a + * directional pad. A forced off-screen shot can be requested for + * auto-reloading function in some games. + */ +#define RETRO_DEVICE_LIGHTGUN 4 + +/* The ANALOG device is an extension to JOYPAD (RetroPad). + * Similar to DualShock2 it adds two analog sticks and all buttons can + * be analog. This is treated as a separate device type as it returns + * axis values in the full analog range of [-0x7fff, 0x7fff], + * although some devices may return -0x8000. + * Positive X axis is right. Positive Y axis is down. + * Buttons are returned in the range [0, 0x7fff]. + * Only use ANALOG type when polling for analog values. + */ +#define RETRO_DEVICE_ANALOG 5 + +/* Abstracts the concept of a pointing mechanism, e.g. touch. + * This allows libretro to query in absolute coordinates where on the + * screen a mouse (or something similar) is being placed. + * For a touch centric device, coordinates reported are the coordinates + * of the press. + * + * Coordinates in X and Y are reported as: + * [-0x7fff, 0x7fff]: -0x7fff corresponds to the far left/top of the screen, + * and 0x7fff corresponds to the far right/bottom of the screen. + * The "screen" is here defined as area that is passed to the frontend and + * later displayed on the monitor. + * + * The frontend is free to scale/resize this screen as it sees fit, however, + * (X, Y) = (-0x7fff, -0x7fff) will correspond to the top-left pixel of the + * game image, etc. + * + * To check if the pointer coordinates are valid (e.g. a touch display + * actually being touched), PRESSED returns 1 or 0. + * + * If using a mouse on a desktop, PRESSED will usually correspond to the + * left mouse button, but this is a frontend decision. + * PRESSED will only return 1 if the pointer is inside the game screen. + * + * For multi-touch, the index variable can be used to successively query + * more presses. + * If index = 0 returns true for _PRESSED, coordinates can be extracted + * with _X, _Y for index = 0. One can then query _PRESSED, _X, _Y with + * index = 1, and so on. + * Eventually _PRESSED will return false for an index. No further presses + * are registered at this point. */ +#define RETRO_DEVICE_POINTER 6 + +/* Buttons for the RetroPad (JOYPAD). + * The placement of these is equivalent to placements on the + * Super Nintendo controller. + * L2/R2/L3/R3 buttons correspond to the PS1 DualShock. + * Also used as id values for RETRO_DEVICE_INDEX_ANALOG_BUTTON */ +#define RETRO_DEVICE_ID_JOYPAD_B 0 +#define RETRO_DEVICE_ID_JOYPAD_Y 1 +#define RETRO_DEVICE_ID_JOYPAD_SELECT 2 +#define RETRO_DEVICE_ID_JOYPAD_START 3 +#define RETRO_DEVICE_ID_JOYPAD_UP 4 +#define RETRO_DEVICE_ID_JOYPAD_DOWN 5 +#define RETRO_DEVICE_ID_JOYPAD_LEFT 6 +#define RETRO_DEVICE_ID_JOYPAD_RIGHT 7 +#define RETRO_DEVICE_ID_JOYPAD_A 8 +#define RETRO_DEVICE_ID_JOYPAD_X 9 +#define RETRO_DEVICE_ID_JOYPAD_L 10 +#define RETRO_DEVICE_ID_JOYPAD_R 11 +#define RETRO_DEVICE_ID_JOYPAD_L2 12 +#define RETRO_DEVICE_ID_JOYPAD_R2 13 +#define RETRO_DEVICE_ID_JOYPAD_L3 14 +#define RETRO_DEVICE_ID_JOYPAD_R3 15 + +#define RETRO_DEVICE_ID_JOYPAD_MASK 256 + +/* Index / Id values for ANALOG device. */ +#define RETRO_DEVICE_INDEX_ANALOG_LEFT 0 +#define RETRO_DEVICE_INDEX_ANALOG_RIGHT 1 +#define RETRO_DEVICE_INDEX_ANALOG_BUTTON 2 +#define RETRO_DEVICE_ID_ANALOG_X 0 +#define RETRO_DEVICE_ID_ANALOG_Y 1 + +/* Id values for MOUSE. */ +#define RETRO_DEVICE_ID_MOUSE_X 0 +#define RETRO_DEVICE_ID_MOUSE_Y 1 +#define RETRO_DEVICE_ID_MOUSE_LEFT 2 +#define RETRO_DEVICE_ID_MOUSE_RIGHT 3 +#define RETRO_DEVICE_ID_MOUSE_WHEELUP 4 +#define RETRO_DEVICE_ID_MOUSE_WHEELDOWN 5 +#define RETRO_DEVICE_ID_MOUSE_MIDDLE 6 +#define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELUP 7 +#define RETRO_DEVICE_ID_MOUSE_HORIZ_WHEELDOWN 8 +#define RETRO_DEVICE_ID_MOUSE_BUTTON_4 9 +#define RETRO_DEVICE_ID_MOUSE_BUTTON_5 10 + +/* Id values for LIGHTGUN. */ +#define RETRO_DEVICE_ID_LIGHTGUN_SCREEN_X 13 /*Absolute Position*/ +#define RETRO_DEVICE_ID_LIGHTGUN_SCREEN_Y 14 /*Absolute*/ +#define RETRO_DEVICE_ID_LIGHTGUN_IS_OFFSCREEN 15 /*Status Check*/ +#define RETRO_DEVICE_ID_LIGHTGUN_TRIGGER 2 +#define RETRO_DEVICE_ID_LIGHTGUN_RELOAD 16 /*Forced off-screen shot*/ +#define RETRO_DEVICE_ID_LIGHTGUN_AUX_A 3 +#define RETRO_DEVICE_ID_LIGHTGUN_AUX_B 4 +#define RETRO_DEVICE_ID_LIGHTGUN_START 6 +#define RETRO_DEVICE_ID_LIGHTGUN_SELECT 7 +#define RETRO_DEVICE_ID_LIGHTGUN_AUX_C 8 +#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_UP 9 +#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_DOWN 10 +#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_LEFT 11 +#define RETRO_DEVICE_ID_LIGHTGUN_DPAD_RIGHT 12 +/* deprecated */ +#define RETRO_DEVICE_ID_LIGHTGUN_X 0 /*Relative Position*/ +#define RETRO_DEVICE_ID_LIGHTGUN_Y 1 /*Relative*/ +#define RETRO_DEVICE_ID_LIGHTGUN_CURSOR 3 /*Use Aux:A*/ +#define RETRO_DEVICE_ID_LIGHTGUN_TURBO 4 /*Use Aux:B*/ +#define RETRO_DEVICE_ID_LIGHTGUN_PAUSE 5 /*Use Start*/ + +/* Id values for POINTER. */ +#define RETRO_DEVICE_ID_POINTER_X 0 +#define RETRO_DEVICE_ID_POINTER_Y 1 +#define RETRO_DEVICE_ID_POINTER_PRESSED 2 +#define RETRO_DEVICE_ID_POINTER_COUNT 3 + +/* Returned from retro_get_region(). */ +#define RETRO_REGION_NTSC 0 +#define RETRO_REGION_PAL 1 + +/* Id values for LANGUAGE */ +enum retro_language +{ + RETRO_LANGUAGE_ENGLISH = 0, + RETRO_LANGUAGE_JAPANESE = 1, + RETRO_LANGUAGE_FRENCH = 2, + RETRO_LANGUAGE_SPANISH = 3, + RETRO_LANGUAGE_GERMAN = 4, + RETRO_LANGUAGE_ITALIAN = 5, + RETRO_LANGUAGE_DUTCH = 6, + RETRO_LANGUAGE_PORTUGUESE_BRAZIL = 7, + RETRO_LANGUAGE_PORTUGUESE_PORTUGAL = 8, + RETRO_LANGUAGE_RUSSIAN = 9, + RETRO_LANGUAGE_KOREAN = 10, + RETRO_LANGUAGE_CHINESE_TRADITIONAL = 11, + RETRO_LANGUAGE_CHINESE_SIMPLIFIED = 12, + RETRO_LANGUAGE_ESPERANTO = 13, + RETRO_LANGUAGE_POLISH = 14, + RETRO_LANGUAGE_VIETNAMESE = 15, + RETRO_LANGUAGE_ARABIC = 16, + RETRO_LANGUAGE_GREEK = 17, + RETRO_LANGUAGE_TURKISH = 18, + RETRO_LANGUAGE_SLOVAK = 19, + RETRO_LANGUAGE_PERSIAN = 20, + RETRO_LANGUAGE_HEBREW = 21, + RETRO_LANGUAGE_ASTURIAN = 22, + RETRO_LANGUAGE_FINNISH = 23, + RETRO_LANGUAGE_INDONESIAN = 24, + RETRO_LANGUAGE_SWEDISH = 25, + RETRO_LANGUAGE_UKRAINIAN = 26, + RETRO_LANGUAGE_CZECH = 27, + RETRO_LANGUAGE_CATALAN_VALENCIA = 28, + RETRO_LANGUAGE_CATALAN = 29, + RETRO_LANGUAGE_BRITISH_ENGLISH = 30, + RETRO_LANGUAGE_HUNGARIAN = 31, + RETRO_LANGUAGE_BELARUSIAN = 32, + RETRO_LANGUAGE_LAST, + + /* Ensure sizeof(enum) == sizeof(int) */ + RETRO_LANGUAGE_DUMMY = INT_MAX +}; + +/* Passed to retro_get_memory_data/size(). + * If the memory type doesn't apply to the + * implementation NULL/0 can be returned. + */ +#define RETRO_MEMORY_MASK 0xff + +/* Regular save RAM. This RAM is usually found on a game cartridge, + * backed up by a battery. + * If save game data is too complex for a single memory buffer, + * the SAVE_DIRECTORY (preferably) or SYSTEM_DIRECTORY environment + * callback can be used. */ +#define RETRO_MEMORY_SAVE_RAM 0 + +/* Some games have a built-in clock to keep track of time. + * This memory is usually just a couple of bytes to keep track of time. + */ +#define RETRO_MEMORY_RTC 1 + +/* System ram lets a frontend peek into a game systems main RAM. */ +#define RETRO_MEMORY_SYSTEM_RAM 2 + +/* Video ram lets a frontend peek into a game systems video RAM (VRAM). */ +#define RETRO_MEMORY_VIDEO_RAM 3 + +/* Keysyms used for ID in input state callback when polling RETRO_KEYBOARD. */ +enum retro_key +{ + RETROK_UNKNOWN = 0, + RETROK_FIRST = 0, + RETROK_BACKSPACE = 8, + RETROK_TAB = 9, + RETROK_CLEAR = 12, + RETROK_RETURN = 13, + RETROK_PAUSE = 19, + RETROK_ESCAPE = 27, + RETROK_SPACE = 32, + RETROK_EXCLAIM = 33, + RETROK_QUOTEDBL = 34, + RETROK_HASH = 35, + RETROK_DOLLAR = 36, + RETROK_AMPERSAND = 38, + RETROK_QUOTE = 39, + RETROK_LEFTPAREN = 40, + RETROK_RIGHTPAREN = 41, + RETROK_ASTERISK = 42, + RETROK_PLUS = 43, + RETROK_COMMA = 44, + RETROK_MINUS = 45, + RETROK_PERIOD = 46, + RETROK_SLASH = 47, + RETROK_0 = 48, + RETROK_1 = 49, + RETROK_2 = 50, + RETROK_3 = 51, + RETROK_4 = 52, + RETROK_5 = 53, + RETROK_6 = 54, + RETROK_7 = 55, + RETROK_8 = 56, + RETROK_9 = 57, + RETROK_COLON = 58, + RETROK_SEMICOLON = 59, + RETROK_LESS = 60, + RETROK_EQUALS = 61, + RETROK_GREATER = 62, + RETROK_QUESTION = 63, + RETROK_AT = 64, + RETROK_LEFTBRACKET = 91, + RETROK_BACKSLASH = 92, + RETROK_RIGHTBRACKET = 93, + RETROK_CARET = 94, + RETROK_UNDERSCORE = 95, + RETROK_BACKQUOTE = 96, + RETROK_a = 97, + RETROK_b = 98, + RETROK_c = 99, + RETROK_d = 100, + RETROK_e = 101, + RETROK_f = 102, + RETROK_g = 103, + RETROK_h = 104, + RETROK_i = 105, + RETROK_j = 106, + RETROK_k = 107, + RETROK_l = 108, + RETROK_m = 109, + RETROK_n = 110, + RETROK_o = 111, + RETROK_p = 112, + RETROK_q = 113, + RETROK_r = 114, + RETROK_s = 115, + RETROK_t = 116, + RETROK_u = 117, + RETROK_v = 118, + RETROK_w = 119, + RETROK_x = 120, + RETROK_y = 121, + RETROK_z = 122, + RETROK_LEFTBRACE = 123, + RETROK_BAR = 124, + RETROK_RIGHTBRACE = 125, + RETROK_TILDE = 126, + RETROK_DELETE = 127, + + RETROK_KP0 = 256, + RETROK_KP1 = 257, + RETROK_KP2 = 258, + RETROK_KP3 = 259, + RETROK_KP4 = 260, + RETROK_KP5 = 261, + RETROK_KP6 = 262, + RETROK_KP7 = 263, + RETROK_KP8 = 264, + RETROK_KP9 = 265, + RETROK_KP_PERIOD = 266, + RETROK_KP_DIVIDE = 267, + RETROK_KP_MULTIPLY = 268, + RETROK_KP_MINUS = 269, + RETROK_KP_PLUS = 270, + RETROK_KP_ENTER = 271, + RETROK_KP_EQUALS = 272, + + RETROK_UP = 273, + RETROK_DOWN = 274, + RETROK_RIGHT = 275, + RETROK_LEFT = 276, + RETROK_INSERT = 277, + RETROK_HOME = 278, + RETROK_END = 279, + RETROK_PAGEUP = 280, + RETROK_PAGEDOWN = 281, + + RETROK_F1 = 282, + RETROK_F2 = 283, + RETROK_F3 = 284, + RETROK_F4 = 285, + RETROK_F5 = 286, + RETROK_F6 = 287, + RETROK_F7 = 288, + RETROK_F8 = 289, + RETROK_F9 = 290, + RETROK_F10 = 291, + RETROK_F11 = 292, + RETROK_F12 = 293, + RETROK_F13 = 294, + RETROK_F14 = 295, + RETROK_F15 = 296, + + RETROK_NUMLOCK = 300, + RETROK_CAPSLOCK = 301, + RETROK_SCROLLOCK = 302, + RETROK_RSHIFT = 303, + RETROK_LSHIFT = 304, + RETROK_RCTRL = 305, + RETROK_LCTRL = 306, + RETROK_RALT = 307, + RETROK_LALT = 308, + RETROK_RMETA = 309, + RETROK_LMETA = 310, + RETROK_LSUPER = 311, + RETROK_RSUPER = 312, + RETROK_MODE = 313, + RETROK_COMPOSE = 314, + + RETROK_HELP = 315, + RETROK_PRINT = 316, + RETROK_SYSREQ = 317, + RETROK_BREAK = 318, + RETROK_MENU = 319, + RETROK_POWER = 320, + RETROK_EURO = 321, + RETROK_UNDO = 322, + RETROK_OEM_102 = 323, + + RETROK_LAST, + + RETROK_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */ +}; + +enum retro_mod +{ + RETROKMOD_NONE = 0x0000, + + RETROKMOD_SHIFT = 0x01, + RETROKMOD_CTRL = 0x02, + RETROKMOD_ALT = 0x04, + RETROKMOD_META = 0x08, + + RETROKMOD_NUMLOCK = 0x10, + RETROKMOD_CAPSLOCK = 0x20, + RETROKMOD_SCROLLOCK = 0x40, + + RETROKMOD_DUMMY = INT_MAX /* Ensure sizeof(enum) == sizeof(int) */ +}; + +/* If set, this call is not part of the public libretro API yet. It can + * change or be removed at any time. */ +#define RETRO_ENVIRONMENT_EXPERIMENTAL 0x10000 +/* Environment callback to be used internally in frontend. */ +#define RETRO_ENVIRONMENT_PRIVATE 0x20000 + +/* Environment commands. */ +#define RETRO_ENVIRONMENT_SET_ROTATION 1 /* const unsigned * -- + * Sets screen rotation of graphics. + * Valid values are 0, 1, 2, 3, which rotates screen by 0, 90, 180, + * 270 degrees counter-clockwise respectively. + */ +#define RETRO_ENVIRONMENT_GET_OVERSCAN 2 /* bool * -- + * NOTE: As of 2019 this callback is considered deprecated in favor of + * using core options to manage overscan in a more nuanced, core-specific way. + * + * Boolean value whether or not the implementation should use overscan, + * or crop away overscan. + */ +#define RETRO_ENVIRONMENT_GET_CAN_DUPE 3 /* bool * -- + * Boolean value whether or not frontend supports frame duping, + * passing NULL to video frame callback. + */ + + /* Environ 4, 5 are no longer supported (GET_VARIABLE / SET_VARIABLES), + * and reserved to avoid possible ABI clash. + */ + +#define RETRO_ENVIRONMENT_SET_MESSAGE 6 /* const struct retro_message * -- + * Sets a message to be displayed in implementation-specific manner + * for a certain amount of 'frames'. + * Should not be used for trivial messages, which should simply be + * logged via RETRO_ENVIRONMENT_GET_LOG_INTERFACE (or as a + * fallback, stderr). + */ +#define RETRO_ENVIRONMENT_SHUTDOWN 7 /* N/A (NULL) -- + * Requests the frontend to shutdown. + * Should only be used if game has a specific + * way to shutdown the game from a menu item or similar. + */ +#define RETRO_ENVIRONMENT_SET_PERFORMANCE_LEVEL 8 + /* const unsigned * -- + * Gives a hint to the frontend how demanding this implementation + * is on a system. E.g. reporting a level of 2 means + * this implementation should run decently on all frontends + * of level 2 and up. + * + * It can be used by the frontend to potentially warn + * about too demanding implementations. + * + * The levels are "floating". + * + * This function can be called on a per-game basis, + * as certain games an implementation can play might be + * particularly demanding. + * If called, it should be called in retro_load_game(). + */ +#define RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY 9 + /* const char ** -- + * Returns the "system" directory of the frontend. + * This directory can be used to store system specific + * content such as BIOSes, configuration data, etc. + * The returned value can be NULL. + * If so, no such directory is defined, + * and it's up to the implementation to find a suitable directory. + * + * NOTE: Some cores used this folder also for "save" data such as + * memory cards, etc, for lack of a better place to put it. + * This is now discouraged, and if possible, cores should try to + * use the new GET_SAVE_DIRECTORY. + */ +#define RETRO_ENVIRONMENT_SET_PIXEL_FORMAT 10 + /* const enum retro_pixel_format * -- + * Sets the internal pixel format used by the implementation. + * The default pixel format is RETRO_PIXEL_FORMAT_0RGB1555. + * This pixel format however, is deprecated (see enum retro_pixel_format). + * If the call returns false, the frontend does not support this pixel + * format. + * + * This function should be called inside retro_load_game() or + * retro_get_system_av_info(). + */ +#define RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS 11 + /* const struct retro_input_descriptor * -- + * Sets an array of retro_input_descriptors. + * It is up to the frontend to present this in a usable way. + * The array is terminated by retro_input_descriptor::description + * being set to NULL. + * This function can be called at any time, but it is recommended + * to call it as early as possible. + */ +#define RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK 12 + /* const struct retro_keyboard_callback * -- + * Sets a callback function used to notify core about keyboard events. + */ +#define RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE 13 + /* const struct retro_disk_control_callback * -- + * Sets an interface which frontend can use to eject and insert + * disk images. + * This is used for games which consist of multiple images and + * must be manually swapped out by the user (e.g. PSX). + */ +#define RETRO_ENVIRONMENT_SET_HW_RENDER 14 + /* struct retro_hw_render_callback * -- + * Sets an interface to let a libretro core render with + * hardware acceleration. + * Should be called in retro_load_game(). + * If successful, libretro cores will be able to render to a + * frontend-provided framebuffer. + * The size of this framebuffer will be at least as large as + * max_width/max_height provided in get_av_info(). + * If HW rendering is used, pass only RETRO_HW_FRAME_BUFFER_VALID or + * NULL to retro_video_refresh_t. + */ +#define RETRO_ENVIRONMENT_GET_VARIABLE 15 + /* struct retro_variable * -- + * Interface to acquire user-defined information from environment + * that cannot feasibly be supported in a multi-system way. + * 'key' should be set to a key which has already been set by + * SET_VARIABLES. + * 'data' will be set to a value or NULL. + */ +#define RETRO_ENVIRONMENT_SET_VARIABLES 16 + /* const struct retro_variable * -- + * Allows an implementation to signal the environment + * which variables it might want to check for later using + * GET_VARIABLE. + * This allows the frontend to present these variables to + * a user dynamically. + * This should be called the first time as early as + * possible (ideally in retro_set_environment). + * Afterward it may be called again for the core to communicate + * updated options to the frontend, but the number of core + * options must not change from the number in the initial call. + * + * 'data' points to an array of retro_variable structs + * terminated by a { NULL, NULL } element. + * retro_variable::key should be namespaced to not collide + * with other implementations' keys. E.g. A core called + * 'foo' should use keys named as 'foo_option'. + * retro_variable::value should contain a human readable + * description of the key as well as a '|' delimited list + * of expected values. + * + * The number of possible options should be very limited, + * i.e. it should be feasible to cycle through options + * without a keyboard. + * + * First entry should be treated as a default. + * + * Example entry: + * { "foo_option", "Speed hack coprocessor X; false|true" } + * + * Text before first ';' is description. This ';' must be + * followed by a space, and followed by a list of possible + * values split up with '|'. + * + * Only strings are operated on. The possible values will + * generally be displayed and stored as-is by the frontend. + */ +#define RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE 17 + /* bool * -- + * Result is set to true if some variables are updated by + * frontend since last call to RETRO_ENVIRONMENT_GET_VARIABLE. + * Variables should be queried with GET_VARIABLE. + */ +#define RETRO_ENVIRONMENT_SET_SUPPORT_NO_GAME 18 + /* const bool * -- + * If true, the libretro implementation supports calls to + * retro_load_game() with NULL as argument. + * Used by cores which can run without particular game data. + * This should be called within retro_set_environment() only. + */ +#define RETRO_ENVIRONMENT_GET_LIBRETRO_PATH 19 + /* const char ** -- + * Retrieves the absolute path from where this libretro + * implementation was loaded. + * NULL is returned if the libretro was loaded statically + * (i.e. linked statically to frontend), or if the path cannot be + * determined. + * Mostly useful in cooperation with SET_SUPPORT_NO_GAME as assets can + * be loaded without ugly hacks. + */ + + /* Environment 20 was an obsolete version of SET_AUDIO_CALLBACK. + * It was not used by any known core at the time, + * and was removed from the API. */ +#define RETRO_ENVIRONMENT_SET_FRAME_TIME_CALLBACK 21 + /* const struct retro_frame_time_callback * -- + * Lets the core know how much time has passed since last + * invocation of retro_run(). + * The frontend can tamper with the timing to fake fast-forward, + * slow-motion, frame stepping, etc. + * In this case the delta time will use the reference value + * in frame_time_callback.. + */ +#define RETRO_ENVIRONMENT_SET_AUDIO_CALLBACK 22 + /* const struct retro_audio_callback * -- + * Sets an interface which is used to notify a libretro core about audio + * being available for writing. + * The callback can be called from any thread, so a core using this must + * have a thread safe audio implementation. + * It is intended for games where audio and video are completely + * asynchronous and audio can be generated on the fly. + * This interface is not recommended for use with emulators which have + * highly synchronous audio. + * + * The callback only notifies about writability; the libretro core still + * has to call the normal audio callbacks + * to write audio. The audio callbacks must be called from within the + * notification callback. + * The amount of audio data to write is up to the implementation. + * Generally, the audio callback will be called continously in a loop. + * + * Due to thread safety guarantees and lack of sync between audio and + * video, a frontend can selectively disallow this interface based on + * internal configuration. A core using this interface must also + * implement the "normal" audio interface. + * + * A libretro core using SET_AUDIO_CALLBACK should also make use of + * SET_FRAME_TIME_CALLBACK. + */ +#define RETRO_ENVIRONMENT_GET_RUMBLE_INTERFACE 23 + /* struct retro_rumble_interface * -- + * Gets an interface which is used by a libretro core to set + * state of rumble motors in controllers. + * A strong and weak motor is supported, and they can be + * controlled indepedently. + * Should be called from either retro_init() or retro_load_game(). + * Should not be called from retro_set_environment(). + * Returns false if rumble functionality is unavailable. + */ +#define RETRO_ENVIRONMENT_GET_INPUT_DEVICE_CAPABILITIES 24 + /* uint64_t * -- + * Gets a bitmask telling which device type are expected to be + * handled properly in a call to retro_input_state_t. + * Devices which are not handled or recognized always return + * 0 in retro_input_state_t. + * Example bitmask: caps = (1 << RETRO_DEVICE_JOYPAD) | (1 << RETRO_DEVICE_ANALOG). + * Should only be called in retro_run(). + */ +#define RETRO_ENVIRONMENT_GET_SENSOR_INTERFACE (25 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_sensor_interface * -- + * Gets access to the sensor interface. + * The purpose of this interface is to allow + * setting state related to sensors such as polling rate, + * enabling/disable it entirely, etc. + * Reading sensor state is done via the normal + * input_state_callback API. + */ +#define RETRO_ENVIRONMENT_GET_CAMERA_INTERFACE (26 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_camera_callback * -- + * Gets an interface to a video camera driver. + * A libretro core can use this interface to get access to a + * video camera. + * New video frames are delivered in a callback in same + * thread as retro_run(). + * + * GET_CAMERA_INTERFACE should be called in retro_load_game(). + * + * Depending on the camera implementation used, camera frames + * will be delivered as a raw framebuffer, + * or as an OpenGL texture directly. + * + * The core has to tell the frontend here which types of + * buffers can be handled properly. + * An OpenGL texture can only be handled when using a + * libretro GL core (SET_HW_RENDER). + * It is recommended to use a libretro GL core when + * using camera interface. + * + * The camera is not started automatically. The retrieved start/stop + * functions must be used to explicitly + * start and stop the camera driver. + */ +#define RETRO_ENVIRONMENT_GET_LOG_INTERFACE 27 + /* struct retro_log_callback * -- + * Gets an interface for logging. This is useful for + * logging in a cross-platform way + * as certain platforms cannot use stderr for logging. + * It also allows the frontend to + * show logging information in a more suitable way. + * If this interface is not used, libretro cores should + * log to stderr as desired. + */ +#define RETRO_ENVIRONMENT_GET_PERF_INTERFACE 28 + /* struct retro_perf_callback * -- + * Gets an interface for performance counters. This is useful + * for performance logging in a cross-platform way and for detecting + * architecture-specific features, such as SIMD support. + */ +#define RETRO_ENVIRONMENT_GET_LOCATION_INTERFACE 29 + /* struct retro_location_callback * -- + * Gets access to the location interface. + * The purpose of this interface is to be able to retrieve + * location-based information from the host device, + * such as current latitude / longitude. + */ +#define RETRO_ENVIRONMENT_GET_CONTENT_DIRECTORY 30 /* Old name, kept for compatibility. */ +#define RETRO_ENVIRONMENT_GET_CORE_ASSETS_DIRECTORY 30 + /* const char ** -- + * Returns the "core assets" directory of the frontend. + * This directory can be used to store specific assets that the + * core relies upon, such as art assets, + * input data, etc etc. + * The returned value can be NULL. + * If so, no such directory is defined, + * and it's up to the implementation to find a suitable directory. + */ +#define RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY 31 + /* const char ** -- + * Returns the "save" directory of the frontend, unless there is no + * save directory available. The save directory should be used to + * store SRAM, memory cards, high scores, etc, if the libretro core + * cannot use the regular memory interface (retro_get_memory_data()). + * + * If the frontend cannot designate a save directory, it will return + * NULL to indicate that the core should attempt to operate without a + * save directory set. + * + * NOTE: early libretro cores used the system directory for save + * files. Cores that need to be backwards-compatible can still check + * GET_SYSTEM_DIRECTORY. + */ +#define RETRO_ENVIRONMENT_SET_SYSTEM_AV_INFO 32 + /* const struct retro_system_av_info * -- + * Sets a new av_info structure. This can only be called from + * within retro_run(). + * This should *only* be used if the core is completely altering the + * internal resolutions, aspect ratios, timings, sampling rate, etc. + * Calling this can require a full reinitialization of video/audio + * drivers in the frontend, + * + * so it is important to call it very sparingly, and usually only with + * the users explicit consent. + * An eventual driver reinitialize will happen so that video and + * audio callbacks + * happening after this call within the same retro_run() call will + * target the newly initialized driver. + * + * This callback makes it possible to support configurable resolutions + * in games, which can be useful to + * avoid setting the "worst case" in max_width/max_height. + * + * ***HIGHLY RECOMMENDED*** Do not call this callback every time + * resolution changes in an emulator core if it's + * expected to be a temporary change, for the reasons of possible + * driver reinitialization. + * This call is not a free pass for not trying to provide + * correct values in retro_get_system_av_info(). If you need to change + * things like aspect ratio or nominal width/height, + * use RETRO_ENVIRONMENT_SET_GEOMETRY, which is a softer variant + * of SET_SYSTEM_AV_INFO. + * + * If this returns false, the frontend does not acknowledge a + * changed av_info struct. + */ +#define RETRO_ENVIRONMENT_SET_PROC_ADDRESS_CALLBACK 33 + /* const struct retro_get_proc_address_interface * -- + * Allows a libretro core to announce support for the + * get_proc_address() interface. + * This interface allows for a standard way to extend libretro where + * use of environment calls are too indirect, + * e.g. for cases where the frontend wants to call directly into the core. + * + * If a core wants to expose this interface, SET_PROC_ADDRESS_CALLBACK + * **MUST** be called from within retro_set_environment(). + */ +#define RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO 34 + /* const struct retro_subsystem_info * -- + * This environment call introduces the concept of libretro "subsystems". + * A subsystem is a variant of a libretro core which supports + * different kinds of games. + * The purpose of this is to support e.g. emulators which might + * have special needs, e.g. Super Nintendo's Super GameBoy, Sufami Turbo. + * It can also be used to pick among subsystems in an explicit way + * if the libretro implementation is a multi-system emulator itself. + * + * Loading a game via a subsystem is done with retro_load_game_special(), + * and this environment call allows a libretro core to expose which + * subsystems are supported for use with retro_load_game_special(). + * A core passes an array of retro_game_special_info which is terminated + * with a zeroed out retro_game_special_info struct. + * + * If a core wants to use this functionality, SET_SUBSYSTEM_INFO + * **MUST** be called from within retro_set_environment(). + */ +#define RETRO_ENVIRONMENT_SET_CONTROLLER_INFO 35 + /* const struct retro_controller_info * -- + * This environment call lets a libretro core tell the frontend + * which controller subclasses are recognized in calls to + * retro_set_controller_port_device(). + * + * Some emulators such as Super Nintendo support multiple lightgun + * types which must be specifically selected from. It is therefore + * sometimes necessary for a frontend to be able to tell the core + * about a special kind of input device which is not specifcally + * provided by the Libretro API. + * + * In order for a frontend to understand the workings of those devices, + * they must be defined as a specialized subclass of the generic device + * types already defined in the libretro API. + * + * The core must pass an array of const struct retro_controller_info which + * is terminated with a blanked out struct. Each element of the + * retro_controller_info struct corresponds to the ascending port index + * that is passed to retro_set_controller_port_device() when that function + * is called to indicate to the core that the frontend has changed the + * active device subclass. SEE ALSO: retro_set_controller_port_device() + * + * The ascending input port indexes provided by the core in the struct + * are generally presented by frontends as ascending User # or Player #, + * such as Player 1, Player 2, Player 3, etc. Which device subclasses are + * supported can vary per input port. + * + * The first inner element of each entry in the retro_controller_info array + * is a retro_controller_description struct that specifies the names and + * codes of all device subclasses that are available for the corresponding + * User or Player, beginning with the generic Libretro device that the + * subclasses are derived from. The second inner element of each entry is the + * total number of subclasses that are listed in the retro_controller_description. + * + * NOTE: Even if special device types are set in the libretro core, + * libretro should only poll input based on the base input device types. + */ +#define RETRO_ENVIRONMENT_SET_MEMORY_MAPS (36 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* const struct retro_memory_map * -- + * This environment call lets a libretro core tell the frontend + * about the memory maps this core emulates. + * This can be used to implement, for example, cheats in a core-agnostic way. + * + * Should only be used by emulators; it doesn't make much sense for + * anything else. + * It is recommended to expose all relevant pointers through + * retro_get_memory_* as well. + */ +#define RETRO_ENVIRONMENT_SET_GEOMETRY 37 + /* const struct retro_game_geometry * -- + * This environment call is similar to SET_SYSTEM_AV_INFO for changing + * video parameters, but provides a guarantee that drivers will not be + * reinitialized. + * This can only be called from within retro_run(). + * + * The purpose of this call is to allow a core to alter nominal + * width/heights as well as aspect ratios on-the-fly, which can be + * useful for some emulators to change in run-time. + * + * max_width/max_height arguments are ignored and cannot be changed + * with this call as this could potentially require a reinitialization or a + * non-constant time operation. + * If max_width/max_height are to be changed, SET_SYSTEM_AV_INFO is required. + * + * A frontend must guarantee that this environment call completes in + * constant time. + */ +#define RETRO_ENVIRONMENT_GET_USERNAME 38 + /* const char ** + * Returns the specified username of the frontend, if specified by the user. + * This username can be used as a nickname for a core that has online facilities + * or any other mode where personalization of the user is desirable. + * The returned value can be NULL. + * If this environ callback is used by a core that requires a valid username, + * a default username should be specified by the core. + */ +#define RETRO_ENVIRONMENT_GET_LANGUAGE 39 + /* unsigned * -- + * Returns the specified language of the frontend, if specified by the user. + * It can be used by the core for localization purposes. + */ +#define RETRO_ENVIRONMENT_GET_CURRENT_SOFTWARE_FRAMEBUFFER (40 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_framebuffer * -- + * Returns a preallocated framebuffer which the core can use for rendering + * the frame into when not using SET_HW_RENDER. + * The framebuffer returned from this call must not be used + * after the current call to retro_run() returns. + * + * The goal of this call is to allow zero-copy behavior where a core + * can render directly into video memory, avoiding extra bandwidth cost by copying + * memory from core to video memory. + * + * If this call succeeds and the core renders into it, + * the framebuffer pointer and pitch can be passed to retro_video_refresh_t. + * If the buffer from GET_CURRENT_SOFTWARE_FRAMEBUFFER is to be used, + * the core must pass the exact + * same pointer as returned by GET_CURRENT_SOFTWARE_FRAMEBUFFER; + * i.e. passing a pointer which is offset from the + * buffer is undefined. The width, height and pitch parameters + * must also match exactly to the values obtained from GET_CURRENT_SOFTWARE_FRAMEBUFFER. + * + * It is possible for a frontend to return a different pixel format + * than the one used in SET_PIXEL_FORMAT. This can happen if the frontend + * needs to perform conversion. + * + * It is still valid for a core to render to a different buffer + * even if GET_CURRENT_SOFTWARE_FRAMEBUFFER succeeds. + * + * A frontend must make sure that the pointer obtained from this function is + * writeable (and readable). + */ +#define RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE (41 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* const struct retro_hw_render_interface ** -- + * Returns an API specific rendering interface for accessing API specific data. + * Not all HW rendering APIs support or need this. + * The contents of the returned pointer is specific to the rendering API + * being used. See the various headers like libretro_vulkan.h, etc. + * + * GET_HW_RENDER_INTERFACE cannot be called before context_reset has been called. + * Similarly, after context_destroyed callback returns, + * the contents of the HW_RENDER_INTERFACE are invalidated. + */ +#define RETRO_ENVIRONMENT_SET_SUPPORT_ACHIEVEMENTS (42 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* const bool * -- + * If true, the libretro implementation supports achievements + * either via memory descriptors set with RETRO_ENVIRONMENT_SET_MEMORY_MAPS + * or via retro_get_memory_data/retro_get_memory_size. + * + * This must be called before the first call to retro_run. + */ +#define RETRO_ENVIRONMENT_SET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE (43 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* const struct retro_hw_render_context_negotiation_interface * -- + * Sets an interface which lets the libretro core negotiate with frontend how a context is created. + * The semantics of this interface depends on which API is used in SET_HW_RENDER earlier. + * This interface will be used when the frontend is trying to create a HW rendering context, + * so it will be used after SET_HW_RENDER, but before the context_reset callback. + */ +#define RETRO_ENVIRONMENT_SET_SERIALIZATION_QUIRKS 44 + /* uint64_t * -- + * Sets quirk flags associated with serialization. The frontend will zero any flags it doesn't + * recognize or support. Should be set in either retro_init or retro_load_game, but not both. + */ +#define RETRO_ENVIRONMENT_SET_HW_SHARED_CONTEXT (44 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* N/A (null) * -- + * The frontend will try to use a 'shared' hardware context (mostly applicable + * to OpenGL) when a hardware context is being set up. + * + * Returns true if the frontend supports shared hardware contexts and false + * if the frontend does not support shared hardware contexts. + * + * This will do nothing on its own until SET_HW_RENDER env callbacks are + * being used. + */ +#define RETRO_ENVIRONMENT_GET_VFS_INTERFACE (45 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_vfs_interface_info * -- + * Gets access to the VFS interface. + * VFS presence needs to be queried prior to load_game or any + * get_system/save/other_directory being called to let front end know + * core supports VFS before it starts handing out paths. + * It is recomended to do so in retro_set_environment + */ +#define RETRO_ENVIRONMENT_GET_LED_INTERFACE (46 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_led_interface * -- + * Gets an interface which is used by a libretro core to set + * state of LEDs. + */ +#define RETRO_ENVIRONMENT_GET_AUDIO_VIDEO_ENABLE (47 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* int * -- + * Tells the core if the frontend wants audio or video. + * If disabled, the frontend will discard the audio or video, + * so the core may decide to skip generating a frame or generating audio. + * This is mainly used for increasing performance. + * Bit 0 (value 1): Enable Video + * Bit 1 (value 2): Enable Audio + * Bit 2 (value 4): Use Fast Savestates. + * Bit 3 (value 8): Hard Disable Audio + * Other bits are reserved for future use and will default to zero. + * If video is disabled: + * * The frontend wants the core to not generate any video, + * including presenting frames via hardware acceleration. + * * The frontend's video frame callback will do nothing. + * * After running the frame, the video output of the next frame should be + * no different than if video was enabled, and saving and loading state + * should have no issues. + * If audio is disabled: + * * The frontend wants the core to not generate any audio. + * * The frontend's audio callbacks will do nothing. + * * After running the frame, the audio output of the next frame should be + * no different than if audio was enabled, and saving and loading state + * should have no issues. + * Fast Savestates: + * * Guaranteed to be created by the same binary that will load them. + * * Will not be written to or read from the disk. + * * Suggest that the core assumes loading state will succeed. + * * Suggest that the core updates its memory buffers in-place if possible. + * * Suggest that the core skips clearing memory. + * * Suggest that the core skips resetting the system. + * * Suggest that the core may skip validation steps. + * Hard Disable Audio: + * * Used for a secondary core when running ahead. + * * Indicates that the frontend will never need audio from the core. + * * Suggests that the core may stop synthesizing audio, but this should not + * compromise emulation accuracy. + * * Audio output for the next frame does not matter, and the frontend will + * never need an accurate audio state in the future. + * * State will never be saved when using Hard Disable Audio. + */ +#define RETRO_ENVIRONMENT_GET_MIDI_INTERFACE (48 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_midi_interface ** -- + * Returns a MIDI interface that can be used for raw data I/O. + */ + +#define RETRO_ENVIRONMENT_GET_FASTFORWARDING (49 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* bool * -- + * Boolean value that indicates whether or not the frontend is in + * fastforwarding mode. + */ + +#define RETRO_ENVIRONMENT_GET_TARGET_REFRESH_RATE (50 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* float * -- + * Float value that lets us know what target refresh rate + * is curently in use by the frontend. + * + * The core can use the returned value to set an ideal + * refresh rate/framerate. + */ + +#define RETRO_ENVIRONMENT_GET_INPUT_BITMASKS (51 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* bool * -- + * Boolean value that indicates whether or not the frontend supports + * input bitmasks being returned by retro_input_state_t. The advantage + * of this is that retro_input_state_t has to be only called once to + * grab all button states instead of multiple times. + * + * If it returns true, you can pass RETRO_DEVICE_ID_JOYPAD_MASK as 'id' + * to retro_input_state_t (make sure 'device' is set to RETRO_DEVICE_JOYPAD). + * It will return a bitmask of all the digital buttons. + */ + +#define RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION 52 + /* unsigned * -- + * Unsigned value is the API version number of the core options + * interface supported by the frontend. If callback return false, + * API version is assumed to be 0. + * + * In legacy code, core options are set by passing an array of + * retro_variable structs to RETRO_ENVIRONMENT_SET_VARIABLES. + * This may be still be done regardless of the core options + * interface version. + * + * If version is >= 1 however, core options may instead be set by + * passing an array of retro_core_option_definition structs to + * RETRO_ENVIRONMENT_SET_CORE_OPTIONS, or a 2D array of + * retro_core_option_definition structs to RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL. + * This allows the core to additionally set option sublabel information + * and/or provide localisation support. + * + * If version is >= 2, core options may instead be set by passing + * a retro_core_options_v2 struct to RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2, + * or an array of retro_core_options_v2 structs to + * RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL. This allows the core + * to additionally set optional core option category information + * for frontends with core option category support. + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS 53 + /* const struct retro_core_option_definition ** -- + * Allows an implementation to signal the environment + * which variables it might want to check for later using + * GET_VARIABLE. + * This allows the frontend to present these variables to + * a user dynamically. + * This should only be called if RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION + * returns an API version of >= 1. + * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES. + * This should be called the first time as early as + * possible (ideally in retro_set_environment). + * Afterwards it may be called again for the core to communicate + * updated options to the frontend, but the number of core + * options must not change from the number in the initial call. + * + * 'data' points to an array of retro_core_option_definition structs + * terminated by a { NULL, NULL, NULL, {{0}}, NULL } element. + * retro_core_option_definition::key should be namespaced to not collide + * with other implementations' keys. e.g. A core called + * 'foo' should use keys named as 'foo_option'. + * retro_core_option_definition::desc should contain a human readable + * description of the key. + * retro_core_option_definition::info should contain any additional human + * readable information text that a typical user may need to + * understand the functionality of the option. + * retro_core_option_definition::values is an array of retro_core_option_value + * structs terminated by a { NULL, NULL } element. + * > retro_core_option_definition::values[index].value is an expected option + * value. + * > retro_core_option_definition::values[index].label is a human readable + * label used when displaying the value on screen. If NULL, + * the value itself is used. + * retro_core_option_definition::default_value is the default core option + * setting. It must match one of the expected option values in the + * retro_core_option_definition::values array. If it does not, or the + * default value is NULL, the first entry in the + * retro_core_option_definition::values array is treated as the default. + * + * The number of possible option values should be very limited, + * and must be less than RETRO_NUM_CORE_OPTION_VALUES_MAX. + * i.e. it should be feasible to cycle through options + * without a keyboard. + * + * Example entry: + * { + * "foo_option", + * "Speed hack coprocessor X", + * "Provides increased performance at the expense of reduced accuracy", + * { + * { "false", NULL }, + * { "true", NULL }, + * { "unstable", "Turbo (Unstable)" }, + * { NULL, NULL }, + * }, + * "false" + * } + * + * Only strings are operated on. The possible values will + * generally be displayed and stored as-is by the frontend. + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL 54 + /* const struct retro_core_options_intl * -- + * Allows an implementation to signal the environment + * which variables it might want to check for later using + * GET_VARIABLE. + * This allows the frontend to present these variables to + * a user dynamically. + * This should only be called if RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION + * returns an API version of >= 1. + * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES. + * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS. + * This should be called the first time as early as + * possible (ideally in retro_set_environment). + * Afterwards it may be called again for the core to communicate + * updated options to the frontend, but the number of core + * options must not change from the number in the initial call. + * + * This is fundamentally the same as RETRO_ENVIRONMENT_SET_CORE_OPTIONS, + * with the addition of localisation support. The description of the + * RETRO_ENVIRONMENT_SET_CORE_OPTIONS callback should be consulted + * for further details. + * + * 'data' points to a retro_core_options_intl struct. + * + * retro_core_options_intl::us is a pointer to an array of + * retro_core_option_definition structs defining the US English + * core options implementation. It must point to a valid array. + * + * retro_core_options_intl::local is a pointer to an array of + * retro_core_option_definition structs defining core options for + * the current frontend language. It may be NULL (in which case + * retro_core_options_intl::us is used by the frontend). Any items + * missing from this array will be read from retro_core_options_intl::us + * instead. + * + * NOTE: Default core option values are always taken from the + * retro_core_options_intl::us array. Any default values in + * retro_core_options_intl::local array will be ignored. + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY 55 + /* struct retro_core_option_display * -- + * + * Allows an implementation to signal the environment to show + * or hide a variable when displaying core options. This is + * considered a *suggestion*. The frontend is free to ignore + * this callback, and its implementation not considered mandatory. + * + * 'data' points to a retro_core_option_display struct + * + * retro_core_option_display::key is a variable identifier + * which has already been set by SET_VARIABLES/SET_CORE_OPTIONS. + * + * retro_core_option_display::visible is a boolean, specifying + * whether variable should be displayed + * + * Note that all core option variables will be set visible by + * default when calling SET_VARIABLES/SET_CORE_OPTIONS. + */ + +#define RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER 56 + /* unsigned * -- + * + * Allows an implementation to ask frontend preferred hardware + * context to use. Core should use this information to deal + * with what specific context to request with SET_HW_RENDER. + * + * 'data' points to an unsigned variable + */ + +#define RETRO_ENVIRONMENT_GET_DISK_CONTROL_INTERFACE_VERSION 57 + /* unsigned * -- + * Unsigned value is the API version number of the disk control + * interface supported by the frontend. If callback return false, + * API version is assumed to be 0. + * + * In legacy code, the disk control interface is defined by passing + * a struct of type retro_disk_control_callback to + * RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE. + * This may be still be done regardless of the disk control + * interface version. + * + * If version is >= 1 however, the disk control interface may + * instead be defined by passing a struct of type + * retro_disk_control_ext_callback to + * RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE. + * This allows the core to provide additional information about + * disk images to the frontend and/or enables extra + * disk control functionality by the frontend. + */ + +#define RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE 58 + /* const struct retro_disk_control_ext_callback * -- + * Sets an interface which frontend can use to eject and insert + * disk images, and also obtain information about individual + * disk image files registered by the core. + * This is used for games which consist of multiple images and + * must be manually swapped out by the user (e.g. PSX, floppy disk + * based systems). + */ + +#define RETRO_ENVIRONMENT_GET_MESSAGE_INTERFACE_VERSION 59 + /* unsigned * -- + * Unsigned value is the API version number of the message + * interface supported by the frontend. If callback returns + * false, API version is assumed to be 0. + * + * In legacy code, messages may be displayed in an + * implementation-specific manner by passing a struct + * of type retro_message to RETRO_ENVIRONMENT_SET_MESSAGE. + * This may be still be done regardless of the message + * interface version. + * + * If version is >= 1 however, messages may instead be + * displayed by passing a struct of type retro_message_ext + * to RETRO_ENVIRONMENT_SET_MESSAGE_EXT. This allows the + * core to specify message logging level, priority and + * destination (OSD, logging interface or both). + */ + +#define RETRO_ENVIRONMENT_SET_MESSAGE_EXT 60 + /* const struct retro_message_ext * -- + * Sets a message to be displayed in an implementation-specific + * manner for a certain amount of 'frames'. Additionally allows + * the core to specify message logging level, priority and + * destination (OSD, logging interface or both). + * Should not be used for trivial messages, which should simply be + * logged via RETRO_ENVIRONMENT_GET_LOG_INTERFACE (or as a + * fallback, stderr). + */ + +#define RETRO_ENVIRONMENT_GET_INPUT_MAX_USERS 61 + /* unsigned * -- + * Unsigned value is the number of active input devices + * provided by the frontend. This may change between + * frames, but will remain constant for the duration + * of each frame. + * If callback returns true, a core need not poll any + * input device with an index greater than or equal to + * the number of active devices. + * If callback returns false, the number of active input + * devices is unknown. In this case, all input devices + * should be considered active. + */ + +#define RETRO_ENVIRONMENT_SET_AUDIO_BUFFER_STATUS_CALLBACK 62 + /* const struct retro_audio_buffer_status_callback * -- + * Lets the core know the occupancy level of the frontend + * audio buffer. Can be used by a core to attempt frame + * skipping in order to avoid buffer under-runs. + * A core may pass NULL to disable buffer status reporting + * in the frontend. + */ + +#define RETRO_ENVIRONMENT_SET_MINIMUM_AUDIO_LATENCY 63 + /* const unsigned * -- + * Sets minimum frontend audio latency in milliseconds. + * Resultant audio latency may be larger than set value, + * or smaller if a hardware limit is encountered. A frontend + * is expected to honour requests up to 512 ms. + * + * - If value is less than current frontend + * audio latency, callback has no effect + * - If value is zero, default frontend audio + * latency is set + * + * May be used by a core to increase audio latency and + * therefore decrease the probability of buffer under-runs + * (crackling) when performing 'intensive' operations. + * A core utilising RETRO_ENVIRONMENT_SET_AUDIO_BUFFER_STATUS_CALLBACK + * to implement audio-buffer-based frame skipping may achieve + * optimal results by setting the audio latency to a 'high' + * (typically 6x or 8x) integer multiple of the expected + * frame time. + * + * WARNING: This can only be called from within retro_run(). + * Calling this can require a full reinitialization of audio + * drivers in the frontend, so it is important to call it very + * sparingly, and usually only with the users explicit consent. + * An eventual driver reinitialize will happen so that audio + * callbacks happening after this call within the same retro_run() + * call will target the newly initialized driver. + */ + +#define RETRO_ENVIRONMENT_SET_FASTFORWARDING_OVERRIDE 64 + /* const struct retro_fastforwarding_override * -- + * Used by a libretro core to override the current + * fastforwarding mode of the frontend. + * If NULL is passed to this function, the frontend + * will return true if fastforwarding override + * functionality is supported (no change in + * fastforwarding state will occur in this case). + */ + +#define RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE 65 + /* const struct retro_system_content_info_override * -- + * Allows an implementation to override 'global' content + * info parameters reported by retro_get_system_info(). + * Overrides also affect subsystem content info parameters + * set via RETRO_ENVIRONMENT_SET_SUBSYSTEM_INFO. + * This function must be called inside retro_set_environment(). + * If callback returns false, content info overrides + * are unsupported by the frontend, and will be ignored. + * If callback returns true, extended game info may be + * retrieved by calling RETRO_ENVIRONMENT_GET_GAME_INFO_EXT + * in retro_load_game() or retro_load_game_special(). + * + * 'data' points to an array of retro_system_content_info_override + * structs terminated by a { NULL, false, false } element. + * If 'data' is NULL, no changes will be made to the frontend; + * a core may therefore pass NULL in order to test whether + * the RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE and + * RETRO_ENVIRONMENT_GET_GAME_INFO_EXT callbacks are supported + * by the frontend. + * + * For struct member descriptions, see the definition of + * struct retro_system_content_info_override. + * + * Example: + * + * - struct retro_system_info: + * { + * "My Core", // library_name + * "v1.0", // library_version + * "m3u|md|cue|iso|chd|sms|gg|sg", // valid_extensions + * true, // need_fullpath + * false // block_extract + * } + * + * - Array of struct retro_system_content_info_override: + * { + * { + * "md|sms|gg", // extensions + * false, // need_fullpath + * true // persistent_data + * }, + * { + * "sg", // extensions + * false, // need_fullpath + * false // persistent_data + * }, + * { NULL, false, false } + * } + * + * Result: + * - Files of type m3u, cue, iso, chd will not be + * loaded by the frontend. Frontend will pass a + * valid path to the core, and core will handle + * loading internally + * - Files of type md, sms, gg will be loaded by + * the frontend. A valid memory buffer will be + * passed to the core. This memory buffer will + * remain valid until retro_deinit() returns + * - Files of type sg will be loaded by the frontend. + * A valid memory buffer will be passed to the core. + * This memory buffer will remain valid until + * retro_load_game() (or retro_load_game_special()) + * returns + * + * NOTE: If an extension is listed multiple times in + * an array of retro_system_content_info_override + * structs, only the first instance will be registered + */ + +#define RETRO_ENVIRONMENT_GET_GAME_INFO_EXT 66 + /* const struct retro_game_info_ext ** -- + * Allows an implementation to fetch extended game + * information, providing additional content path + * and memory buffer status details. + * This function may only be called inside + * retro_load_game() or retro_load_game_special(). + * If callback returns false, extended game information + * is unsupported by the frontend. In this case, only + * regular retro_game_info will be available. + * RETRO_ENVIRONMENT_GET_GAME_INFO_EXT is guaranteed + * to return true if RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE + * returns true. + * + * 'data' points to an array of retro_game_info_ext structs. + * + * For struct member descriptions, see the definition of + * struct retro_game_info_ext. + * + * - If function is called inside retro_load_game(), + * the retro_game_info_ext array is guaranteed to + * have a size of 1 - i.e. the returned pointer may + * be used to access directly the members of the + * first retro_game_info_ext struct, for example: + * + * struct retro_game_info_ext *game_info_ext; + * if (environ_cb(RETRO_ENVIRONMENT_GET_GAME_INFO_EXT, &game_info_ext)) + * printf("Content Directory: %s\n", game_info_ext->dir); + * + * - If the function is called inside retro_load_game_special(), + * the retro_game_info_ext array is guaranteed to have a + * size equal to the num_info argument passed to + * retro_load_game_special() + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 67 + /* const struct retro_core_options_v2 * -- + * Allows an implementation to signal the environment + * which variables it might want to check for later using + * GET_VARIABLE. + * This allows the frontend to present these variables to + * a user dynamically. + * This should only be called if RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION + * returns an API version of >= 2. + * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES. + * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS. + * This should be called the first time as early as + * possible (ideally in retro_set_environment). + * Afterwards it may be called again for the core to communicate + * updated options to the frontend, but the number of core + * options must not change from the number in the initial call. + * If RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION returns an API + * version of >= 2, this callback is guaranteed to succeed + * (i.e. callback return value does not indicate success) + * If callback returns true, frontend has core option category + * support. + * If callback returns false, frontend does not have core option + * category support. + * + * 'data' points to a retro_core_options_v2 struct, containing + * of two pointers: + * - retro_core_options_v2::categories is an array of + * retro_core_option_v2_category structs terminated by a + * { NULL, NULL, NULL } element. If retro_core_options_v2::categories + * is NULL, all core options will have no category and will be shown + * at the top level of the frontend core option interface. If frontend + * does not have core option category support, categories array will + * be ignored. + * - retro_core_options_v2::definitions is an array of + * retro_core_option_v2_definition structs terminated by a + * { NULL, NULL, NULL, NULL, NULL, NULL, {{0}}, NULL } + * element. + * + * >> retro_core_option_v2_category notes: + * + * - retro_core_option_v2_category::key should contain string + * that uniquely identifies the core option category. Valid + * key characters are [a-z, A-Z, 0-9, _, -] + * Namespace collisions with other implementations' category + * keys are permitted. + * - retro_core_option_v2_category::desc should contain a human + * readable description of the category key. + * - retro_core_option_v2_category::info should contain any + * additional human readable information text that a typical + * user may need to understand the nature of the core option + * category. + * + * Example entry: + * { + * "advanced_settings", + * "Advanced", + * "Options affecting low-level emulation performance and accuracy." + * } + * + * >> retro_core_option_v2_definition notes: + * + * - retro_core_option_v2_definition::key should be namespaced to not + * collide with other implementations' keys. e.g. A core called + * 'foo' should use keys named as 'foo_option'. Valid key characters + * are [a-z, A-Z, 0-9, _, -]. + * - retro_core_option_v2_definition::desc should contain a human readable + * description of the key. Will be used when the frontend does not + * have core option category support. Examples: "Aspect Ratio" or + * "Video > Aspect Ratio". + * - retro_core_option_v2_definition::desc_categorized should contain a + * human readable description of the key, which will be used when + * frontend has core option category support. Example: "Aspect Ratio", + * where associated retro_core_option_v2_category::desc is "Video". + * If empty or NULL, the string specified by + * retro_core_option_v2_definition::desc will be used instead. + * retro_core_option_v2_definition::desc_categorized will be ignored + * if retro_core_option_v2_definition::category_key is empty or NULL. + * - retro_core_option_v2_definition::info should contain any additional + * human readable information text that a typical user may need to + * understand the functionality of the option. + * - retro_core_option_v2_definition::info_categorized should contain + * any additional human readable information text that a typical user + * may need to understand the functionality of the option, and will be + * used when frontend has core option category support. This is provided + * to accommodate the case where info text references an option by + * name/desc, and the desc/desc_categorized text for that option differ. + * If empty or NULL, the string specified by + * retro_core_option_v2_definition::info will be used instead. + * retro_core_option_v2_definition::info_categorized will be ignored + * if retro_core_option_v2_definition::category_key is empty or NULL. + * - retro_core_option_v2_definition::category_key should contain a + * category identifier (e.g. "video" or "audio") that will be + * assigned to the core option if frontend has core option category + * support. A categorized option will be shown in a subsection/ + * submenu of the frontend core option interface. If key is empty + * or NULL, or if key does not match one of the + * retro_core_option_v2_category::key values in the associated + * retro_core_option_v2_category array, option will have no category + * and will be shown at the top level of the frontend core option + * interface. + * - retro_core_option_v2_definition::values is an array of + * retro_core_option_value structs terminated by a { NULL, NULL } + * element. + * --> retro_core_option_v2_definition::values[index].value is an + * expected option value. + * --> retro_core_option_v2_definition::values[index].label is a + * human readable label used when displaying the value on screen. + * If NULL, the value itself is used. + * - retro_core_option_v2_definition::default_value is the default + * core option setting. It must match one of the expected option + * values in the retro_core_option_v2_definition::values array. If + * it does not, or the default value is NULL, the first entry in the + * retro_core_option_v2_definition::values array is treated as the + * default. + * + * The number of possible option values should be very limited, + * and must be less than RETRO_NUM_CORE_OPTION_VALUES_MAX. + * i.e. it should be feasible to cycle through options + * without a keyboard. + * + * Example entries: + * + * - Uncategorized: + * + * { + * "foo_option", + * "Speed hack coprocessor X", + * NULL, + * "Provides increased performance at the expense of reduced accuracy.", + * NULL, + * NULL, + * { + * { "false", NULL }, + * { "true", NULL }, + * { "unstable", "Turbo (Unstable)" }, + * { NULL, NULL }, + * }, + * "false" + * } + * + * - Categorized: + * + * { + * "foo_option", + * "Advanced > Speed hack coprocessor X", + * "Speed hack coprocessor X", + * "Setting 'Advanced > Speed hack coprocessor X' to 'true' or 'Turbo' provides increased performance at the expense of reduced accuracy", + * "Setting 'Speed hack coprocessor X' to 'true' or 'Turbo' provides increased performance at the expense of reduced accuracy", + * "advanced_settings", + * { + * { "false", NULL }, + * { "true", NULL }, + * { "unstable", "Turbo (Unstable)" }, + * { NULL, NULL }, + * }, + * "false" + * } + * + * Only strings are operated on. The possible values will + * generally be displayed and stored as-is by the frontend. + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL 68 + /* const struct retro_core_options_v2_intl * -- + * Allows an implementation to signal the environment + * which variables it might want to check for later using + * GET_VARIABLE. + * This allows the frontend to present these variables to + * a user dynamically. + * This should only be called if RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION + * returns an API version of >= 2. + * This should be called instead of RETRO_ENVIRONMENT_SET_VARIABLES. + * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS. + * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL. + * This should be called instead of RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2. + * This should be called the first time as early as + * possible (ideally in retro_set_environment). + * Afterwards it may be called again for the core to communicate + * updated options to the frontend, but the number of core + * options must not change from the number in the initial call. + * If RETRO_ENVIRONMENT_GET_CORE_OPTIONS_VERSION returns an API + * version of >= 2, this callback is guaranteed to succeed + * (i.e. callback return value does not indicate success) + * If callback returns true, frontend has core option category + * support. + * If callback returns false, frontend does not have core option + * category support. + * + * This is fundamentally the same as RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2, + * with the addition of localisation support. The description of the + * RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 callback should be consulted + * for further details. + * + * 'data' points to a retro_core_options_v2_intl struct. + * + * - retro_core_options_v2_intl::us is a pointer to a + * retro_core_options_v2 struct defining the US English + * core options implementation. It must point to a valid struct. + * + * - retro_core_options_v2_intl::local is a pointer to a + * retro_core_options_v2 struct defining core options for + * the current frontend language. It may be NULL (in which case + * retro_core_options_v2_intl::us is used by the frontend). Any items + * missing from this struct will be read from + * retro_core_options_v2_intl::us instead. + * + * NOTE: Default core option values are always taken from the + * retro_core_options_v2_intl::us struct. Any default values in + * the retro_core_options_v2_intl::local struct will be ignored. + */ + +#define RETRO_ENVIRONMENT_SET_CORE_OPTIONS_UPDATE_DISPLAY_CALLBACK 69 + /* const struct retro_core_options_update_display_callback * -- + * Allows a frontend to signal that a core must update + * the visibility of any dynamically hidden core options, + * and enables the frontend to detect visibility changes. + * Used by the frontend to update the menu display status + * of core options without requiring a call of retro_run(). + * Must be called in retro_set_environment(). + */ + +#define RETRO_ENVIRONMENT_SET_VARIABLE 70 + /* const struct retro_variable * -- + * Allows an implementation to notify the frontend + * that a core option value has changed. + * + * retro_variable::key and retro_variable::value + * must match strings that have been set previously + * via one of the following: + * + * - RETRO_ENVIRONMENT_SET_VARIABLES + * - RETRO_ENVIRONMENT_SET_CORE_OPTIONS + * - RETRO_ENVIRONMENT_SET_CORE_OPTIONS_INTL + * - RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2 + * - RETRO_ENVIRONMENT_SET_CORE_OPTIONS_V2_INTL + * + * After changing a core option value via this + * callback, RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE + * will return true. + * + * If data is NULL, no changes will be registered + * and the callback will return true; an + * implementation may therefore pass NULL in order + * to test whether the callback is supported. + */ + +#define RETRO_ENVIRONMENT_GET_THROTTLE_STATE (71 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_throttle_state * -- + * Allows an implementation to get details on the actual rate + * the frontend is attempting to call retro_run(). + */ + +#define RETRO_ENVIRONMENT_GET_SAVESTATE_CONTEXT (72 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* int * -- + * Tells the core about the context the frontend is asking for savestate. + * (see enum retro_savestate_context) + */ + +#define RETRO_ENVIRONMENT_GET_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_SUPPORT (73 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_hw_render_context_negotiation_interface * -- + * Before calling SET_HW_RNEDER_CONTEXT_NEGOTIATION_INTERFACE, a core can query + * which version of the interface is supported. + * + * Frontend looks at interface_type and returns the maximum supported + * context negotiation interface version. + * If the interface_type is not supported or recognized by the frontend, a version of 0 + * must be returned in interface_version and true is returned by frontend. + * + * If this environment call returns true with interface_version greater than 0, + * a core can always use a negotiation interface version larger than what the frontend returns, but only + * earlier versions of the interface will be used by the frontend. + * A frontend must not reject a negotiation interface version that is larger than + * what the frontend supports. Instead, the frontend will use the older entry points that it recognizes. + * If this is incompatible with a particular core's requirements, it can error out early. + * + * Backwards compatibility note: + * This environment call was introduced after Vulkan v1 context negotiation. + * If this environment call is not supported by frontend - i.e. the environment call returns false - + * only Vulkan v1 context negotiation is supported (if Vulkan HW rendering is supported at all). + * If a core uses Vulkan negotiation interface with version > 1, negotiation may fail unexpectedly. + * All future updates to the context negotiation interface implies that frontend must support + * this environment call to query support. + */ + +#define RETRO_ENVIRONMENT_GET_JIT_CAPABLE 74 + /* bool * -- + * Result is set to true if the frontend has already verified JIT can be + * used, mainly for use iOS/tvOS. On other platforms the result is true. + */ + +#define RETRO_ENVIRONMENT_GET_MICROPHONE_INTERFACE (75 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_microphone_interface * -- + * Returns an interface that can be used to receive input from the microphone driver. + * + * Returns true if microphone support is available, + * even if no microphones are plugged in. + * Returns false if mic support is disabled or unavailable. + * + * This callback can be invoked at any time, + * even before the microphone driver is ready. + */ + +#define RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE 76 + /* const struct retro_netpacket_callback * -- + * When set, a core gains control over network packets sent and + * received during a multiplayer session. This can be used to + * emulate multiplayer games that were originally played on two + * or more separate consoles or computers connected together. + * + * The frontend will take care of connecting players together, + * and the core only needs to send the actual data as needed for + * the emulation, while handshake and connection management happen + * in the background. + * + * When two or more players are connected and this interface has + * been set, time manipulation features (such as pausing, slow motion, + * fast forward, rewinding, save state loading, etc.) are disabled to + * avoid interrupting communication. + * + * Should be set in either retro_init or retro_load_game, but not both. + * + * When not set, a frontend may use state serialization-based + * multiplayer, where a deterministic core supporting multiple + * input devices does not need to take any action on its own. + */ + +#define RETRO_ENVIRONMENT_GET_DEVICE_POWER (77 | RETRO_ENVIRONMENT_EXPERIMENTAL) + /* struct retro_device_power * -- + * Returns the device's current power state as reported by the frontend. + * This is useful for emulating the battery level in handheld consoles, + * or for reducing power consumption when on battery power. + * + * The return value indicates whether the frontend can provide this information, + * even if the parameter is NULL. + * + * If the frontend does not support this functionality, + * then the provided argument will remain unchanged. + * + * Note that this environment call describes the power state for the entire device, + * not for individual peripherals like controllers. + */ + +/* VFS functionality */ + +/* File paths: + * File paths passed as parameters when using this API shall be well formed UNIX-style, + * using "/" (unquoted forward slash) as directory separator regardless of the platform's native separator. + * Paths shall also include at least one forward slash ("game.bin" is an invalid path, use "./game.bin" instead). + * Other than the directory separator, cores shall not make assumptions about path format: + * "C:/path/game.bin", "http://example.com/game.bin", "#game/game.bin", "./game.bin" (without quotes) are all valid paths. + * Cores may replace the basename or remove path components from the end, and/or add new components; + * however, cores shall not append "./", "../" or multiple consecutive forward slashes ("//") to paths they request to front end. + * The frontend is encouraged to make such paths work as well as it can, but is allowed to give up if the core alters paths too much. + * Frontends are encouraged, but not required, to support native file system paths (modulo replacing the directory separator, if applicable). + * Cores are allowed to try using them, but must remain functional if the front rejects such requests. + * Cores are encouraged to use the libretro-common filestream functions for file I/O, + * as they seamlessly integrate with VFS, deal with directory separator replacement as appropriate + * and provide platform-specific fallbacks in cases where front ends do not support VFS. */ + +/* Opaque file handle + * Introduced in VFS API v1 */ +struct retro_vfs_file_handle; + +/* Opaque directory handle + * Introduced in VFS API v3 */ +struct retro_vfs_dir_handle; + +/* File open flags + * Introduced in VFS API v1 */ +#define RETRO_VFS_FILE_ACCESS_READ (1 << 0) /* Read only mode */ +#define RETRO_VFS_FILE_ACCESS_WRITE (1 << 1) /* Write only mode, discard contents and overwrites existing file unless RETRO_VFS_FILE_ACCESS_UPDATE is also specified */ +#define RETRO_VFS_FILE_ACCESS_READ_WRITE (RETRO_VFS_FILE_ACCESS_READ | RETRO_VFS_FILE_ACCESS_WRITE) /* Read-write mode, discard contents and overwrites existing file unless RETRO_VFS_FILE_ACCESS_UPDATE is also specified*/ +#define RETRO_VFS_FILE_ACCESS_UPDATE_EXISTING (1 << 2) /* Prevents discarding content of existing files opened for writing */ + +/* These are only hints. The frontend may choose to ignore them. Other than RAM/CPU/etc use, + and how they react to unlikely external interference (for example someone else writing to that file, + or the file's server going down), behavior will not change. */ +#define RETRO_VFS_FILE_ACCESS_HINT_NONE (0) +/* Indicate that the file will be accessed many times. The frontend should aggressively cache everything. */ +#define RETRO_VFS_FILE_ACCESS_HINT_FREQUENT_ACCESS (1 << 0) + +/* Seek positions */ +#define RETRO_VFS_SEEK_POSITION_START 0 +#define RETRO_VFS_SEEK_POSITION_CURRENT 1 +#define RETRO_VFS_SEEK_POSITION_END 2 + +/* stat() result flags + * Introduced in VFS API v3 */ +#define RETRO_VFS_STAT_IS_VALID (1 << 0) +#define RETRO_VFS_STAT_IS_DIRECTORY (1 << 1) +#define RETRO_VFS_STAT_IS_CHARACTER_SPECIAL (1 << 2) + +/* Get path from opaque handle. Returns the exact same path passed to file_open when getting the handle + * Introduced in VFS API v1 */ +typedef const char *(RETRO_CALLCONV *retro_vfs_get_path_t)(struct retro_vfs_file_handle *stream); + +/* Open a file for reading or writing. If path points to a directory, this will + * fail. Returns the opaque file handle, or NULL for error. + * Introduced in VFS API v1 */ +typedef struct retro_vfs_file_handle *(RETRO_CALLCONV *retro_vfs_open_t)(const char *path, unsigned mode, unsigned hints); + +/* Close the file and release its resources. Must be called if open_file returns non-NULL. Returns 0 on success, -1 on failure. + * Whether the call succeeds ot not, the handle passed as parameter becomes invalid and should no longer be used. + * Introduced in VFS API v1 */ +typedef int (RETRO_CALLCONV *retro_vfs_close_t)(struct retro_vfs_file_handle *stream); + +/* Return the size of the file in bytes, or -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_size_t)(struct retro_vfs_file_handle *stream); + +/* Truncate file to specified size. Returns 0 on success or -1 on error + * Introduced in VFS API v2 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_truncate_t)(struct retro_vfs_file_handle *stream, int64_t length); + +/* Get the current read / write position for the file. Returns -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_tell_t)(struct retro_vfs_file_handle *stream); + +/* Set the current read/write position for the file. Returns the new position, -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_seek_t)(struct retro_vfs_file_handle *stream, int64_t offset, int seek_position); + +/* Read data from a file. Returns the number of bytes read, or -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_read_t)(struct retro_vfs_file_handle *stream, void *s, uint64_t len); + +/* Write data to a file. Returns the number of bytes written, or -1 for error. + * Introduced in VFS API v1 */ +typedef int64_t (RETRO_CALLCONV *retro_vfs_write_t)(struct retro_vfs_file_handle *stream, const void *s, uint64_t len); + +/* Flush pending writes to file, if using buffered IO. Returns 0 on sucess, or -1 on failure. + * Introduced in VFS API v1 */ +typedef int (RETRO_CALLCONV *retro_vfs_flush_t)(struct retro_vfs_file_handle *stream); + +/* Delete the specified file. Returns 0 on success, -1 on failure + * Introduced in VFS API v1 */ +typedef int (RETRO_CALLCONV *retro_vfs_remove_t)(const char *path); + +/* Rename the specified file. Returns 0 on success, -1 on failure + * Introduced in VFS API v1 */ +typedef int (RETRO_CALLCONV *retro_vfs_rename_t)(const char *old_path, const char *new_path); + +/* Stat the specified file. Retruns a bitmask of RETRO_VFS_STAT_* flags, none are set if path was not valid. + * Additionally stores file size in given variable, unless NULL is given. + * Introduced in VFS API v3 */ +typedef int (RETRO_CALLCONV *retro_vfs_stat_t)(const char *path, int32_t *size); + +/* Create the specified directory. Returns 0 on success, -1 on unknown failure, -2 if already exists. + * Introduced in VFS API v3 */ +typedef int (RETRO_CALLCONV *retro_vfs_mkdir_t)(const char *dir); + +/* Open the specified directory for listing. Returns the opaque dir handle, or NULL for error. + * Support for the include_hidden argument may vary depending on the platform. + * Introduced in VFS API v3 */ +typedef struct retro_vfs_dir_handle *(RETRO_CALLCONV *retro_vfs_opendir_t)(const char *dir, bool include_hidden); + +/* Read the directory entry at the current position, and move the read pointer to the next position. + * Returns true on success, false if already on the last entry. + * Introduced in VFS API v3 */ +typedef bool (RETRO_CALLCONV *retro_vfs_readdir_t)(struct retro_vfs_dir_handle *dirstream); + +/* Get the name of the last entry read. Returns a string on success, or NULL for error. + * The returned string pointer is valid until the next call to readdir or closedir. + * Introduced in VFS API v3 */ +typedef const char *(RETRO_CALLCONV *retro_vfs_dirent_get_name_t)(struct retro_vfs_dir_handle *dirstream); + +/* Check if the last entry read was a directory. Returns true if it was, false otherwise (or on error). + * Introduced in VFS API v3 */ +typedef bool (RETRO_CALLCONV *retro_vfs_dirent_is_dir_t)(struct retro_vfs_dir_handle *dirstream); + +/* Close the directory and release its resources. Must be called if opendir returns non-NULL. Returns 0 on success, -1 on failure. + * Whether the call succeeds ot not, the handle passed as parameter becomes invalid and should no longer be used. + * Introduced in VFS API v3 */ +typedef int (RETRO_CALLCONV *retro_vfs_closedir_t)(struct retro_vfs_dir_handle *dirstream); + +struct retro_vfs_interface +{ + /* VFS API v1 */ + retro_vfs_get_path_t get_path; + retro_vfs_open_t open; + retro_vfs_close_t close; + retro_vfs_size_t size; + retro_vfs_tell_t tell; + retro_vfs_seek_t seek; + retro_vfs_read_t read; + retro_vfs_write_t write; + retro_vfs_flush_t flush; + retro_vfs_remove_t remove; + retro_vfs_rename_t rename; + /* VFS API v2 */ + retro_vfs_truncate_t truncate; + /* VFS API v3 */ + retro_vfs_stat_t stat; + retro_vfs_mkdir_t mkdir; + retro_vfs_opendir_t opendir; + retro_vfs_readdir_t readdir; + retro_vfs_dirent_get_name_t dirent_get_name; + retro_vfs_dirent_is_dir_t dirent_is_dir; + retro_vfs_closedir_t closedir; +}; + +struct retro_vfs_interface_info +{ + /* Set by core: should this be higher than the version the front end supports, + * front end will return false in the RETRO_ENVIRONMENT_GET_VFS_INTERFACE call + * Introduced in VFS API v1 */ + uint32_t required_interface_version; + + /* Frontend writes interface pointer here. The frontend also sets the actual + * version, must be at least required_interface_version. + * Introduced in VFS API v1 */ + struct retro_vfs_interface *iface; +}; + +enum retro_hw_render_interface_type +{ + RETRO_HW_RENDER_INTERFACE_VULKAN = 0, + RETRO_HW_RENDER_INTERFACE_D3D9 = 1, + RETRO_HW_RENDER_INTERFACE_D3D10 = 2, + RETRO_HW_RENDER_INTERFACE_D3D11 = 3, + RETRO_HW_RENDER_INTERFACE_D3D12 = 4, + RETRO_HW_RENDER_INTERFACE_GSKIT_PS2 = 5, + RETRO_HW_RENDER_INTERFACE_DUMMY = INT_MAX +}; + +/* Base struct. All retro_hw_render_interface_* types + * contain at least these fields. */ +struct retro_hw_render_interface +{ + enum retro_hw_render_interface_type interface_type; + unsigned interface_version; +}; + +typedef void (RETRO_CALLCONV *retro_set_led_state_t)(int led, int state); +struct retro_led_interface +{ + retro_set_led_state_t set_led_state; +}; + +/* Retrieves the current state of the MIDI input. + * Returns true if it's enabled, false otherwise. */ +typedef bool (RETRO_CALLCONV *retro_midi_input_enabled_t)(void); + +/* Retrieves the current state of the MIDI output. + * Returns true if it's enabled, false otherwise */ +typedef bool (RETRO_CALLCONV *retro_midi_output_enabled_t)(void); + +/* Reads next byte from the input stream. + * Returns true if byte is read, false otherwise. */ +typedef bool (RETRO_CALLCONV *retro_midi_read_t)(uint8_t *byte); + +/* Writes byte to the output stream. + * 'delta_time' is in microseconds and represent time elapsed since previous write. + * Returns true if byte is written, false otherwise. */ +typedef bool (RETRO_CALLCONV *retro_midi_write_t)(uint8_t byte, uint32_t delta_time); + +/* Flushes previously written data. + * Returns true if successful, false otherwise. */ +typedef bool (RETRO_CALLCONV *retro_midi_flush_t)(void); + +struct retro_midi_interface +{ + retro_midi_input_enabled_t input_enabled; + retro_midi_output_enabled_t output_enabled; + retro_midi_read_t read; + retro_midi_write_t write; + retro_midi_flush_t flush; +}; + +enum retro_hw_render_context_negotiation_interface_type +{ + RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_VULKAN = 0, + RETRO_HW_RENDER_CONTEXT_NEGOTIATION_INTERFACE_DUMMY = INT_MAX +}; + +/* Base struct. All retro_hw_render_context_negotiation_interface_* types + * contain at least these fields. */ +struct retro_hw_render_context_negotiation_interface +{ + enum retro_hw_render_context_negotiation_interface_type interface_type; + unsigned interface_version; +}; + +/* Serialized state is incomplete in some way. Set if serialization is + * usable in typical end-user cases but should not be relied upon to + * implement frame-sensitive frontend features such as netplay or + * rerecording. */ +#define RETRO_SERIALIZATION_QUIRK_INCOMPLETE (1 << 0) +/* The core must spend some time initializing before serialization is + * supported. retro_serialize() will initially fail; retro_unserialize() + * and retro_serialize_size() may or may not work correctly either. */ +#define RETRO_SERIALIZATION_QUIRK_MUST_INITIALIZE (1 << 1) +/* Serialization size may change within a session. */ +#define RETRO_SERIALIZATION_QUIRK_CORE_VARIABLE_SIZE (1 << 2) +/* Set by the frontend to acknowledge that it supports variable-sized + * states. */ +#define RETRO_SERIALIZATION_QUIRK_FRONT_VARIABLE_SIZE (1 << 3) +/* Serialized state can only be loaded during the same session. */ +#define RETRO_SERIALIZATION_QUIRK_SINGLE_SESSION (1 << 4) +/* Serialized state cannot be loaded on an architecture with a different + * endianness from the one it was saved on. */ +#define RETRO_SERIALIZATION_QUIRK_ENDIAN_DEPENDENT (1 << 5) +/* Serialized state cannot be loaded on a different platform from the one it + * was saved on for reasons other than endianness, such as word size + * dependence */ +#define RETRO_SERIALIZATION_QUIRK_PLATFORM_DEPENDENT (1 << 6) + +#define RETRO_MEMDESC_CONST (1 << 0) /* The frontend will never change this memory area once retro_load_game has returned. */ +#define RETRO_MEMDESC_BIGENDIAN (1 << 1) /* The memory area contains big endian data. Default is little endian. */ +#define RETRO_MEMDESC_SYSTEM_RAM (1 << 2) /* The memory area is system RAM. This is main RAM of the gaming system. */ +#define RETRO_MEMDESC_SAVE_RAM (1 << 3) /* The memory area is save RAM. This RAM is usually found on a game cartridge, backed up by a battery. */ +#define RETRO_MEMDESC_VIDEO_RAM (1 << 4) /* The memory area is video RAM (VRAM) */ +#define RETRO_MEMDESC_ALIGN_2 (1 << 16) /* All memory access in this area is aligned to their own size, or 2, whichever is smaller. */ +#define RETRO_MEMDESC_ALIGN_4 (2 << 16) +#define RETRO_MEMDESC_ALIGN_8 (3 << 16) +#define RETRO_MEMDESC_MINSIZE_2 (1 << 24) /* All memory in this region is accessed at least 2 bytes at the time. */ +#define RETRO_MEMDESC_MINSIZE_4 (2 << 24) +#define RETRO_MEMDESC_MINSIZE_8 (3 << 24) +struct retro_memory_descriptor +{ + uint64_t flags; + + /* Pointer to the start of the relevant ROM or RAM chip. + * It's strongly recommended to use 'offset' if possible, rather than + * doing math on the pointer. + * + * If the same byte is mapped my multiple descriptors, their descriptors + * must have the same pointer. + * If 'start' does not point to the first byte in the pointer, put the + * difference in 'offset' instead. + * + * May be NULL if there's nothing usable here (e.g. hardware registers and + * open bus). No flags should be set if the pointer is NULL. + * It's recommended to minimize the number of descriptors if possible, + * but not mandatory. */ + void *ptr; + size_t offset; + + /* This is the location in the emulated address space + * where the mapping starts. */ + size_t start; + + /* Which bits must be same as in 'start' for this mapping to apply. + * The first memory descriptor to claim a certain byte is the one + * that applies. + * A bit which is set in 'start' must also be set in this. + * Can be zero, in which case each byte is assumed mapped exactly once. + * In this case, 'len' must be a power of two. */ + size_t select; + + /* If this is nonzero, the set bits are assumed not connected to the + * memory chip's address pins. */ + size_t disconnect; + + /* This one tells the size of the current memory area. + * If, after start+disconnect are applied, the address is higher than + * this, the highest bit of the address is cleared. + * + * If the address is still too high, the next highest bit is cleared. + * Can be zero, in which case it's assumed to be infinite (as limited + * by 'select' and 'disconnect'). */ + size_t len; + + /* To go from emulated address to physical address, the following + * order applies: + * Subtract 'start', pick off 'disconnect', apply 'len', add 'offset'. */ + + /* The address space name must consist of only a-zA-Z0-9_-, + * should be as short as feasible (maximum length is 8 plus the NUL), + * and may not be any other address space plus one or more 0-9A-F + * at the end. + * However, multiple memory descriptors for the same address space is + * allowed, and the address space name can be empty. NULL is treated + * as empty. + * + * Address space names are case sensitive, but avoid lowercase if possible. + * The same pointer may exist in multiple address spaces. + * + * Examples: + * blank+blank - valid (multiple things may be mapped in the same namespace) + * 'Sp'+'Sp' - valid (multiple things may be mapped in the same namespace) + * 'A'+'B' - valid (neither is a prefix of each other) + * 'S'+blank - valid ('S' is not in 0-9A-F) + * 'a'+blank - valid ('a' is not in 0-9A-F) + * 'a'+'A' - valid (neither is a prefix of each other) + * 'AR'+blank - valid ('R' is not in 0-9A-F) + * 'ARB'+blank - valid (the B can't be part of the address either, because + * there is no namespace 'AR') + * blank+'B' - not valid, because it's ambigous which address space B1234 + * would refer to. + * The length can't be used for that purpose; the frontend may want + * to append arbitrary data to an address, without a separator. */ + const char *addrspace; + + /* TODO: When finalizing this one, add a description field, which should be + * "WRAM" or something roughly equally long. */ + + /* TODO: When finalizing this one, replace 'select' with 'limit', which tells + * which bits can vary and still refer to the same address (limit = ~select). + * TODO: limit? range? vary? something else? */ + + /* TODO: When finalizing this one, if 'len' is above what 'select' (or + * 'limit') allows, it's bankswitched. Bankswitched data must have both 'len' + * and 'select' != 0, and the mappings don't tell how the system switches the + * banks. */ + + /* TODO: When finalizing this one, fix the 'len' bit removal order. + * For len=0x1800, pointer 0x1C00 should go to 0x1400, not 0x0C00. + * Algorithm: Take bits highest to lowest, but if it goes above len, clear + * the most recent addition and continue on the next bit. + * TODO: Can the above be optimized? Is "remove the lowest bit set in both + * pointer and 'len'" equivalent? */ + + /* TODO: Some emulators (MAME?) emulate big endian systems by only accessing + * the emulated memory in 32-bit chunks, native endian. But that's nothing + * compared to Darek Mihocka + * (section Emulation 103 - Nearly Free Byte Reversal) - he flips the ENTIRE + * RAM backwards! I'll want to represent both of those, via some flags. + * + * I suspect MAME either didn't think of that idea, or don't want the #ifdef. + * Not sure which, nor do I really care. */ + + /* TODO: Some of those flags are unused and/or don't really make sense. Clean + * them up. */ +}; + +/* The frontend may use the largest value of 'start'+'select' in a + * certain namespace to infer the size of the address space. + * + * If the address space is larger than that, a mapping with .ptr=NULL + * should be at the end of the array, with .select set to all ones for + * as long as the address space is big. + * + * Sample descriptors (minus .ptr, and RETRO_MEMFLAG_ on the flags): + * SNES WRAM: + * .start=0x7E0000, .len=0x20000 + * (Note that this must be mapped before the ROM in most cases; some of the + * ROM mappers + * try to claim $7E0000, or at least $7E8000.) + * SNES SPC700 RAM: + * .addrspace="S", .len=0x10000 + * SNES WRAM mirrors: + * .flags=MIRROR, .start=0x000000, .select=0xC0E000, .len=0x2000 + * .flags=MIRROR, .start=0x800000, .select=0xC0E000, .len=0x2000 + * SNES WRAM mirrors, alternate equivalent descriptor: + * .flags=MIRROR, .select=0x40E000, .disconnect=~0x1FFF + * (Various similar constructions can be created by combining parts of + * the above two.) + * SNES LoROM (512KB, mirrored a couple of times): + * .flags=CONST, .start=0x008000, .select=0x408000, .disconnect=0x8000, .len=512*1024 + * .flags=CONST, .start=0x400000, .select=0x400000, .disconnect=0x8000, .len=512*1024 + * SNES HiROM (4MB): + * .flags=CONST, .start=0x400000, .select=0x400000, .len=4*1024*1024 + * .flags=CONST, .offset=0x8000, .start=0x008000, .select=0x408000, .len=4*1024*1024 + * SNES ExHiROM (8MB): + * .flags=CONST, .offset=0, .start=0xC00000, .select=0xC00000, .len=4*1024*1024 + * .flags=CONST, .offset=4*1024*1024, .start=0x400000, .select=0xC00000, .len=4*1024*1024 + * .flags=CONST, .offset=0x8000, .start=0x808000, .select=0xC08000, .len=4*1024*1024 + * .flags=CONST, .offset=4*1024*1024+0x8000, .start=0x008000, .select=0xC08000, .len=4*1024*1024 + * Clarify the size of the address space: + * .ptr=NULL, .select=0xFFFFFF + * .len can be implied by .select in many of them, but was included for clarity. + */ + +struct retro_memory_map +{ + const struct retro_memory_descriptor *descriptors; + unsigned num_descriptors; +}; + +struct retro_controller_description +{ + /* Human-readable description of the controller. Even if using a generic + * input device type, this can be set to the particular device type the + * core uses. */ + const char *desc; + + /* Device type passed to retro_set_controller_port_device(). If the device + * type is a sub-class of a generic input device type, use the + * RETRO_DEVICE_SUBCLASS macro to create an ID. + * + * E.g. RETRO_DEVICE_SUBCLASS(RETRO_DEVICE_JOYPAD, 1). */ + unsigned id; +}; + +struct retro_controller_info +{ + const struct retro_controller_description *types; + unsigned num_types; +}; + +struct retro_subsystem_memory_info +{ + /* The extension associated with a memory type, e.g. "psram". */ + const char *extension; + + /* The memory type for retro_get_memory(). This should be at + * least 0x100 to avoid conflict with standardized + * libretro memory types. */ + unsigned type; +}; + +struct retro_subsystem_rom_info +{ + /* Describes what the content is (SGB BIOS, GB ROM, etc). */ + const char *desc; + + /* Same definition as retro_get_system_info(). */ + const char *valid_extensions; + + /* Same definition as retro_get_system_info(). */ + bool need_fullpath; + + /* Same definition as retro_get_system_info(). */ + bool block_extract; + + /* This is set if the content is required to load a game. + * If this is set to false, a zeroed-out retro_game_info can be passed. */ + bool required; + + /* Content can have multiple associated persistent + * memory types (retro_get_memory()). */ + const struct retro_subsystem_memory_info *memory; + unsigned num_memory; +}; + +struct retro_subsystem_info +{ + /* Human-readable string of the subsystem type, e.g. "Super GameBoy" */ + const char *desc; + + /* A computer friendly short string identifier for the subsystem type. + * This name must be [a-z]. + * E.g. if desc is "Super GameBoy", this can be "sgb". + * This identifier can be used for command-line interfaces, etc. + */ + const char *ident; + + /* Infos for each content file. The first entry is assumed to be the + * "most significant" content for frontend purposes. + * E.g. with Super GameBoy, the first content should be the GameBoy ROM, + * as it is the most "significant" content to a user. + * If a frontend creates new file paths based on the content used + * (e.g. savestates), it should use the path for the first ROM to do so. */ + const struct retro_subsystem_rom_info *roms; + + /* Number of content files associated with a subsystem. */ + unsigned num_roms; + + /* The type passed to retro_load_game_special(). */ + unsigned id; +}; + +typedef void (RETRO_CALLCONV *retro_proc_address_t)(void); + +/* libretro API extension functions: + * (None here so far). + * + * Get a symbol from a libretro core. + * Cores should only return symbols which are actual + * extensions to the libretro API. + * + * Frontends should not use this to obtain symbols to standard + * libretro entry points (static linking or dlsym). + * + * The symbol name must be equal to the function name, + * e.g. if void retro_foo(void); exists, the symbol must be called "retro_foo". + * The returned function pointer must be cast to the corresponding type. + */ +typedef retro_proc_address_t (RETRO_CALLCONV *retro_get_proc_address_t)(const char *sym); + +struct retro_get_proc_address_interface +{ + retro_get_proc_address_t get_proc_address; +}; + +enum retro_log_level +{ + RETRO_LOG_DEBUG = 0, + RETRO_LOG_INFO, + RETRO_LOG_WARN, + RETRO_LOG_ERROR, + + RETRO_LOG_DUMMY = INT_MAX +}; + +/* Logging function. Takes log level argument as well. */ +typedef void (RETRO_CALLCONV *retro_log_printf_t)(enum retro_log_level level, + const char *fmt, ...); + +struct retro_log_callback +{ + retro_log_printf_t log; +}; + +/* Performance related functions */ + +/* ID values for SIMD CPU features */ +#define RETRO_SIMD_SSE (1 << 0) +#define RETRO_SIMD_SSE2 (1 << 1) +#define RETRO_SIMD_VMX (1 << 2) +#define RETRO_SIMD_VMX128 (1 << 3) +#define RETRO_SIMD_AVX (1 << 4) +#define RETRO_SIMD_NEON (1 << 5) +#define RETRO_SIMD_SSE3 (1 << 6) +#define RETRO_SIMD_SSSE3 (1 << 7) +#define RETRO_SIMD_MMX (1 << 8) +#define RETRO_SIMD_MMXEXT (1 << 9) +#define RETRO_SIMD_SSE4 (1 << 10) +#define RETRO_SIMD_SSE42 (1 << 11) +#define RETRO_SIMD_AVX2 (1 << 12) +#define RETRO_SIMD_VFPU (1 << 13) +#define RETRO_SIMD_PS (1 << 14) +#define RETRO_SIMD_AES (1 << 15) +#define RETRO_SIMD_VFPV3 (1 << 16) +#define RETRO_SIMD_VFPV4 (1 << 17) +#define RETRO_SIMD_POPCNT (1 << 18) +#define RETRO_SIMD_MOVBE (1 << 19) +#define RETRO_SIMD_CMOV (1 << 20) +#define RETRO_SIMD_ASIMD (1 << 21) + +typedef uint64_t retro_perf_tick_t; +typedef int64_t retro_time_t; + +struct retro_perf_counter +{ + const char *ident; + retro_perf_tick_t start; + retro_perf_tick_t total; + retro_perf_tick_t call_cnt; + + bool registered; +}; + +/* Returns current time in microseconds. + * Tries to use the most accurate timer available. + */ +typedef retro_time_t (RETRO_CALLCONV *retro_perf_get_time_usec_t)(void); + +/* A simple counter. Usually nanoseconds, but can also be CPU cycles. + * Can be used directly if desired (when creating a more sophisticated + * performance counter system). + * */ +typedef retro_perf_tick_t (RETRO_CALLCONV *retro_perf_get_counter_t)(void); + +/* Returns a bit-mask of detected CPU features (RETRO_SIMD_*). */ +typedef uint64_t (RETRO_CALLCONV *retro_get_cpu_features_t)(void); + +/* Asks frontend to log and/or display the state of performance counters. + * Performance counters can always be poked into manually as well. + */ +typedef void (RETRO_CALLCONV *retro_perf_log_t)(void); + +/* Register a performance counter. + * ident field must be set with a discrete value and other values in + * retro_perf_counter must be 0. + * Registering can be called multiple times. To avoid calling to + * frontend redundantly, you can check registered field first. */ +typedef void (RETRO_CALLCONV *retro_perf_register_t)(struct retro_perf_counter *counter); + +/* Starts a registered counter. */ +typedef void (RETRO_CALLCONV *retro_perf_start_t)(struct retro_perf_counter *counter); + +/* Stops a registered counter. */ +typedef void (RETRO_CALLCONV *retro_perf_stop_t)(struct retro_perf_counter *counter); + +/* For convenience it can be useful to wrap register, start and stop in macros. + * E.g.: + * #ifdef LOG_PERFORMANCE + * #define RETRO_PERFORMANCE_INIT(perf_cb, name) static struct retro_perf_counter name = {#name}; if (!name.registered) perf_cb.perf_register(&(name)) + * #define RETRO_PERFORMANCE_START(perf_cb, name) perf_cb.perf_start(&(name)) + * #define RETRO_PERFORMANCE_STOP(perf_cb, name) perf_cb.perf_stop(&(name)) + * #else + * ... Blank macros ... + * #endif + * + * These can then be used mid-functions around code snippets. + * + * extern struct retro_perf_callback perf_cb; * Somewhere in the core. + * + * void do_some_heavy_work(void) + * { + * RETRO_PERFORMANCE_INIT(cb, work_1; + * RETRO_PERFORMANCE_START(cb, work_1); + * heavy_work_1(); + * RETRO_PERFORMANCE_STOP(cb, work_1); + * + * RETRO_PERFORMANCE_INIT(cb, work_2); + * RETRO_PERFORMANCE_START(cb, work_2); + * heavy_work_2(); + * RETRO_PERFORMANCE_STOP(cb, work_2); + * } + * + * void retro_deinit(void) + * { + * perf_cb.perf_log(); * Log all perf counters here for example. + * } + */ + +struct retro_perf_callback +{ + retro_perf_get_time_usec_t get_time_usec; + retro_get_cpu_features_t get_cpu_features; + + retro_perf_get_counter_t get_perf_counter; + retro_perf_register_t perf_register; + retro_perf_start_t perf_start; + retro_perf_stop_t perf_stop; + retro_perf_log_t perf_log; +}; + +/* FIXME: Document the sensor API and work out behavior. + * It will be marked as experimental until then. + */ +enum retro_sensor_action +{ + RETRO_SENSOR_ACCELEROMETER_ENABLE = 0, + RETRO_SENSOR_ACCELEROMETER_DISABLE, + RETRO_SENSOR_GYROSCOPE_ENABLE, + RETRO_SENSOR_GYROSCOPE_DISABLE, + RETRO_SENSOR_ILLUMINANCE_ENABLE, + RETRO_SENSOR_ILLUMINANCE_DISABLE, + + RETRO_SENSOR_DUMMY = INT_MAX +}; + +/* Id values for SENSOR types. */ +#define RETRO_SENSOR_ACCELEROMETER_X 0 +#define RETRO_SENSOR_ACCELEROMETER_Y 1 +#define RETRO_SENSOR_ACCELEROMETER_Z 2 +#define RETRO_SENSOR_GYROSCOPE_X 3 +#define RETRO_SENSOR_GYROSCOPE_Y 4 +#define RETRO_SENSOR_GYROSCOPE_Z 5 +#define RETRO_SENSOR_ILLUMINANCE 6 + +typedef bool (RETRO_CALLCONV *retro_set_sensor_state_t)(unsigned port, + enum retro_sensor_action action, unsigned rate); + +typedef float (RETRO_CALLCONV *retro_sensor_get_input_t)(unsigned port, unsigned id); + +struct retro_sensor_interface +{ + retro_set_sensor_state_t set_sensor_state; + retro_sensor_get_input_t get_sensor_input; +}; + +enum retro_camera_buffer +{ + RETRO_CAMERA_BUFFER_OPENGL_TEXTURE = 0, + RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER, + + RETRO_CAMERA_BUFFER_DUMMY = INT_MAX +}; + +/* Starts the camera driver. Can only be called in retro_run(). */ +typedef bool (RETRO_CALLCONV *retro_camera_start_t)(void); + +/* Stops the camera driver. Can only be called in retro_run(). */ +typedef void (RETRO_CALLCONV *retro_camera_stop_t)(void); + +/* Callback which signals when the camera driver is initialized + * and/or deinitialized. + * retro_camera_start_t can be called in initialized callback. + */ +typedef void (RETRO_CALLCONV *retro_camera_lifetime_status_t)(void); + +/* A callback for raw framebuffer data. buffer points to an XRGB8888 buffer. + * Width, height and pitch are similar to retro_video_refresh_t. + * First pixel is top-left origin. + */ +typedef void (RETRO_CALLCONV *retro_camera_frame_raw_framebuffer_t)(const uint32_t *buffer, + unsigned width, unsigned height, size_t pitch); + +/* A callback for when OpenGL textures are used. + * + * texture_id is a texture owned by camera driver. + * Its state or content should be considered immutable, except for things like + * texture filtering and clamping. + * + * texture_target is the texture target for the GL texture. + * These can include e.g. GL_TEXTURE_2D, GL_TEXTURE_RECTANGLE, and possibly + * more depending on extensions. + * + * affine points to a packed 3x3 column-major matrix used to apply an affine + * transform to texture coordinates. (affine_matrix * vec3(coord_x, coord_y, 1.0)) + * After transform, normalized texture coord (0, 0) should be bottom-left + * and (1, 1) should be top-right (or (width, height) for RECTANGLE). + * + * GL-specific typedefs are avoided here to avoid relying on gl.h in + * the API definition. + */ +typedef void (RETRO_CALLCONV *retro_camera_frame_opengl_texture_t)(unsigned texture_id, + unsigned texture_target, const float *affine); + +struct retro_camera_callback +{ + /* Set by libretro core. + * Example bitmask: caps = (1 << RETRO_CAMERA_BUFFER_OPENGL_TEXTURE) | (1 << RETRO_CAMERA_BUFFER_RAW_FRAMEBUFFER). + */ + uint64_t caps; + + /* Desired resolution for camera. Is only used as a hint. */ + unsigned width; + unsigned height; + + /* Set by frontend. */ + retro_camera_start_t start; + retro_camera_stop_t stop; + + /* Set by libretro core if raw framebuffer callbacks will be used. */ + retro_camera_frame_raw_framebuffer_t frame_raw_framebuffer; + + /* Set by libretro core if OpenGL texture callbacks will be used. */ + retro_camera_frame_opengl_texture_t frame_opengl_texture; + + /* Set by libretro core. Called after camera driver is initialized and + * ready to be started. + * Can be NULL, in which this callback is not called. + */ + retro_camera_lifetime_status_t initialized; + + /* Set by libretro core. Called right before camera driver is + * deinitialized. + * Can be NULL, in which this callback is not called. + */ + retro_camera_lifetime_status_t deinitialized; +}; + +/* Sets the interval of time and/or distance at which to update/poll + * location-based data. + * + * To ensure compatibility with all location-based implementations, + * values for both interval_ms and interval_distance should be provided. + * + * interval_ms is the interval expressed in milliseconds. + * interval_distance is the distance interval expressed in meters. + */ +typedef void (RETRO_CALLCONV *retro_location_set_interval_t)(unsigned interval_ms, + unsigned interval_distance); + +/* Start location services. The device will start listening for changes to the + * current location at regular intervals (which are defined with + * retro_location_set_interval_t). */ +typedef bool (RETRO_CALLCONV *retro_location_start_t)(void); + +/* Stop location services. The device will stop listening for changes + * to the current location. */ +typedef void (RETRO_CALLCONV *retro_location_stop_t)(void); + +/* Get the position of the current location. Will set parameters to + * 0 if no new location update has happened since the last time. */ +typedef bool (RETRO_CALLCONV *retro_location_get_position_t)(double *lat, double *lon, + double *horiz_accuracy, double *vert_accuracy); + +/* Callback which signals when the location driver is initialized + * and/or deinitialized. + * retro_location_start_t can be called in initialized callback. + */ +typedef void (RETRO_CALLCONV *retro_location_lifetime_status_t)(void); + +struct retro_location_callback +{ + retro_location_start_t start; + retro_location_stop_t stop; + retro_location_get_position_t get_position; + retro_location_set_interval_t set_interval; + + retro_location_lifetime_status_t initialized; + retro_location_lifetime_status_t deinitialized; +}; + +enum retro_rumble_effect +{ + RETRO_RUMBLE_STRONG = 0, + RETRO_RUMBLE_WEAK = 1, + + RETRO_RUMBLE_DUMMY = INT_MAX +}; + +/* Sets rumble state for joypad plugged in port 'port'. + * Rumble effects are controlled independently, + * and setting e.g. strong rumble does not override weak rumble. + * Strength has a range of [0, 0xffff]. + * + * Returns true if rumble state request was honored. + * Calling this before first retro_run() is likely to return false. */ +typedef bool (RETRO_CALLCONV *retro_set_rumble_state_t)(unsigned port, + enum retro_rumble_effect effect, uint16_t strength); + +struct retro_rumble_interface +{ + retro_set_rumble_state_t set_rumble_state; +}; + +/* Notifies libretro that audio data should be written. */ +typedef void (RETRO_CALLCONV *retro_audio_callback_t)(void); + +/* True: Audio driver in frontend is active, and callback is + * expected to be called regularily. + * False: Audio driver in frontend is paused or inactive. + * Audio callback will not be called until set_state has been + * called with true. + * Initial state is false (inactive). + */ +typedef void (RETRO_CALLCONV *retro_audio_set_state_callback_t)(bool enabled); + +struct retro_audio_callback +{ + retro_audio_callback_t callback; + retro_audio_set_state_callback_t set_state; +}; + +/* Notifies a libretro core of time spent since last invocation + * of retro_run() in microseconds. + * + * It will be called right before retro_run() every frame. + * The frontend can tamper with timing to support cases like + * fast-forward, slow-motion and framestepping. + * + * In those scenarios the reference frame time value will be used. */ +typedef int64_t retro_usec_t; +typedef void (RETRO_CALLCONV *retro_frame_time_callback_t)(retro_usec_t usec); +struct retro_frame_time_callback +{ + retro_frame_time_callback_t callback; + /* Represents the time of one frame. It is computed as + * 1000000 / fps, but the implementation will resolve the + * rounding to ensure that framestepping, etc is exact. */ + retro_usec_t reference; +}; + +/* Notifies a libretro core of the current occupancy + * level of the frontend audio buffer. + * + * - active: 'true' if audio buffer is currently + * in use. Will be 'false' if audio is + * disabled in the frontend + * + * - occupancy: Given as a value in the range [0,100], + * corresponding to the occupancy percentage + * of the audio buffer + * + * - underrun_likely: 'true' if the frontend expects an + * audio buffer underrun during the + * next frame (indicates that a core + * should attempt frame skipping) + * + * It will be called right before retro_run() every frame. */ +typedef void (RETRO_CALLCONV *retro_audio_buffer_status_callback_t)( + bool active, unsigned occupancy, bool underrun_likely); +struct retro_audio_buffer_status_callback +{ + retro_audio_buffer_status_callback_t callback; +}; + +/* Pass this to retro_video_refresh_t if rendering to hardware. + * Passing NULL to retro_video_refresh_t is still a frame dupe as normal. + * */ +#define RETRO_HW_FRAME_BUFFER_VALID ((void*)-1) + +/* Invalidates the current HW context. + * Any GL state is lost, and must not be deinitialized explicitly. + * If explicit deinitialization is desired by the libretro core, + * it should implement context_destroy callback. + * If called, all GPU resources must be reinitialized. + * Usually called when frontend reinits video driver. + * Also called first time video driver is initialized, + * allowing libretro core to initialize resources. + */ +typedef void (RETRO_CALLCONV *retro_hw_context_reset_t)(void); + +/* Gets current framebuffer which is to be rendered to. + * Could change every frame potentially. + */ +typedef uintptr_t (RETRO_CALLCONV *retro_hw_get_current_framebuffer_t)(void); + +/* Get a symbol from HW context. */ +typedef retro_proc_address_t (RETRO_CALLCONV *retro_hw_get_proc_address_t)(const char *sym); + +enum retro_hw_context_type +{ + RETRO_HW_CONTEXT_NONE = 0, + /* OpenGL 2.x. Driver can choose to use latest compatibility context. */ + RETRO_HW_CONTEXT_OPENGL = 1, + /* OpenGL ES 2.0. */ + RETRO_HW_CONTEXT_OPENGLES2 = 2, + /* Modern desktop core GL context. Use version_major/ + * version_minor fields to set GL version. */ + RETRO_HW_CONTEXT_OPENGL_CORE = 3, + /* OpenGL ES 3.0 */ + RETRO_HW_CONTEXT_OPENGLES3 = 4, + /* OpenGL ES 3.1+. Set version_major/version_minor. For GLES2 and GLES3, + * use the corresponding enums directly. */ + RETRO_HW_CONTEXT_OPENGLES_VERSION = 5, + + /* Vulkan, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE. */ + RETRO_HW_CONTEXT_VULKAN = 6, + + /* Direct3D11, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE */ + RETRO_HW_CONTEXT_D3D11 = 7, + + /* Direct3D10, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE */ + RETRO_HW_CONTEXT_D3D10 = 8, + + /* Direct3D12, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE */ + RETRO_HW_CONTEXT_D3D12 = 9, + + /* Direct3D9, see RETRO_ENVIRONMENT_GET_HW_RENDER_INTERFACE */ + RETRO_HW_CONTEXT_D3D9 = 10, + + RETRO_HW_CONTEXT_DUMMY = INT_MAX +}; + +struct retro_hw_render_callback +{ + /* Which API to use. Set by libretro core. */ + enum retro_hw_context_type context_type; + + /* Called when a context has been created or when it has been reset. + * An OpenGL context is only valid after context_reset() has been called. + * + * When context_reset is called, OpenGL resources in the libretro + * implementation are guaranteed to be invalid. + * + * It is possible that context_reset is called multiple times during an + * application lifecycle. + * If context_reset is called without any notification (context_destroy), + * the OpenGL context was lost and resources should just be recreated + * without any attempt to "free" old resources. + */ + retro_hw_context_reset_t context_reset; + + /* Set by frontend. + * TODO: This is rather obsolete. The frontend should not + * be providing preallocated framebuffers. */ + retro_hw_get_current_framebuffer_t get_current_framebuffer; + + /* Set by frontend. + * Can return all relevant functions, including glClear on Windows. */ + retro_hw_get_proc_address_t get_proc_address; + + /* Set if render buffers should have depth component attached. + * TODO: Obsolete. */ + bool depth; + + /* Set if stencil buffers should be attached. + * TODO: Obsolete. */ + bool stencil; + + /* If depth and stencil are true, a packed 24/8 buffer will be added. + * Only attaching stencil is invalid and will be ignored. */ + + /* Use conventional bottom-left origin convention. If false, + * standard libretro top-left origin semantics are used. + * TODO: Move to GL specific interface. */ + bool bottom_left_origin; + + /* Major version number for core GL context or GLES 3.1+. */ + unsigned version_major; + + /* Minor version number for core GL context or GLES 3.1+. */ + unsigned version_minor; + + /* If this is true, the frontend will go very far to avoid + * resetting context in scenarios like toggling fullscreen, etc. + * TODO: Obsolete? Maybe frontend should just always assume this ... + */ + bool cache_context; + + /* The reset callback might still be called in extreme situations + * such as if the context is lost beyond recovery. + * + * For optimal stability, set this to false, and allow context to be + * reset at any time. + */ + + /* A callback to be called before the context is destroyed in a + * controlled way by the frontend. */ + retro_hw_context_reset_t context_destroy; + + /* OpenGL resources can be deinitialized cleanly at this step. + * context_destroy can be set to NULL, in which resources will + * just be destroyed without any notification. + * + * Even when context_destroy is non-NULL, it is possible that + * context_reset is called without any destroy notification. + * This happens if context is lost by external factors (such as + * notified by GL_ARB_robustness). + * + * In this case, the context is assumed to be already dead, + * and the libretro implementation must not try to free any OpenGL + * resources in the subsequent context_reset. + */ + + /* Creates a debug context. */ + bool debug_context; +}; + +/* Callback type passed in RETRO_ENVIRONMENT_SET_KEYBOARD_CALLBACK. + * Called by the frontend in response to keyboard events. + * down is set if the key is being pressed, or false if it is being released. + * keycode is the RETROK value of the char. + * character is the text character of the pressed key. (UTF-32). + * key_modifiers is a set of RETROKMOD values or'ed together. + * + * The pressed/keycode state can be indepedent of the character. + * It is also possible that multiple characters are generated from a + * single keypress. + * Keycode events should be treated separately from character events. + * However, when possible, the frontend should try to synchronize these. + * If only a character is posted, keycode should be RETROK_UNKNOWN. + * + * Similarily if only a keycode event is generated with no corresponding + * character, character should be 0. + */ +typedef void (RETRO_CALLCONV *retro_keyboard_event_t)(bool down, unsigned keycode, + uint32_t character, uint16_t key_modifiers); + +struct retro_keyboard_callback +{ + retro_keyboard_event_t callback; +}; + +/* Callbacks for RETRO_ENVIRONMENT_SET_DISK_CONTROL_INTERFACE & + * RETRO_ENVIRONMENT_SET_DISK_CONTROL_EXT_INTERFACE. + * Should be set for implementations which can swap out multiple disk + * images in runtime. + * + * If the implementation can do this automatically, it should strive to do so. + * However, there are cases where the user must manually do so. + * + * Overview: To swap a disk image, eject the disk image with + * set_eject_state(true). + * Set the disk index with set_image_index(index). Insert the disk again + * with set_eject_state(false). + */ + +/* If ejected is true, "ejects" the virtual disk tray. + * When ejected, the disk image index can be set. + */ +typedef bool (RETRO_CALLCONV *retro_set_eject_state_t)(bool ejected); + +/* Gets current eject state. The initial state is 'not ejected'. */ +typedef bool (RETRO_CALLCONV *retro_get_eject_state_t)(void); + +/* Gets current disk index. First disk is index 0. + * If return value is >= get_num_images(), no disk is currently inserted. + */ +typedef unsigned (RETRO_CALLCONV *retro_get_image_index_t)(void); + +/* Sets image index. Can only be called when disk is ejected. + * The implementation supports setting "no disk" by using an + * index >= get_num_images(). + */ +typedef bool (RETRO_CALLCONV *retro_set_image_index_t)(unsigned index); + +/* Gets total number of images which are available to use. */ +typedef unsigned (RETRO_CALLCONV *retro_get_num_images_t)(void); + +struct retro_game_info; + +/* Replaces the disk image associated with index. + * Arguments to pass in info have same requirements as retro_load_game(). + * Virtual disk tray must be ejected when calling this. + * + * Replacing a disk image with info = NULL will remove the disk image + * from the internal list. + * As a result, calls to get_image_index() can change. + * + * E.g. replace_image_index(1, NULL), and previous get_image_index() + * returned 4 before. + * Index 1 will be removed, and the new index is 3. + */ +typedef bool (RETRO_CALLCONV *retro_replace_image_index_t)(unsigned index, + const struct retro_game_info *info); + +/* Adds a new valid index (get_num_images()) to the internal disk list. + * This will increment subsequent return values from get_num_images() by 1. + * This image index cannot be used until a disk image has been set + * with replace_image_index. */ +typedef bool (RETRO_CALLCONV *retro_add_image_index_t)(void); + +/* Sets initial image to insert in drive when calling + * core_load_game(). + * Since we cannot pass the initial index when loading + * content (this would require a major API change), this + * is set by the frontend *before* calling the core's + * retro_load_game()/retro_load_game_special() implementation. + * A core should therefore cache the index/path values and handle + * them inside retro_load_game()/retro_load_game_special(). + * - If 'index' is invalid (index >= get_num_images()), the + * core should ignore the set value and instead use 0 + * - 'path' is used purely for error checking - i.e. when + * content is loaded, the core should verify that the + * disk specified by 'index' has the specified file path. + * This is to guard against auto selecting the wrong image + * if (for example) the user should modify an existing M3U + * playlist. We have to let the core handle this because + * set_initial_image() must be called before loading content, + * i.e. the frontend cannot access image paths in advance + * and thus cannot perform the error check itself. + * If set path and content path do not match, the core should + * ignore the set 'index' value and instead use 0 + * Returns 'false' if index or 'path' are invalid, or core + * does not support this functionality + */ +typedef bool (RETRO_CALLCONV *retro_set_initial_image_t)(unsigned index, const char *path); + +/* Fetches the path of the specified disk image file. + * Returns 'false' if index is invalid (index >= get_num_images()) + * or path is otherwise unavailable. + */ +typedef bool (RETRO_CALLCONV *retro_get_image_path_t)(unsigned index, char *path, size_t len); + +/* Fetches a core-provided 'label' for the specified disk + * image file. In the simplest case this may be a file name + * (without extension), but for cores with more complex + * content requirements information may be provided to + * facilitate user disk swapping - for example, a core + * running floppy-disk-based content may uniquely label + * save disks, data disks, level disks, etc. with names + * corresponding to in-game disk change prompts (so the + * frontend can provide better user guidance than a 'dumb' + * disk index value). + * Returns 'false' if index is invalid (index >= get_num_images()) + * or label is otherwise unavailable. + */ +typedef bool (RETRO_CALLCONV *retro_get_image_label_t)(unsigned index, char *label, size_t len); + +struct retro_disk_control_callback +{ + retro_set_eject_state_t set_eject_state; + retro_get_eject_state_t get_eject_state; + + retro_get_image_index_t get_image_index; + retro_set_image_index_t set_image_index; + retro_get_num_images_t get_num_images; + + retro_replace_image_index_t replace_image_index; + retro_add_image_index_t add_image_index; +}; + +struct retro_disk_control_ext_callback +{ + retro_set_eject_state_t set_eject_state; + retro_get_eject_state_t get_eject_state; + + retro_get_image_index_t get_image_index; + retro_set_image_index_t set_image_index; + retro_get_num_images_t get_num_images; + + retro_replace_image_index_t replace_image_index; + retro_add_image_index_t add_image_index; + + /* NOTE: Frontend will only attempt to record/restore + * last used disk index if both set_initial_image() + * and get_image_path() are implemented */ + retro_set_initial_image_t set_initial_image; /* Optional - may be NULL */ + + retro_get_image_path_t get_image_path; /* Optional - may be NULL */ + retro_get_image_label_t get_image_label; /* Optional - may be NULL */ +}; + +/* Definitions for RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE. + * A core can set it if sending and receiving custom network packets + * during a multiplayer session is desired. + */ + +/* Netpacket flags for retro_netpacket_send_t */ +#define RETRO_NETPACKET_UNRELIABLE 0 /* Packet to be sent unreliable, depending on network quality it might not arrive. */ +#define RETRO_NETPACKET_RELIABLE (1 << 0) /* Reliable packets are guaranteed to arrive at the target in the order they were send. */ +#define RETRO_NETPACKET_UNSEQUENCED (1 << 1) /* Packet will not be sequenced with other packets and may arrive out of order. Cannot be set on reliable packets. */ + +/* Used by the core to send a packet to one or more connected players. + * A single packet sent via this interface can contain up to 64 KB of data. + * + * The broadcast flag can be set to true to send to multiple connected clients. + * In a broadcast, the client_id argument indicates 1 client NOT to send the + * packet to (pass 0xFFFF to send to everyone). Otherwise, the client_id + * argument indicates a single client to send the packet to. + * + * A frontend must support sending reliable packets (RETRO_NETPACKET_RELIABLE). + * Unreliable packets might not be supported by the frontend, but the flags can + * still be specified. Reliable transmission will be used instead. + * + * If this function is called passing NULL for buf, it will instead flush all + * previously buffered outgoing packets and instantly read any incoming packets. + * During such a call, retro_netpacket_receive_t and retro_netpacket_stop_t can + * be called. The core can perform this in a loop to do a blocking read, i.e., + * wait for incoming data, but needs to handle stop getting called and also + * give up after a short while to avoid freezing on a connection problem. + * + * This function is not guaranteed to be thread-safe and must be called during + * retro_run or any of the netpacket callbacks passed with this interface. + */ +typedef void (RETRO_CALLCONV *retro_netpacket_send_t)(int flags, const void* buf, size_t len, uint16_t client_id, bool broadcast); + +/* Called by the frontend to signify that a multiplayer session has started. + * If client_id is 0 the local player is the host of the session and at this + * point no other player has connected yet. + * + * If client_id is > 0 the local player is a client connected to a host and + * at this point is already fully connected to the host. + * + * The core must store the retro_netpacket_send_t function pointer provided + * here and use it whenever it wants to send a packet. This function pointer + * remains valid until the frontend calls retro_netpacket_stop_t. + */ +typedef void (RETRO_CALLCONV *retro_netpacket_start_t)(uint16_t client_id, retro_netpacket_send_t send_fn); + +/* Called by the frontend when a new packet arrives which has been sent from + * another player with retro_netpacket_send_t. The client_id argument indicates + * who has sent the packet. + */ +typedef void (RETRO_CALLCONV *retro_netpacket_receive_t)(const void* buf, size_t len, uint16_t client_id); + +/* Called by the frontend when the multiplayer session has ended. + * Once this gets called the retro_netpacket_send_t function pointer passed + * to retro_netpacket_start_t will not be valid anymore. + */ +typedef void (RETRO_CALLCONV *retro_netpacket_stop_t)(void); + +/* Called by the frontend every frame (between calls to retro_run while + * updating the state of the multiplayer session. + * This is a good place for the core to call retro_netpacket_send_t from. + */ +typedef void (RETRO_CALLCONV *retro_netpacket_poll_t)(void); + +/* Called by the frontend when a new player connects to the hosted session. + * This is only called on the host side, not for clients connected to the host. + * If this function returns false, the newly connected player gets dropped. + * This can be used for example to limit the number of players. + */ +typedef bool (RETRO_CALLCONV *retro_netpacket_connected_t)(uint16_t client_id); + +/* Called by the frontend when a player leaves or disconnects from the hosted session. + * This is only called on the host side, not for clients connected to the host. + */ +typedef void (RETRO_CALLCONV *retro_netpacket_disconnected_t)(uint16_t client_id); + +/** + * A callback interface for giving a core the ability to send and receive custom + * network packets during a multiplayer session between two or more instances + * of a libretro frontend. + * + * @see RETRO_ENVIRONMENT_SET_NETPACKET_INTERFACE + */ +struct retro_netpacket_callback +{ + retro_netpacket_start_t start; + retro_netpacket_receive_t receive; + retro_netpacket_stop_t stop; /* Optional - may be NULL */ + retro_netpacket_poll_t poll; /* Optional - may be NULL */ + retro_netpacket_connected_t connected; /* Optional - may be NULL */ + retro_netpacket_disconnected_t disconnected; /* Optional - may be NULL */ +}; + +enum retro_pixel_format +{ + /* 0RGB1555, native endian. + * 0 bit must be set to 0. + * This pixel format is default for compatibility concerns only. + * If a 15/16-bit pixel format is desired, consider using RGB565. */ + RETRO_PIXEL_FORMAT_0RGB1555 = 0, + + /* XRGB8888, native endian. + * X bits are ignored. */ + RETRO_PIXEL_FORMAT_XRGB8888 = 1, + + /* RGB565, native endian. + * This pixel format is the recommended format to use if a 15/16-bit + * format is desired as it is the pixel format that is typically + * available on a wide range of low-power devices. + * + * It is also natively supported in APIs like OpenGL ES. */ + RETRO_PIXEL_FORMAT_RGB565 = 2, + + /* Ensure sizeof() == sizeof(int). */ + RETRO_PIXEL_FORMAT_UNKNOWN = INT_MAX +}; + +enum retro_savestate_context +{ + /* Standard savestate written to disk. */ + RETRO_SAVESTATE_CONTEXT_NORMAL = 0, + + /* Savestate where you are guaranteed that the same instance will load the save state. + * You can store internal pointers to code or data. + * It's still a full serialization and deserialization, and could be loaded or saved at any time. + * It won't be written to disk or sent over the network. + */ + RETRO_SAVESTATE_CONTEXT_RUNAHEAD_SAME_INSTANCE = 1, + + /* Savestate where you are guaranteed that the same emulator binary will load that savestate. + * You can skip anything that would slow down saving or loading state but you can not store internal pointers. + * It won't be written to disk or sent over the network. + * Example: "Second Instance" runahead + */ + RETRO_SAVESTATE_CONTEXT_RUNAHEAD_SAME_BINARY = 2, + + /* Savestate used within a rollback netplay feature. + * You should skip anything that would unnecessarily increase bandwidth usage. + * It won't be written to disk but it will be sent over the network. + */ + RETRO_SAVESTATE_CONTEXT_ROLLBACK_NETPLAY = 3, + + /* Ensure sizeof() == sizeof(int). */ + RETRO_SAVESTATE_CONTEXT_UNKNOWN = INT_MAX +}; + +struct retro_message +{ + const char *msg; /* Message to be displayed. */ + unsigned frames; /* Duration in frames of message. */ +}; + +enum retro_message_target +{ + RETRO_MESSAGE_TARGET_ALL = 0, + RETRO_MESSAGE_TARGET_OSD, + RETRO_MESSAGE_TARGET_LOG +}; + +enum retro_message_type +{ + RETRO_MESSAGE_TYPE_NOTIFICATION = 0, + RETRO_MESSAGE_TYPE_NOTIFICATION_ALT, + RETRO_MESSAGE_TYPE_STATUS, + RETRO_MESSAGE_TYPE_PROGRESS +}; + +struct retro_message_ext +{ + /* Message string to be displayed/logged */ + const char *msg; + /* Duration (in ms) of message when targeting the OSD */ + unsigned duration; + /* Message priority when targeting the OSD + * > When multiple concurrent messages are sent to + * the frontend and the frontend does not have the + * capacity to display them all, messages with the + * *highest* priority value should be shown + * > There is no upper limit to a message priority + * value (within the bounds of the unsigned data type) + * > In the reference frontend (RetroArch), the same + * priority values are used for frontend-generated + * notifications, which are typically assigned values + * between 0 and 3 depending upon importance */ + unsigned priority; + /* Message logging level (info, warn, error, etc.) */ + enum retro_log_level level; + /* Message destination: OSD, logging interface or both */ + enum retro_message_target target; + /* Message 'type' when targeting the OSD + * > RETRO_MESSAGE_TYPE_NOTIFICATION: Specifies that a + * message should be handled in identical fashion to + * a standard frontend-generated notification + * > RETRO_MESSAGE_TYPE_NOTIFICATION_ALT: Specifies that + * message is a notification that requires user attention + * or action, but that it should be displayed in a manner + * that differs from standard frontend-generated notifications. + * This would typically correspond to messages that should be + * displayed immediately (independently from any internal + * frontend message queue), and/or which should be visually + * distinguishable from frontend-generated notifications. + * For example, a core may wish to inform the user of + * information related to a disk-change event. It is + * expected that the frontend itself may provide a + * notification in this case; if the core sends a + * message of type RETRO_MESSAGE_TYPE_NOTIFICATION, an + * uncomfortable 'double-notification' may occur. A message + * of RETRO_MESSAGE_TYPE_NOTIFICATION_ALT should therefore + * be presented such that visual conflict with regular + * notifications does not occur + * > RETRO_MESSAGE_TYPE_STATUS: Indicates that message + * is not a standard notification. This typically + * corresponds to 'status' indicators, such as a core's + * internal FPS, which are intended to be displayed + * either permanently while a core is running, or in + * a manner that does not suggest user attention or action + * is required. 'Status' type messages should therefore be + * displayed in a different on-screen location and in a manner + * easily distinguishable from both standard frontend-generated + * notifications and messages of type RETRO_MESSAGE_TYPE_NOTIFICATION_ALT + * > RETRO_MESSAGE_TYPE_PROGRESS: Indicates that message reports + * the progress of an internal core task. For example, in cases + * where a core itself handles the loading of content from a file, + * this may correspond to the percentage of the file that has been + * read. Alternatively, an audio/video playback core may use a + * message of type RETRO_MESSAGE_TYPE_PROGRESS to display the current + * playback position as a percentage of the runtime. 'Progress' type + * messages should therefore be displayed as a literal progress bar, + * where: + * - 'retro_message_ext.msg' is the progress bar title/label + * - 'retro_message_ext.progress' determines the length of + * the progress bar + * NOTE: Message type is a *hint*, and may be ignored + * by the frontend. If a frontend lacks support for + * displaying messages via alternate means than standard + * frontend-generated notifications, it will treat *all* + * messages as having the type RETRO_MESSAGE_TYPE_NOTIFICATION */ + enum retro_message_type type; + /* Task progress when targeting the OSD and message is + * of type RETRO_MESSAGE_TYPE_PROGRESS + * > -1: Unmetered/indeterminate + * > 0-100: Current progress percentage + * NOTE: Since message type is a hint, a frontend may ignore + * progress values. Where relevant, a core should therefore + * include progress percentage within the message string, + * such that the message intent remains clear when displayed + * as a standard frontend-generated notification */ + int8_t progress; +}; + +/* Describes how the libretro implementation maps a libretro input bind + * to its internal input system through a human readable string. + * This string can be used to better let a user configure input. */ +struct retro_input_descriptor +{ + /* Associates given parameters with a description. */ + unsigned port; + unsigned device; + unsigned index; + unsigned id; + + /* Human readable description for parameters. + * The pointer must remain valid until + * retro_unload_game() is called. */ + const char *description; +}; + +struct retro_system_info +{ + /* All pointers are owned by libretro implementation, and pointers must + * remain valid until it is unloaded. */ + + const char *library_name; /* Descriptive name of library. Should not + * contain any version numbers, etc. */ + const char *library_version; /* Descriptive version of core. */ + + const char *valid_extensions; /* A string listing probably content + * extensions the core will be able to + * load, separated with pipe. + * I.e. "bin|rom|iso". + * Typically used for a GUI to filter + * out extensions. */ + + /* Libretro cores that need to have direct access to their content + * files, including cores which use the path of the content files to + * determine the paths of other files, should set need_fullpath to true. + * + * Cores should strive for setting need_fullpath to false, + * as it allows the frontend to perform patching, etc. + * + * If need_fullpath is true and retro_load_game() is called: + * - retro_game_info::path is guaranteed to have a valid path + * - retro_game_info::data and retro_game_info::size are invalid + * + * If need_fullpath is false and retro_load_game() is called: + * - retro_game_info::path may be NULL + * - retro_game_info::data and retro_game_info::size are guaranteed + * to be valid + * + * See also: + * - RETRO_ENVIRONMENT_GET_SYSTEM_DIRECTORY + * - RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY + */ + bool need_fullpath; + + /* If true, the frontend is not allowed to extract any archives before + * loading the real content. + * Necessary for certain libretro implementations that load games + * from zipped archives. */ + bool block_extract; +}; + +/* Defines overrides which modify frontend handling of + * specific content file types. + * An array of retro_system_content_info_override is + * passed to RETRO_ENVIRONMENT_SET_CONTENT_INFO_OVERRIDE + * NOTE: In the following descriptions, references to + * retro_load_game() may be replaced with + * retro_load_game_special() */ +struct retro_system_content_info_override +{ + /* A list of file extensions for which the override + * should apply, delimited by a 'pipe' character + * (e.g. "md|sms|gg") + * Permitted file extensions are limited to those + * included in retro_system_info::valid_extensions + * and/or retro_subsystem_rom_info::valid_extensions */ + const char *extensions; + + /* Overrides the need_fullpath value set in + * retro_system_info and/or retro_subsystem_rom_info. + * To reiterate: + * + * If need_fullpath is true and retro_load_game() is called: + * - retro_game_info::path is guaranteed to contain a valid + * path to an existent file + * - retro_game_info::data and retro_game_info::size are invalid + * + * If need_fullpath is false and retro_load_game() is called: + * - retro_game_info::path may be NULL + * - retro_game_info::data and retro_game_info::size are guaranteed + * to be valid + * + * In addition: + * + * If need_fullpath is true and retro_load_game() is called: + * - retro_game_info_ext::full_path is guaranteed to contain a valid + * path to an existent file + * - retro_game_info_ext::archive_path may be NULL + * - retro_game_info_ext::archive_file may be NULL + * - retro_game_info_ext::dir is guaranteed to contain a valid path + * to the directory in which the content file exists + * - retro_game_info_ext::name is guaranteed to contain the + * basename of the content file, without extension + * - retro_game_info_ext::ext is guaranteed to contain the + * extension of the content file in lower case format + * - retro_game_info_ext::data and retro_game_info_ext::size + * are invalid + * + * If need_fullpath is false and retro_load_game() is called: + * - If retro_game_info_ext::file_in_archive is false: + * - retro_game_info_ext::full_path is guaranteed to contain + * a valid path to an existent file + * - retro_game_info_ext::archive_path may be NULL + * - retro_game_info_ext::archive_file may be NULL + * - retro_game_info_ext::dir is guaranteed to contain a + * valid path to the directory in which the content file exists + * - retro_game_info_ext::name is guaranteed to contain the + * basename of the content file, without extension + * - retro_game_info_ext::ext is guaranteed to contain the + * extension of the content file in lower case format + * - If retro_game_info_ext::file_in_archive is true: + * - retro_game_info_ext::full_path may be NULL + * - retro_game_info_ext::archive_path is guaranteed to + * contain a valid path to an existent compressed file + * inside which the content file is located + * - retro_game_info_ext::archive_file is guaranteed to + * contain a valid path to an existent content file + * inside the compressed file referred to by + * retro_game_info_ext::archive_path + * e.g. for a compressed file '/path/to/foo.zip' + * containing 'bar.sfc' + * > retro_game_info_ext::archive_path will be '/path/to/foo.zip' + * > retro_game_info_ext::archive_file will be 'bar.sfc' + * - retro_game_info_ext::dir is guaranteed to contain a + * valid path to the directory in which the compressed file + * (containing the content file) exists + * - retro_game_info_ext::name is guaranteed to contain + * EITHER + * 1) the basename of the compressed file (containing + * the content file), without extension + * OR + * 2) the basename of the content file inside the + * compressed file, without extension + * In either case, a core should consider 'name' to + * be the canonical name/ID of the the content file + * - retro_game_info_ext::ext is guaranteed to contain the + * extension of the content file inside the compressed file, + * in lower case format + * - retro_game_info_ext::data and retro_game_info_ext::size are + * guaranteed to be valid */ + bool need_fullpath; + + /* If need_fullpath is false, specifies whether the content + * data buffer available in retro_load_game() is 'persistent' + * + * If persistent_data is false and retro_load_game() is called: + * - retro_game_info::data and retro_game_info::size + * are valid only until retro_load_game() returns + * - retro_game_info_ext::data and retro_game_info_ext::size + * are valid only until retro_load_game() returns + * + * If persistent_data is true and retro_load_game() is called: + * - retro_game_info::data and retro_game_info::size + * are valid until retro_deinit() returns + * - retro_game_info_ext::data and retro_game_info_ext::size + * are valid until retro_deinit() returns */ + bool persistent_data; +}; + +/* Similar to retro_game_info, but provides extended + * information about the source content file and + * game memory buffer status. + * And array of retro_game_info_ext is returned by + * RETRO_ENVIRONMENT_GET_GAME_INFO_EXT + * NOTE: In the following descriptions, references to + * retro_load_game() may be replaced with + * retro_load_game_special() */ +struct retro_game_info_ext +{ + /* - If file_in_archive is false, contains a valid + * path to an existent content file (UTF-8 encoded) + * - If file_in_archive is true, may be NULL */ + const char *full_path; + + /* - If file_in_archive is false, may be NULL + * - If file_in_archive is true, contains a valid path + * to an existent compressed file inside which the + * content file is located (UTF-8 encoded) */ + const char *archive_path; + + /* - If file_in_archive is false, may be NULL + * - If file_in_archive is true, contain a valid path + * to an existent content file inside the compressed + * file referred to by archive_path (UTF-8 encoded) + * e.g. for a compressed file '/path/to/foo.zip' + * containing 'bar.sfc' + * > archive_path will be '/path/to/foo.zip' + * > archive_file will be 'bar.sfc' */ + const char *archive_file; + + /* - If file_in_archive is false, contains a valid path + * to the directory in which the content file exists + * (UTF-8 encoded) + * - If file_in_archive is true, contains a valid path + * to the directory in which the compressed file + * (containing the content file) exists (UTF-8 encoded) */ + const char *dir; + + /* Contains the canonical name/ID of the content file + * (UTF-8 encoded). Intended for use when identifying + * 'complementary' content named after the loaded file - + * i.e. companion data of a different format (a CD image + * required by a ROM), texture packs, internally handled + * save files, etc. + * - If file_in_archive is false, contains the basename + * of the content file, without extension + * - If file_in_archive is true, then string is + * implementation specific. A frontend may choose to + * set a name value of: + * EITHER + * 1) the basename of the compressed file (containing + * the content file), without extension + * OR + * 2) the basename of the content file inside the + * compressed file, without extension + * RetroArch sets the 'name' value according to (1). + * A frontend that supports routine loading of + * content from archives containing multiple unrelated + * content files may set the 'name' value according + * to (2). */ + const char *name; + + /* - If file_in_archive is false, contains the extension + * of the content file in lower case format + * - If file_in_archive is true, contains the extension + * of the content file inside the compressed file, + * in lower case format */ + const char *ext; + + /* String of implementation specific meta-data. */ + const char *meta; + + /* Memory buffer of loaded game content. Will be NULL: + * IF + * - retro_system_info::need_fullpath is true and + * retro_system_content_info_override::need_fullpath + * is unset + * OR + * - retro_system_content_info_override::need_fullpath + * is true */ + const void *data; + + /* Size of game content memory buffer, in bytes */ + size_t size; + + /* True if loaded content file is inside a compressed + * archive */ + bool file_in_archive; + + /* - If data is NULL, value is unset/ignored + * - If data is non-NULL: + * - If persistent_data is false, data and size are + * valid only until retro_load_game() returns + * - If persistent_data is true, data and size are + * are valid until retro_deinit() returns */ + bool persistent_data; +}; + +struct retro_game_geometry +{ + unsigned base_width; /* Nominal video width of game. */ + unsigned base_height; /* Nominal video height of game. */ + unsigned max_width; /* Maximum possible width of game. */ + unsigned max_height; /* Maximum possible height of game. */ + + float aspect_ratio; /* Nominal aspect ratio of game. If + * aspect_ratio is <= 0.0, an aspect ratio + * of base_width / base_height is assumed. + * A frontend could override this setting, + * if desired. */ +}; + +struct retro_system_timing +{ + double fps; /* FPS of video content. */ + double sample_rate; /* Sampling rate of audio. */ +}; + +struct retro_system_av_info +{ + struct retro_game_geometry geometry; + struct retro_system_timing timing; +}; + +struct retro_variable +{ + /* Variable to query in RETRO_ENVIRONMENT_GET_VARIABLE. + * If NULL, obtains the complete environment string if more + * complex parsing is necessary. + * The environment string is formatted as key-value pairs + * delimited by semicolons as so: + * "key1=value1;key2=value2;..." + */ + const char *key; + + /* Value to be obtained. If key does not exist, it is set to NULL. */ + const char *value; +}; + +struct retro_core_option_display +{ + /* Variable to configure in RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY */ + const char *key; + + /* Specifies whether variable should be displayed + * when presenting core options to the user */ + bool visible; +}; + +/* Maximum number of values permitted for a core option + * > Note: We have to set a maximum value due the limitations + * of the C language - i.e. it is not possible to create an + * array of structs each containing a variable sized array, + * so the retro_core_option_definition values array must + * have a fixed size. The size limit of 128 is a balancing + * act - it needs to be large enough to support all 'sane' + * core options, but setting it too large may impact low memory + * platforms. In practise, if a core option has more than + * 128 values then the implementation is likely flawed. + * To quote the above API reference: + * "The number of possible options should be very limited + * i.e. it should be feasible to cycle through options + * without a keyboard." + */ +#define RETRO_NUM_CORE_OPTION_VALUES_MAX 128 + +struct retro_core_option_value +{ + /* Expected option value */ + const char *value; + + /* Human-readable value label. If NULL, value itself + * will be displayed by the frontend */ + const char *label; +}; + +struct retro_core_option_definition +{ + /* Variable to query in RETRO_ENVIRONMENT_GET_VARIABLE. */ + const char *key; + + /* Human-readable core option description (used as menu label) */ + const char *desc; + + /* Human-readable core option information (used as menu sublabel) */ + const char *info; + + /* Array of retro_core_option_value structs, terminated by NULL */ + struct retro_core_option_value values[RETRO_NUM_CORE_OPTION_VALUES_MAX]; + + /* Default core option value. Must match one of the values + * in the retro_core_option_value array, otherwise will be + * ignored */ + const char *default_value; +}; + +#ifdef __PS3__ +#undef local +#endif + +struct retro_core_options_intl +{ + /* Pointer to an array of retro_core_option_definition structs + * - US English implementation + * - Must point to a valid array */ + struct retro_core_option_definition *us; + + /* Pointer to an array of retro_core_option_definition structs + * - Implementation for current frontend language + * - May be NULL */ + struct retro_core_option_definition *local; +}; + +struct retro_core_option_v2_category +{ + /* Variable uniquely identifying the + * option category. Valid key characters + * are [a-z, A-Z, 0-9, _, -] */ + const char *key; + + /* Human-readable category description + * > Used as category menu label when + * frontend has core option category + * support */ + const char *desc; + + /* Human-readable category information + * > Used as category menu sublabel when + * frontend has core option category + * support + * > Optional (may be NULL or an empty + * string) */ + const char *info; +}; + +struct retro_core_option_v2_definition +{ + /* Variable to query in RETRO_ENVIRONMENT_GET_VARIABLE. + * Valid key characters are [a-z, A-Z, 0-9, _, -] */ + const char *key; + + /* Human-readable core option description + * > Used as menu label when frontend does + * not have core option category support + * e.g. "Video > Aspect Ratio" */ + const char *desc; + + /* Human-readable core option description + * > Used as menu label when frontend has + * core option category support + * e.g. "Aspect Ratio", where associated + * retro_core_option_v2_category::desc + * is "Video" + * > If empty or NULL, the string specified by + * desc will be used as the menu label + * > Will be ignored (and may be set to NULL) + * if category_key is empty or NULL */ + const char *desc_categorized; + + /* Human-readable core option information + * > Used as menu sublabel */ + const char *info; + + /* Human-readable core option information + * > Used as menu sublabel when frontend + * has core option category support + * (e.g. may be required when info text + * references an option by name/desc, + * and the desc/desc_categorized text + * for that option differ) + * > If empty or NULL, the string specified by + * info will be used as the menu sublabel + * > Will be ignored (and may be set to NULL) + * if category_key is empty or NULL */ + const char *info_categorized; + + /* Variable specifying category (e.g. "video", + * "audio") that will be assigned to the option + * if frontend has core option category support. + * > Categorized options will be displayed in a + * subsection/submenu of the frontend core + * option interface + * > Specified string must match one of the + * retro_core_option_v2_category::key values + * in the associated retro_core_option_v2_category + * array; If no match is not found, specified + * string will be considered as NULL + * > If specified string is empty or NULL, option will + * have no category and will be shown at the top + * level of the frontend core option interface */ + const char *category_key; + + /* Array of retro_core_option_value structs, terminated by NULL */ + struct retro_core_option_value values[RETRO_NUM_CORE_OPTION_VALUES_MAX]; + + /* Default core option value. Must match one of the values + * in the retro_core_option_value array, otherwise will be + * ignored */ + const char *default_value; +}; + +struct retro_core_options_v2 +{ + /* Array of retro_core_option_v2_category structs, + * terminated by NULL + * > If NULL, all entries in definitions array + * will have no category and will be shown at + * the top level of the frontend core option + * interface + * > Will be ignored if frontend does not have + * core option category support */ + struct retro_core_option_v2_category *categories; + + /* Array of retro_core_option_v2_definition structs, + * terminated by NULL */ + struct retro_core_option_v2_definition *definitions; +}; + +struct retro_core_options_v2_intl +{ + /* Pointer to a retro_core_options_v2 struct + * > US English implementation + * > Must point to a valid struct */ + struct retro_core_options_v2 *us; + + /* Pointer to a retro_core_options_v2 struct + * - Implementation for current frontend language + * - May be NULL */ + struct retro_core_options_v2 *local; +}; + +/* Used by the frontend to monitor changes in core option + * visibility. May be called each time any core option + * value is set via the frontend. + * - On each invocation, the core must update the visibility + * of any dynamically hidden options using the + * RETRO_ENVIRONMENT_SET_CORE_OPTIONS_DISPLAY environment + * callback. + * - On the first invocation, returns 'true' if the visibility + * of any core option has changed since the last call of + * retro_load_game() or retro_load_game_special(). + * - On each subsequent invocation, returns 'true' if the + * visibility of any core option has changed since the last + * time the function was called. */ +typedef bool (RETRO_CALLCONV *retro_core_options_update_display_callback_t)(void); +struct retro_core_options_update_display_callback +{ + retro_core_options_update_display_callback_t callback; +}; + +struct retro_game_info +{ + const char *path; /* Path to game, UTF-8 encoded. + * Sometimes used as a reference for building other paths. + * May be NULL if game was loaded from stdin or similar, + * but in this case some cores will be unable to load `data`. + * So, it is preferable to fabricate something here instead + * of passing NULL, which will help more cores to succeed. + * retro_system_info::need_fullpath requires + * that this path is valid. */ + const void *data; /* Memory buffer of loaded game. Will be NULL + * if need_fullpath was set. */ + size_t size; /* Size of memory buffer. */ + const char *meta; /* String of implementation specific meta-data. */ +}; + +#define RETRO_MEMORY_ACCESS_WRITE (1 << 0) + /* The core will write to the buffer provided by retro_framebuffer::data. */ +#define RETRO_MEMORY_ACCESS_READ (1 << 1) + /* The core will read from retro_framebuffer::data. */ +#define RETRO_MEMORY_TYPE_CACHED (1 << 0) + /* The memory in data is cached. + * If not cached, random writes and/or reading from the buffer is expected to be very slow. */ +struct retro_framebuffer +{ + void *data; /* The framebuffer which the core can render into. + Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. + The initial contents of data are unspecified. */ + unsigned width; /* The framebuffer width used by the core. Set by core. */ + unsigned height; /* The framebuffer height used by the core. Set by core. */ + size_t pitch; /* The number of bytes between the beginning of a scanline, + and beginning of the next scanline. + Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. */ + enum retro_pixel_format format; /* The pixel format the core must use to render into data. + This format could differ from the format used in + SET_PIXEL_FORMAT. + Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. */ + + unsigned access_flags; /* How the core will access the memory in the framebuffer. + RETRO_MEMORY_ACCESS_* flags. + Set by core. */ + unsigned memory_flags; /* Flags telling core how the memory has been mapped. + RETRO_MEMORY_TYPE_* flags. + Set by frontend in GET_CURRENT_SOFTWARE_FRAMEBUFFER. */ +}; + +/* Used by a libretro core to override the current + * fastforwarding mode of the frontend */ +struct retro_fastforwarding_override +{ + /* Specifies the runtime speed multiplier that + * will be applied when 'fastforward' is true. + * For example, a value of 5.0 when running 60 FPS + * content will cap the fast-forward rate at 300 FPS. + * Note that the target multiplier may not be achieved + * if the host hardware has insufficient processing + * power. + * Setting a value of 0.0 (or greater than 0.0 but + * less than 1.0) will result in an uncapped + * fast-forward rate (limited only by hardware + * capacity). + * If the value is negative, it will be ignored + * (i.e. the frontend will use a runtime speed + * multiplier of its own choosing) */ + float ratio; + + /* If true, fastforwarding mode will be enabled. + * If false, fastforwarding mode will be disabled. */ + bool fastforward; + + /* If true, and if supported by the frontend, an + * on-screen notification will be displayed while + * 'fastforward' is true. + * If false, and if supported by the frontend, any + * on-screen fast-forward notifications will be + * suppressed */ + bool notification; + + /* If true, the core will have sole control over + * when fastforwarding mode is enabled/disabled; + * the frontend will not be able to change the + * state set by 'fastforward' until either + * 'inhibit_toggle' is set to false, or the core + * is unloaded */ + bool inhibit_toggle; +}; + +/* During normal operation. Rate will be equal to the core's internal FPS. */ +#define RETRO_THROTTLE_NONE 0 + +/* While paused or stepping single frames. Rate will be 0. */ +#define RETRO_THROTTLE_FRAME_STEPPING 1 + +/* During fast forwarding. + * Rate will be 0 if not specifically limited to a maximum speed. */ +#define RETRO_THROTTLE_FAST_FORWARD 2 + +/* During slow motion. Rate will be less than the core's internal FPS. */ +#define RETRO_THROTTLE_SLOW_MOTION 3 + +/* While rewinding recorded save states. Rate can vary depending on the rewind + * speed or be 0 if the frontend is not aiming for a specific rate. */ +#define RETRO_THROTTLE_REWINDING 4 + +/* While vsync is active in the video driver and the target refresh rate is + * lower than the core's internal FPS. Rate is the target refresh rate. */ +#define RETRO_THROTTLE_VSYNC 5 + +/* When the frontend does not throttle in any way. Rate will be 0. + * An example could be if no vsync or audio output is active. */ +#define RETRO_THROTTLE_UNBLOCKED 6 + +struct retro_throttle_state +{ + /* The current throttling mode. Should be one of the values above. */ + unsigned mode; + + /* How many times per second the frontend aims to call retro_run. + * Depending on the mode, it can be 0 if there is no known fixed rate. + * This won't be accurate if the total processing time of the core and + * the frontend is longer than what is available for one frame. */ + float rate; +}; + +/** + * Opaque handle to a microphone that's been opened for use. + * The underlying object is accessed or created with \c retro_microphone_interface_t. + */ +typedef struct retro_microphone retro_microphone_t; + +/** + * Parameters for configuring a microphone. + * Some of these might not be honored, + * depending on the available hardware and driver configuration. + */ +typedef struct retro_microphone_params +{ + /** + * The desired sample rate of the microphone's input, in Hz. + * The microphone's input will be resampled, + * so cores can ask for whichever frequency they need. + * + * If zero, some reasonable default will be provided by the frontend + * (usually from its config file). + * + * @see retro_get_mic_rate_t + */ + unsigned rate; +} retro_microphone_params_t; + +/** + * @copydoc retro_microphone_interface::open_mic + */ +typedef retro_microphone_t *(RETRO_CALLCONV *retro_open_mic_t)(const retro_microphone_params_t *params); + +/** + * @copydoc retro_microphone_interface::close_mic + */ +typedef void (RETRO_CALLCONV *retro_close_mic_t)(retro_microphone_t *microphone); + +/** + * @copydoc retro_microphone_interface::get_params + */ +typedef bool (RETRO_CALLCONV *retro_get_mic_params_t)(const retro_microphone_t *microphone, retro_microphone_params_t *params); + +/** + * @copydoc retro_microphone_interface::set_mic_state + */ +typedef bool (RETRO_CALLCONV *retro_set_mic_state_t)(retro_microphone_t *microphone, bool state); + +/** + * @copydoc retro_microphone_interface::get_mic_state + */ +typedef bool (RETRO_CALLCONV *retro_get_mic_state_t)(const retro_microphone_t *microphone); + +/** + * @copydoc retro_microphone_interface::read_mic + */ +typedef int (RETRO_CALLCONV *retro_read_mic_t)(retro_microphone_t *microphone, int16_t* samples, size_t num_samples); + +/** + * The current version of the microphone interface. + * Will be incremented whenever \c retro_microphone_interface or \c retro_microphone_params_t + * receive new fields. + * + * Frontends using cores built against older mic interface versions + * should not access fields introduced in newer versions. + */ +#define RETRO_MICROPHONE_INTERFACE_VERSION 1 + +/** + * An interface for querying the microphone and accessing data read from it. + * + * @see RETRO_ENVIRONMENT_GET_MICROPHONE_INTERFACE + */ +struct retro_microphone_interface +{ + /** + * The version of this microphone interface. + * Set by the core to request a particular version, + * and set by the frontend to indicate the returned version. + * 0 indicates that the interface is invalid or uninitialized. + */ + unsigned interface_version; + + /** + * Initializes a new microphone. + * Assuming that microphone support is enabled and provided by the frontend, + * cores may call this function whenever necessary. + * A microphone could be opened throughout a core's lifetime, + * or it could wait until a microphone is plugged in to the emulated device. + * + * The returned handle will be valid until it's freed, + * even if the audio driver is reinitialized. + * + * This function is not guaranteed to be thread-safe. + * + * @param args[in] Parameters used to create the microphone. + * May be \c NULL, in which case the default value of each parameter will be used. + * + * @returns Pointer to the newly-opened microphone, + * or \c NULL if one couldn't be opened. + * This likely means that no microphone is plugged in and recognized, + * or the maximum number of supported microphones has been reached. + * + * @note Microphones are \em inactive by default; + * to begin capturing audio, call \c set_mic_state. + * @see retro_microphone_params_t + */ + retro_open_mic_t open_mic; + + /** + * Closes a microphone that was initialized with \c open_mic. + * Calling this function will stop all microphone activity + * and free up the resources that it allocated. + * Afterwards, the handle is invalid and must not be used. + * + * A frontend may close opened microphones when unloading content, + * but this behavior is not guaranteed. + * Cores should close their microphones when exiting, just to be safe. + * + * @param microphone Pointer to the microphone that was allocated by \c open_mic. + * If \c NULL, this function does nothing. + * + * @note The handle might be reused if another microphone is opened later. + */ + retro_close_mic_t close_mic; + + /** + * Returns the configured parameters of this microphone. + * These may differ from what was requested depending on + * the driver and device configuration. + * + * Cores should check these values before they start fetching samples. + * + * Will not change after the mic was opened. + * + * @param microphone[in] Opaque handle to the microphone + * whose parameters will be retrieved. + * @param params[out] The parameters object that the + * microphone's parameters will be copied to. + * + * @return \c true if the parameters were retrieved, + * \c false if there was an error. + */ + retro_get_mic_params_t get_params; + + /** + * Enables or disables the given microphone. + * Microphones are disabled by default + * and must be explicitly enabled before they can be used. + * Disabled microphones will not process incoming audio samples, + * and will therefore have minimal impact on overall performance. + * Cores may enable microphones throughout their lifetime, + * or only for periods where they're needed. + * + * Cores that accept microphone input should be able to operate without it; + * we suggest substituting silence in this case. + * + * @param microphone Opaque handle to the microphone + * whose state will be adjusted. + * This will have been provided by \c open_mic. + * @param state \c true if the microphone should receive audio input, + * \c false if it should be idle. + * @returns \c true if the microphone's state was successfully set, + * \c false if \c microphone is invalid + * or if there was an error. + */ + retro_set_mic_state_t set_mic_state; + + /** + * Queries the active state of a microphone at the given index. + * Will return whether the microphone is enabled, + * even if the driver is paused. + * + * @param microphone Opaque handle to the microphone + * whose state will be queried. + * @return \c true if the provided \c microphone is valid and active, + * \c false if not or if there was an error. + */ + retro_get_mic_state_t get_mic_state; + + /** + * Retrieves the input processed by the microphone since the last call. + * \em Must be called every frame unless \c microphone is disabled, + * similar to how \c retro_audio_sample_batch_t works. + * + * @param[in] microphone Opaque handle to the microphone + * whose recent input will be retrieved. + * @param[out] samples The buffer that will be used to store the microphone's data. + * Microphone input is in mono (i.e. one number per sample). + * Should be large enough to accommodate the expected number of samples per frame; + * for example, a 44.1kHz sample rate at 60 FPS would require space for 735 samples. + * @param[in] num_samples The size of the data buffer in samples (\em not bytes). + * Microphone input is in mono, so a "frame" and a "sample" are equivalent in length here. + * + * @return The number of samples that were copied into \c samples. + * If \c microphone is pending driver initialization, + * this function will copy silence of the requested length into \c samples. + * + * Will return -1 if the microphone is disabled, + * the audio driver is paused, + * or there was an error. + */ + retro_read_mic_t read_mic; +}; + +/** + * Describes how a device is being powered. + * @see RETRO_ENVIRONMENT_GET_DEVICE_POWER + */ +enum retro_power_state +{ + /** + * Indicates that the frontend cannot report its power state at this time, + * most likely due to a lack of support. + * + * \c RETRO_ENVIRONMENT_GET_DEVICE_POWER will not return this value; + * instead, the environment callback will return \c false. + */ + RETRO_POWERSTATE_UNKNOWN = 0, + + /** + * Indicates that the device is running on its battery. + * Usually applies to portable devices such as handhelds, laptops, and smartphones. + */ + RETRO_POWERSTATE_DISCHARGING, + + /** + * Indicates that the device's battery is currently charging. + */ + RETRO_POWERSTATE_CHARGING, + + /** + * Indicates that the device is connected to a power source + * and that its battery has finished charging. + */ + RETRO_POWERSTATE_CHARGED, + + /** + * Indicates that the device is connected to a power source + * and that it does not have a battery. + * This usually suggests a desktop computer or a non-portable game console. + */ + RETRO_POWERSTATE_PLUGGED_IN +}; + +/** + * Indicates that an estimate is not available for the battery level or time remaining, + * even if the actual power state is known. + */ +#define RETRO_POWERSTATE_NO_ESTIMATE (-1) + +/** + * Describes the power state of the device running the frontend. + * @see RETRO_ENVIRONMENT_GET_DEVICE_POWER + */ +struct retro_device_power +{ + /** + * The current state of the frontend's power usage. + */ + enum retro_power_state state; + + /** + * A rough estimate of the amount of time remaining (in seconds) + * before the device powers off. + * This value depends on a variety of factors, + * so it is not guaranteed to be accurate. + * + * Will be set to \c RETRO_POWERSTATE_NO_ESTIMATE if \c state does not equal \c RETRO_POWERSTATE_DISCHARGING. + * May still be set to \c RETRO_POWERSTATE_NO_ESTIMATE if the frontend is unable to provide an estimate. + */ + int seconds; + + /** + * The approximate percentage of battery charge, + * ranging from 0 to 100 (inclusive). + * The device may power off before this reaches 0. + * + * The user might have configured their device + * to stop charging before the battery is full, + * so do not assume that this will be 100 in the \c RETRO_POWERSTATE_CHARGED state. + */ + int8_t percent; +}; + +/* Callbacks */ + +/* Environment callback. Gives implementations a way of performing + * uncommon tasks. Extensible. */ +typedef bool (RETRO_CALLCONV *retro_environment_t)(unsigned cmd, void *data); + +/* Render a frame. Pixel format is 15-bit 0RGB1555 native endian + * unless changed (see RETRO_ENVIRONMENT_SET_PIXEL_FORMAT). + * + * Width and height specify dimensions of buffer. + * Pitch specifices length in bytes between two lines in buffer. + * + * For performance reasons, it is highly recommended to have a frame + * that is packed in memory, i.e. pitch == width * byte_per_pixel. + * Certain graphic APIs, such as OpenGL ES, do not like textures + * that are not packed in memory. + */ +typedef void (RETRO_CALLCONV *retro_video_refresh_t)(const void *data, unsigned width, + unsigned height, size_t pitch); + +/* Renders a single audio frame. Should only be used if implementation + * generates a single sample at a time. + * Format is signed 16-bit native endian. + */ +typedef void (RETRO_CALLCONV *retro_audio_sample_t)(int16_t left, int16_t right); + +/* Renders multiple audio frames in one go. + * + * One frame is defined as a sample of left and right channels, interleaved. + * I.e. int16_t buf[4] = { l, r, l, r }; would be 2 frames. + * Only one of the audio callbacks must ever be used. + */ +typedef size_t (RETRO_CALLCONV *retro_audio_sample_batch_t)(const int16_t *data, + size_t frames); + +/* Polls input. */ +typedef void (RETRO_CALLCONV *retro_input_poll_t)(void); + +/* Queries for input for player 'port'. device will be masked with + * RETRO_DEVICE_MASK. + * + * Specialization of devices such as RETRO_DEVICE_JOYPAD_MULTITAP that + * have been set with retro_set_controller_port_device() + * will still use the higher level RETRO_DEVICE_JOYPAD to request input. + */ +typedef int16_t (RETRO_CALLCONV *retro_input_state_t)(unsigned port, unsigned device, + unsigned index, unsigned id); + +/* Sets callbacks. retro_set_environment() is guaranteed to be called + * before retro_init(). + * + * The rest of the set_* functions are guaranteed to have been called + * before the first call to retro_run() is made. */ +RETRO_API void retro_set_environment(retro_environment_t); +RETRO_API void retro_set_video_refresh(retro_video_refresh_t); +RETRO_API void retro_set_audio_sample(retro_audio_sample_t); +RETRO_API void retro_set_audio_sample_batch(retro_audio_sample_batch_t); +RETRO_API void retro_set_input_poll(retro_input_poll_t); +RETRO_API void retro_set_input_state(retro_input_state_t); + +/* Library global initialization/deinitialization. */ +RETRO_API void retro_init(void); +RETRO_API void retro_deinit(void); + +/* Must return RETRO_API_VERSION. Used to validate ABI compatibility + * when the API is revised. */ +RETRO_API unsigned retro_api_version(void); + +/* Gets statically known system info. Pointers provided in *info + * must be statically allocated. + * Can be called at any time, even before retro_init(). */ +RETRO_API void retro_get_system_info(struct retro_system_info *info); + +/* Gets information about system audio/video timings and geometry. + * Can be called only after retro_load_game() has successfully completed. + * NOTE: The implementation of this function might not initialize every + * variable if needed. + * E.g. geom.aspect_ratio might not be initialized if core doesn't + * desire a particular aspect ratio. */ +RETRO_API void retro_get_system_av_info(struct retro_system_av_info *info); + +/* Sets device to be used for player 'port'. + * By default, RETRO_DEVICE_JOYPAD is assumed to be plugged into all + * available ports. + * Setting a particular device type is not a guarantee that libretro cores + * will only poll input based on that particular device type. It is only a + * hint to the libretro core when a core cannot automatically detect the + * appropriate input device type on its own. It is also relevant when a + * core can change its behavior depending on device type. + * + * As part of the core's implementation of retro_set_controller_port_device, + * the core should call RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS to notify the + * frontend if the descriptions for any controls have changed as a + * result of changing the device type. + */ +RETRO_API void retro_set_controller_port_device(unsigned port, unsigned device); + +/* Resets the current game. */ +RETRO_API void retro_reset(void); + +/* Runs the game for one video frame. + * During retro_run(), input_poll callback must be called at least once. + * + * If a frame is not rendered for reasons where a game "dropped" a frame, + * this still counts as a frame, and retro_run() should explicitly dupe + * a frame if GET_CAN_DUPE returns true. + * In this case, the video callback can take a NULL argument for data. + */ +RETRO_API void retro_run(void); + +/* Returns the amount of data the implementation requires to serialize + * internal state (save states). + * Between calls to retro_load_game() and retro_unload_game(), the + * returned size is never allowed to be larger than a previous returned + * value, to ensure that the frontend can allocate a save state buffer once. + */ +RETRO_API size_t retro_serialize_size(void); + +/* Serializes internal state. If failed, or size is lower than + * retro_serialize_size(), it should return false, true otherwise. */ +RETRO_API bool retro_serialize(void *data, size_t size); +RETRO_API bool retro_unserialize(const void *data, size_t size); + +RETRO_API void retro_cheat_reset(void); +RETRO_API void retro_cheat_set(unsigned index, bool enabled, const char *code); + +/* Loads a game. + * Return true to indicate successful loading and false to indicate load failure. + */ +RETRO_API bool retro_load_game(const struct retro_game_info *game); + +/* Loads a "special" kind of game. Should not be used, + * except in extreme cases. */ +RETRO_API bool retro_load_game_special( + unsigned game_type, + const struct retro_game_info *info, size_t num_info +); + +/* Unloads the currently loaded game. Called before retro_deinit(void). */ +RETRO_API void retro_unload_game(void); + +/* Gets region of game. */ +RETRO_API unsigned retro_get_region(void); + +/* Gets region of memory. */ +RETRO_API void *retro_get_memory_data(unsigned id); +RETRO_API size_t retro_get_memory_size(unsigned id); + +#ifdef __cplusplus +} +#endif + +#endif From dc629e1b3f2ac7704fe7966d58fc6fa1ec679449 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 2 Jul 2024 18:57:46 +0300 Subject: [PATCH 040/251] Create panda3ds_libretro.info --- docs/libretro/panda3ds_libretro.info | 34 ++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 docs/libretro/panda3ds_libretro.info diff --git a/docs/libretro/panda3ds_libretro.info b/docs/libretro/panda3ds_libretro.info new file mode 100644 index 00000000..40df7e22 --- /dev/null +++ b/docs/libretro/panda3ds_libretro.info @@ -0,0 +1,34 @@ +# Software Information +display_name = "Nintendo - 3DS (Panda3DS)" +authors = "Panda3DS Authors (tm)" +supported_extensions = "3ds|3dsx|elf|axf|cci|cxi|app" +corename = "Panda3DS" +categories = "Emulator" +license = "GPLv3" +permissions = "" +display_version = "Git" + +# Hardware Information +manufacturer = "Nintendo" +systemname = "3DS" +systemid = "3ds" + +# Libretro Information +database = "Nintendo - Nintendo 3DS" +supports_no_game = "false" +savestate = "true" +savestate_features = "basic" +cheats = "false" +input_descriptors = "true" +memory_descriptors = "false" +libretro_saves = "true" +core_options = "true" +core_options_version = "1.0" +load_subsystem = "false" +hw_render = "true" +required_hw_api = "OpenGL Core >= 4.1" +needs_fullpath = "true" +disk_control = "false" +is_experimental = "true" + +description = "Panda3DS !" From 173bd03a53a58cefe62ce33b7366ac0af24139a9 Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Tue, 2 Jul 2024 19:07:30 +0300 Subject: [PATCH 041/251] Libretro: Fix lib output name --- CMakeLists.txt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2897560b..92a939fa 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -510,8 +510,13 @@ elseif(BUILD_HYDRA_CORE) target_link_libraries(Alber PUBLIC AlberCore) elseif(BUILD_LIBRETRO_CORE) include_directories(third_party/libretro/include) - add_library(panda3ds_libretro SHARED src/libretro_core.cpp) - target_link_libraries(panda3ds_libretro PUBLIC AlberCore) + add_library(Alber SHARED src/libretro_core.cpp) + target_link_libraries(Alber PUBLIC AlberCore) + + set_target_properties(Alber PROPERTIES + OUTPUT_NAME "panda3ds_libretro" + PREFIX "" + ) endif() if(ENABLE_LTO OR ENABLE_USER_BUILD) From 0a49dc0af70f073c9d15c2e1f473af430f89a8e3 Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Tue, 9 Jul 2024 14:47:44 +0300 Subject: [PATCH 042/251] Libretro: Various fixes and optimizations --- src/libretro_core.cpp | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index ff57f0c8..8cb66c83 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -17,17 +17,17 @@ static struct retro_hw_render_callback hw_render; std::unique_ptr emulator; RendererGL* renderer; -static void* GetProcAddress(const char* name) { +static void* GetRenderProcAddress(const char* name) { return (void*)hw_render.get_proc_address(name); } static void VideoResetContext(void) { #ifdef USING_GLES - if (!gladLoadGLES2Loader(reinterpret_cast(GetProcAddress))) { + if (!gladLoadGLES2Loader(reinterpret_cast(GetRenderProcAddress))) { Helpers::panic("OpenGL ES init failed"); } #else - if (!gladLoadGLLoader(reinterpret_cast(GetProcAddress))) { + if (!gladLoadGLLoader(reinterpret_cast(GetRenderProcAddress))) { Helpers::panic("OpenGL init failed"); } #endif @@ -47,8 +47,8 @@ static bool SetHWRender(retro_hw_context_type type) { switch (type) { case RETRO_HW_CONTEXT_OPENGL_CORE: - hw_render.version_major = 3; - hw_render.version_minor = 3; + hw_render.version_major = 4; + hw_render.version_minor = 1; if (environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render)) { return true; @@ -57,7 +57,7 @@ static bool SetHWRender(retro_hw_context_type type) { case RETRO_HW_CONTEXT_OPENGLES3: case RETRO_HW_CONTEXT_OPENGL: hw_render.version_major = 3; - hw_render.version_minor = 0; + hw_render.version_minor = 1; if (environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render)) { return true; @@ -173,6 +173,16 @@ static void ConfigUpdate() { config.sdCardInserted = FetchVariableBool("panda3ds_use_virtual_sd", true); config.sdWriteProtected = FetchVariableBool("panda3ds_write_protect_virtual_sd", false); config.discordRpcEnabled = false; + + config.save(); +} + +static void ConfigCheckVariables() { + bool updated = false; + environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated); + + if (updated) + ConfigUpdate(); } void retro_get_system_info(retro_system_info* info) { @@ -192,7 +202,7 @@ void retro_get_system_av_info(retro_system_av_info* info) { info->geometry.aspect_ratio = 5.0 / 6.0; info->timing.fps = 60.0; - info->timing.sample_rate = 32000; + info->timing.sample_rate = 32768; } void retro_set_environment(retro_environment_t cb) { @@ -260,6 +270,8 @@ void retro_reset(void) { } void retro_run(void) { + ConfigCheckVariables(); + renderer->setFBO(hw_render.get_current_framebuffer()); renderer->resetStateManager(); @@ -283,8 +295,8 @@ void retro_run(void) { float x_left = GetAxisState(RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X); float y_left = GetAxisState(RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y); - hid.setCirclepadX(x_left == 0 ? 0 : x_left < 0 ? -0x9C : 0x9C); - hid.setCirclepadY(y_left == 0 ? 0 : y_left > 0 ? -0x9C : 0x9C); + hid.setCirclepadX((x_left / +32767) * 0x9C); + hid.setCirclepadY((y_left / -32767) * 0x9C); bool touch = input_state_cb(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT); auto pos_x = input_state_cb(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X); From ea03d135dab99da9d77aaa64c5a3dc41a6a034a9 Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Tue, 9 Jul 2024 14:48:22 +0300 Subject: [PATCH 043/251] Allow overriding config/data paths in emulator --- include/emulator.hpp | 7 ++++--- src/emulator.cpp | 5 ++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/include/emulator.hpp b/include/emulator.hpp index de04648e..66aeb27e 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -87,6 +87,7 @@ class Emulator { bool frameDone = false; Emulator(); + Emulator(const std::filesystem::path& configPath); ~Emulator(); void step(); @@ -129,10 +130,10 @@ class Emulator { Renderer* getRenderer() { return gpu.getRenderer(); } u64 getTicks() { return cpu.getTicks(); } - std::filesystem::path getConfigPath(); - std::filesystem::path getAndroidAppPath(); + virtual std::filesystem::path getConfigPath(); + virtual std::filesystem::path getAndroidAppPath(); // Get the root path for the emulator's app data - std::filesystem::path getAppDataRoot(); + virtual std::filesystem::path getAppDataRoot(); std::span getSMDH(); }; diff --git a/src/emulator.cpp b/src/emulator.cpp index af156eeb..a7d859be 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -17,7 +17,10 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1; #endif Emulator::Emulator() - : config(getConfigPath()), kernel(cpu, memory, gpu, config), cpu(memory, kernel, *this), gpu(memory, config), memory(cpu.getTicksRef(), config), + : Emulator(getConfigPath()) {} + +Emulator::Emulator(const std::filesystem::path& configPath) + : config(configPath), kernel(cpu, memory, gpu, config), cpu(memory, kernel, *this), gpu(memory, config), memory(cpu.getTicksRef(), config), cheats(memory, kernel.getServiceManager().getHID()), lua(*this), running(false) #ifdef PANDA3DS_ENABLE_HTTP_SERVER , From c7e22c540d572687e5d85effceba482caf0809c6 Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Tue, 9 Jul 2024 14:49:44 +0300 Subject: [PATCH 044/251] Libretro: Use libretro save dir for emulator files --- src/libretro_core.cpp | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index 8cb66c83..10934233 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -13,10 +13,26 @@ static retro_input_poll_t input_poll_cb; static retro_input_state_t input_state_cb; static struct retro_hw_render_callback hw_render; +static std::filesystem::path retro_save_dir; -std::unique_ptr emulator; +class EmulatorCore : public Emulator { + public: + EmulatorCore() : Emulator(getConfigPath()) {} + std::filesystem::path getConfigPath() override; + std::filesystem::path getAppDataRoot() override; +}; + +std::unique_ptr emulator; RendererGL* renderer; +std::filesystem::path EmulatorCore::getConfigPath() { + return std::filesystem::path(retro_save_dir / "config.toml"); +} + +std::filesystem::path EmulatorCore::getAppDataRoot() { + return std::filesystem::path(retro_save_dir / "Emulator Files"); +} + static void* GetRenderProcAddress(const char* name) { return (void*)hw_render.get_proc_address(name); } @@ -232,7 +248,16 @@ void retro_init(void) { enum retro_pixel_format xrgb888 = RETRO_PIXEL_FORMAT_XRGB8888; environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &xrgb888); - emulator = std::make_unique(); + char* save_dir = nullptr; + + if (!environ_cb(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &save_dir) || save_dir == nullptr) { + Helpers::warn("No save directory provided by LibRetro."); + retro_save_dir = std::filesystem::current_path(); + } else { + retro_save_dir = std::filesystem::path(save_dir); + } + + emulator = std::make_unique(); } void retro_deinit(void) { From 623a9a64d6b2f313d015c40cc70511237c07fa43 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:11:25 +0300 Subject: [PATCH 045/251] LR: Format/Cleanup --- include/emulator.hpp | 7 +- src/emulator.cpp | 9 +- src/libretro_core.cpp | 534 ++++++++++++++++++++---------------------- 3 files changed, 260 insertions(+), 290 deletions(-) diff --git a/include/emulator.hpp b/include/emulator.hpp index 66aeb27e..de04648e 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -87,7 +87,6 @@ class Emulator { bool frameDone = false; Emulator(); - Emulator(const std::filesystem::path& configPath); ~Emulator(); void step(); @@ -130,10 +129,10 @@ class Emulator { Renderer* getRenderer() { return gpu.getRenderer(); } u64 getTicks() { return cpu.getTicks(); } - virtual std::filesystem::path getConfigPath(); - virtual std::filesystem::path getAndroidAppPath(); + std::filesystem::path getConfigPath(); + std::filesystem::path getAndroidAppPath(); // Get the root path for the emulator's app data - virtual std::filesystem::path getAppDataRoot(); + std::filesystem::path getAppDataRoot(); std::span getSMDH(); }; diff --git a/src/emulator.cpp b/src/emulator.cpp index a7d859be..db6c2e1f 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -17,10 +17,7 @@ __declspec(dllexport) DWORD AmdPowerXpressRequestHighPerformance = 1; #endif Emulator::Emulator() - : Emulator(getConfigPath()) {} - -Emulator::Emulator(const std::filesystem::path& configPath) - : config(configPath), kernel(cpu, memory, gpu, config), cpu(memory, kernel, *this), gpu(memory, config), memory(cpu.getTicksRef(), config), + : config(getConfigPath()), kernel(cpu, memory, gpu, config), cpu(memory, kernel, *this), gpu(memory, config), memory(cpu.getTicksRef(), config), cheats(memory, kernel.getServiceManager().getHID()), lua(*this), running(false) #ifdef PANDA3DS_ENABLE_HTTP_SERVER , @@ -87,6 +84,7 @@ void Emulator::reset(ReloadOption reload) { } } +#ifndef __LIBRETRO__ std::filesystem::path Emulator::getAndroidAppPath() { // SDL_GetPrefPath fails to get the path due to no JNI environment std::ifstream cmdline("/proc/self/cmdline"); @@ -103,6 +101,7 @@ std::filesystem::path Emulator::getConfigPath() { return std::filesystem::current_path() / "config.toml"; } } +#endif void Emulator::step() {} void Emulator::render() {} @@ -182,6 +181,7 @@ void Emulator::pollScheduler() { } } +#ifndef __LIBRETRO__ // Get path for saving files (AppData on Windows, /home/user/.local/share/ApplicationName on Linux, etc) // Inside that path, we be use a game-specific folder as well. Eg if we were loading a ROM called PenguinDemo.3ds, the savedata would be in // %APPDATA%/Alber/PenguinDemo/SaveData on Windows, and so on. We do this because games save data in their own filesystem on the cart. @@ -205,6 +205,7 @@ std::filesystem::path Emulator::getAppDataRoot() { return appDataPath; } +#endif bool Emulator::loadROM(const std::filesystem::path& path) { // Reset the emulator if we've already loaded a ROM diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index 10934233..c329b881 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -6,391 +6,361 @@ #include #include -static retro_environment_t environ_cb; -static retro_video_refresh_t video_cb; -static retro_audio_sample_batch_t audio_batch_cb; -static retro_input_poll_t input_poll_cb; -static retro_input_state_t input_state_cb; +static retro_environment_t envCallbacks; +static retro_video_refresh_t videoCallbacks; +static retro_audio_sample_batch_t audioBatchCallback; +static retro_input_poll_t inputPollCallback; +static retro_input_state_t inputStateCallback; -static struct retro_hw_render_callback hw_render; -static std::filesystem::path retro_save_dir; +static retro_hw_render_callback hw_render; +static std::filesystem::path savePath; -class EmulatorCore : public Emulator { - public: - EmulatorCore() : Emulator(getConfigPath()) {} - std::filesystem::path getConfigPath() override; - std::filesystem::path getAppDataRoot() override; -}; - -std::unique_ptr emulator; +std::unique_ptr emulator; RendererGL* renderer; -std::filesystem::path EmulatorCore::getConfigPath() { - return std::filesystem::path(retro_save_dir / "config.toml"); +std::filesystem::path Emulator::getConfigPath() { + return std::filesystem::path(savePath / "config.toml"); } -std::filesystem::path EmulatorCore::getAppDataRoot() { - return std::filesystem::path(retro_save_dir / "Emulator Files"); +std::filesystem::path Emulator::getAppDataRoot() { + return std::filesystem::path(savePath / "Emulator Files"); } -static void* GetRenderProcAddress(const char* name) { - return (void*)hw_render.get_proc_address(name); +static void* GetGLProcAddress(const char* name) { + return (void*)hw_render.get_proc_address(name); } -static void VideoResetContext(void) { +static void VideoResetContext() { #ifdef USING_GLES - if (!gladLoadGLES2Loader(reinterpret_cast(GetRenderProcAddress))) { - Helpers::panic("OpenGL ES init failed"); - } + if (!gladLoadGLES2Loader(reinterpret_cast(GetGLProcAddress))) { + Helpers::panic("OpenGL ES init failed"); + } #else - if (!gladLoadGLLoader(reinterpret_cast(GetRenderProcAddress))) { - Helpers::panic("OpenGL init failed"); - } + if (!gladLoadGLLoader(reinterpret_cast(GetGLProcAddress))) { + Helpers::panic("OpenGL init failed"); + } #endif - emulator->initGraphicsContext(nullptr); + emulator->initGraphicsContext(nullptr); } -static void VideoDestroyContext(void) { +static void VideoDestroyContext() { emulator->deinitGraphicsContext(); } static bool SetHWRender(retro_hw_context_type type) { - hw_render.context_type = type; - hw_render.context_reset = VideoResetContext; - hw_render.context_destroy = VideoDestroyContext; - hw_render.bottom_left_origin = true; + hw_render.context_type = type; + hw_render.context_reset = VideoResetContext; + hw_render.context_destroy = VideoDestroyContext; + hw_render.bottom_left_origin = true; - switch (type) { - case RETRO_HW_CONTEXT_OPENGL_CORE: - hw_render.version_major = 4; - hw_render.version_minor = 1; + switch (type) { + case RETRO_HW_CONTEXT_OPENGL_CORE: + hw_render.version_major = 4; + hw_render.version_minor = 1; - if (environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render)) { - return true; - } - break; - case RETRO_HW_CONTEXT_OPENGLES3: - case RETRO_HW_CONTEXT_OPENGL: - hw_render.version_major = 3; - hw_render.version_minor = 1; + if (envCallbacks(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render)) { + return true; + } + break; + case RETRO_HW_CONTEXT_OPENGLES3: + case RETRO_HW_CONTEXT_OPENGL: + hw_render.version_major = 3; + hw_render.version_minor = 1; - if (environ_cb(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render)) { - return true; - } - break; - default: - break; - } + if (envCallbacks(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render)) { + return true; + } + break; + default: break; + } - return false; + return false; } -static void VideoInit(void) { - retro_hw_context_type preferred = RETRO_HW_CONTEXT_NONE; - environ_cb(RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER, &preferred); +static void videoInit() { + retro_hw_context_type preferred = RETRO_HW_CONTEXT_NONE; + envCallbacks(RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER, &preferred); - if (preferred && SetHWRender(preferred)) - return; - if (SetHWRender(RETRO_HW_CONTEXT_OPENGL_CORE)) - return; - if (SetHWRender(RETRO_HW_CONTEXT_OPENGL)) - return; - if (SetHWRender(RETRO_HW_CONTEXT_OPENGLES3)) - return; + if (preferred && SetHWRender(preferred)) return; + if (SetHWRender(RETRO_HW_CONTEXT_OPENGL_CORE)) return; + if (SetHWRender(RETRO_HW_CONTEXT_OPENGL)) return; + if (SetHWRender(RETRO_HW_CONTEXT_OPENGLES3)) return; - hw_render.context_type = RETRO_HW_CONTEXT_NONE; + hw_render.context_type = RETRO_HW_CONTEXT_NONE; } -static bool GetButtonState(unsigned id) { - return input_state_cb(0, RETRO_DEVICE_JOYPAD, 0, id); -} +static bool GetButtonState(uint id) { return inputStateCallback(0, RETRO_DEVICE_JOYPAD, 0, id); } +static float GetAxisState(uint index, uint id) { return inputStateCallback(0, RETRO_DEVICE_ANALOG, index, id); } -static float GetAxisState(unsigned index, unsigned id) { - return input_state_cb(0, RETRO_DEVICE_ANALOG, index, id); -} +static void inputInit() { + static const retro_controller_description controllers[] = { + {"Nintendo 3DS", RETRO_DEVICE_JOYPAD}, + {NULL, 0}, + }; -static void InputInit(void) { - static const struct retro_controller_description controllers[] = { - { "Nintendo 3DS", RETRO_DEVICE_JOYPAD }, - { NULL, 0 }, - }; + static const retro_controller_info ports[] = { + {controllers, 1}, + {NULL, 0}, + }; - static const struct retro_controller_info ports[] = { - { controllers, 1 }, - { NULL, 0 }, - }; + envCallbacks(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports); - environ_cb(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports); + retro_input_descriptor desc[] = { + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R, "R"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L, "L"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X, "X"}, + {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y, "Y"}, + {0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X, "Circle Pad X"}, + {0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y, "Circle Pad Y"}, + {0}, + }; - struct retro_input_descriptor desc[] = { - { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left" }, - { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_UP, "Up" }, - { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_DOWN, "Down" }, - { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_RIGHT, "Right" }, - { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_A, "A" }, - { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_B, "B" }, - { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_SELECT, "Select" }, - { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_START, "Start" }, - { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_R, "R" }, - { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_L, "L" }, - { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_X, "X" }, - { 0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_Y, "Y" }, - { 0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X, "Circle Pad X" }, - { 0, RETRO_DEVICE_ANALOG, RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y, "Circle Pad Y" }, - { 0 }, - }; - - environ_cb(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, &desc); + envCallbacks(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, &desc); } static std::string FetchVariable(std::string key, std::string def) { - struct retro_variable var = { nullptr }; - var.key = key.c_str(); + retro_variable var = {nullptr}; + var.key = key.c_str(); - if (!environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE, &var) || var.value == nullptr) { - Helpers::warn("Fetching variable %s failed.", key); - return def; - } + if (!envCallbacks(RETRO_ENVIRONMENT_GET_VARIABLE, &var) || var.value == nullptr) { + Helpers::warn("Fetching variable %s failed.", key); + return def; + } - return std::string(var.value); + return std::string(var.value); } static bool FetchVariableBool(std::string key, bool def) { - return FetchVariable(key, def ? "enabled" : "disabled") == "enabled"; + return FetchVariable(key, def ? "enabled" : "disabled") == "enabled"; } -static void ConfigInit() { - static const retro_variable values[] = { - { "panda3ds_use_shader_jit", "Enable shader JIT; enabled|disabled" }, - { "panda3ds_use_vsync", "Enable VSync; enabled|disabled" }, - { "panda3ds_dsp_emulation", "DSP emulation; Null|HLE|LLE" }, - { "panda3ds_use_audio", "Enable audio; disabled|enabled" }, - { "panda3ds_use_virtual_sd", "Enable virtual SD card; enabled|disabled" }, - { "panda3ds_write_protect_virtual_sd", "Write protect virtual SD card; disabled|enabled" }, - { "panda3ds_battery_level", "Battery percentage; 5|10|20|30|50|70|90|100" }, - { "panda3ds_use_charger", "Charger plugged; enabled|disabled" }, - { nullptr, nullptr } - }; +static void configInit() { + static const retro_variable values[] = { + {"panda3ds_use_shader_jit", "Enable shader JIT; enabled|disabled"}, + {"panda3ds_use_vsync", "Enable VSync; enabled|disabled"}, + {"panda3ds_dsp_emulation", "DSP emulation; Null|HLE|LLE"}, + {"panda3ds_use_audio", "Enable audio; disabled|enabled"}, + {"panda3ds_use_virtual_sd", "Enable virtual SD card; enabled|disabled"}, + {"panda3ds_write_protect_virtual_sd", "Write protect virtual SD card; disabled|enabled"}, + {"panda3ds_battery_level", "Battery percentage; 5|10|20|30|50|70|90|100"}, + {"panda3ds_use_charger", "Charger plugged; enabled|disabled"}, + {nullptr, nullptr} + }; - environ_cb(RETRO_ENVIRONMENT_SET_VARIABLES, (void*)values); + envCallbacks(RETRO_ENVIRONMENT_SET_VARIABLES, (void*)values); } -static void ConfigUpdate() { - EmulatorConfig& config = emulator->getConfig(); +static void configUpdate() { + EmulatorConfig& config = emulator->getConfig(); - config.rendererType = RendererType::OpenGL; - config.vsyncEnabled = FetchVariableBool("panda3ds_use_vsync", true); - config.shaderJitEnabled = FetchVariableBool("panda3ds_use_shader_jit", true); - config.chargerPlugged = FetchVariableBool("panda3ds_use_charger", true); - config.batteryPercentage = std::clamp(std::stoi(FetchVariable("panda3ds_battery_level", "5")), 0, 100); - config.dspType = Audio::DSPCore::typeFromString(FetchVariable("panda3ds_dsp_emulation", "null")); - config.audioEnabled = FetchVariableBool("panda3ds_use_audio", false); - config.sdCardInserted = FetchVariableBool("panda3ds_use_virtual_sd", true); - config.sdWriteProtected = FetchVariableBool("panda3ds_write_protect_virtual_sd", false); - config.discordRpcEnabled = false; + config.rendererType = RendererType::OpenGL; + config.vsyncEnabled = FetchVariableBool("panda3ds_use_vsync", true); + config.shaderJitEnabled = FetchVariableBool("panda3ds_use_shader_jit", true); + config.chargerPlugged = FetchVariableBool("panda3ds_use_charger", true); + config.batteryPercentage = std::clamp(std::stoi(FetchVariable("panda3ds_battery_level", "5")), 0, 100); + config.dspType = Audio::DSPCore::typeFromString(FetchVariable("panda3ds_dsp_emulation", "null")); + config.audioEnabled = FetchVariableBool("panda3ds_use_audio", false); + config.sdCardInserted = FetchVariableBool("panda3ds_use_virtual_sd", true); + config.sdWriteProtected = FetchVariableBool("panda3ds_write_protect_virtual_sd", false); + config.discordRpcEnabled = false; - config.save(); + config.save(); } static void ConfigCheckVariables() { - bool updated = false; - environ_cb(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated); + bool updated = false; + envCallbacks(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated); - if (updated) - ConfigUpdate(); + if (updated) { + configUpdate(); + } } void retro_get_system_info(retro_system_info* info) { - info->need_fullpath = true; - info->valid_extensions = "3ds|3dsx|elf|axf|cci|cxi|app"; - info->library_version = "0.8"; - info->library_name = "Panda3DS"; - info->block_extract = true; + info->need_fullpath = true; + info->valid_extensions = "3ds|3dsx|elf|axf|cci|cxi|app"; + info->library_version = "0.8"; + info->library_name = "Panda3DS"; + info->block_extract = true; } void retro_get_system_av_info(retro_system_av_info* info) { - info->geometry.base_width = emulator->width; - info->geometry.base_height = emulator->height; + info->geometry.base_width = emulator->width; + info->geometry.base_height = emulator->height; - info->geometry.max_width = info->geometry.base_width; - info->geometry.max_height = info->geometry.base_height; + info->geometry.max_width = info->geometry.base_width; + info->geometry.max_height = info->geometry.base_height; - info->geometry.aspect_ratio = 5.0 / 6.0; - info->timing.fps = 60.0; - info->timing.sample_rate = 32768; + info->geometry.aspect_ratio = float(5.0 / 6.0); + info->timing.fps = 60.0; + info->timing.sample_rate = 32768; } void retro_set_environment(retro_environment_t cb) { - environ_cb = cb; + envCallbacks = cb; } void retro_set_video_refresh(retro_video_refresh_t cb) { - video_cb = cb; + videoCallbacks = cb; } void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) { - audio_batch_cb = cb; + audioBatchCallback = cb; } -void retro_set_audio_sample(retro_audio_sample_t cb) { -} +void retro_set_audio_sample(retro_audio_sample_t cb) {} void retro_set_input_poll(retro_input_poll_t cb) { - input_poll_cb = cb; + inputPollCallback = cb; } void retro_set_input_state(retro_input_state_t cb) { - input_state_cb = cb; + inputStateCallback = cb; } -void retro_init(void) { - enum retro_pixel_format xrgb888 = RETRO_PIXEL_FORMAT_XRGB8888; - environ_cb(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &xrgb888); +void retro_init() { + enum retro_pixel_format xrgb888 = RETRO_PIXEL_FORMAT_XRGB8888; + envCallbacks(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &xrgb888); - char* save_dir = nullptr; + char* save_dir = nullptr; - if (!environ_cb(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &save_dir) || save_dir == nullptr) { - Helpers::warn("No save directory provided by LibRetro."); - retro_save_dir = std::filesystem::current_path(); - } else { - retro_save_dir = std::filesystem::path(save_dir); - } + if (!envCallbacks(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &save_dir) || save_dir == nullptr) { + Helpers::warn("No save directory provided by LibRetro."); + savePath = std::filesystem::current_path(); + } else { + savePath = std::filesystem::path(save_dir); + } - emulator = std::make_unique(); + emulator = std::make_unique(); } -void retro_deinit(void) { - emulator = nullptr; +void retro_deinit() { + emulator = nullptr; } -bool retro_load_game(const struct retro_game_info* game) { - ConfigInit(); - ConfigUpdate(); +bool retro_load_game(const retro_game_info* game) { + configInit(); + configUpdate(); - if (emulator->getRendererType() != RendererType::OpenGL) { - throw std::runtime_error("Libretro: Renderer is not OpenGL"); - } + if (emulator->getRendererType() != RendererType::OpenGL) { + Helpers::panic("Libretro: Renderer is not OpenGL"); + } - renderer = static_cast(emulator->getRenderer()); - emulator->setOutputSize(emulator->width, emulator->height); + renderer = static_cast(emulator->getRenderer()); + emulator->setOutputSize(emulator->width, emulator->height); - InputInit(); - VideoInit(); + inputInit(); + videoInit(); - return emulator->loadROM(game->path); + return emulator->loadROM(game->path); } -bool retro_load_game_special(unsigned type, const struct retro_game_info* info, size_t num) { - return false; +bool retro_load_game_special(uint type, const retro_game_info* info, size_t num) { return false; } + +void retro_unload_game() { + renderer->setFBO(0); + renderer = nullptr; } -void retro_unload_game(void) { - renderer->setFBO(0); - renderer = nullptr; +void retro_reset() { + emulator->reset(Emulator::ReloadOption::Reload); } -void retro_reset(void) { - emulator->reset(Emulator::ReloadOption::Reload); +void retro_run() { + ConfigCheckVariables(); + + renderer->setFBO(hw_render.get_current_framebuffer()); + renderer->resetStateManager(); + + inputPollCallback(); + + HIDService& hid = emulator->getServiceManager().getHID(); + + hid.setKey(HID::Keys::A, GetButtonState(RETRO_DEVICE_ID_JOYPAD_A)); + hid.setKey(HID::Keys::B, GetButtonState(RETRO_DEVICE_ID_JOYPAD_B)); + hid.setKey(HID::Keys::X, GetButtonState(RETRO_DEVICE_ID_JOYPAD_X)); + hid.setKey(HID::Keys::Y, GetButtonState(RETRO_DEVICE_ID_JOYPAD_Y)); + hid.setKey(HID::Keys::L, GetButtonState(RETRO_DEVICE_ID_JOYPAD_L)); + hid.setKey(HID::Keys::R, GetButtonState(RETRO_DEVICE_ID_JOYPAD_R)); + hid.setKey(HID::Keys::Start, GetButtonState(RETRO_DEVICE_ID_JOYPAD_START)); + hid.setKey(HID::Keys::Select, GetButtonState(RETRO_DEVICE_ID_JOYPAD_SELECT)); + hid.setKey(HID::Keys::Up, GetButtonState(RETRO_DEVICE_ID_JOYPAD_UP)); + hid.setKey(HID::Keys::Down, GetButtonState(RETRO_DEVICE_ID_JOYPAD_DOWN)); + hid.setKey(HID::Keys::Left, GetButtonState(RETRO_DEVICE_ID_JOYPAD_LEFT)); + hid.setKey(HID::Keys::Right, GetButtonState(RETRO_DEVICE_ID_JOYPAD_RIGHT)); + + // Get analog values for the left analog stick (Right analog stick is N3DS-only and unimplemented) + float xLeft = GetAxisState(RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X); + float yLeft = GetAxisState(RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y); + + hid.setCirclepadX((xLeft / +32767) * 0x9C); + hid.setCirclepadY((yLeft / -32767) * 0x9C); + + bool touch = inputStateCallback(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT); + const int posX = inputStateCallback(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X); + const int posY = inputStateCallback(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y); + + const int newX = static_cast((posX + 0x7fff) / (float)(0x7fff * 2) * emulator->width); + const int newY = static_cast((posY + 0x7fff) / (float)(0x7fff * 2) * emulator->height); + + const int offsetX = 40; + const int offsetY = emulator->height / 2; + + const bool inScreenX = newX >= offsetX && newX < emulator->width - offsetX; + const bool inScreenY = newY >= offsetY && newY <= emulator->height; + + if (touch && inScreenX && inScreenY) { + u16 x = static_cast(newX - offsetX); + u16 y = static_cast(newY - offsetY); + + hid.setTouchScreenPress(x, y); + } else { + hid.releaseTouchScreen(); + } + + hid.updateInputs(emulator->getTicks()); + emulator->runFrame(); + + videoCallbacks(RETRO_HW_FRAME_BUFFER_VALID, emulator->width, emulator->height, 0); } -void retro_run(void) { - ConfigCheckVariables(); +void retro_set_controller_port_device(uint port, uint device) {} - renderer->setFBO(hw_render.get_current_framebuffer()); - renderer->resetStateManager(); - - input_poll_cb(); - - HIDService& hid = emulator->getServiceManager().getHID(); - - hid.setKey(HID::Keys::A, GetButtonState(RETRO_DEVICE_ID_JOYPAD_A)); - hid.setKey(HID::Keys::B, GetButtonState(RETRO_DEVICE_ID_JOYPAD_B)); - hid.setKey(HID::Keys::X, GetButtonState(RETRO_DEVICE_ID_JOYPAD_X)); - hid.setKey(HID::Keys::Y, GetButtonState(RETRO_DEVICE_ID_JOYPAD_Y)); - hid.setKey(HID::Keys::L, GetButtonState(RETRO_DEVICE_ID_JOYPAD_L)); - hid.setKey(HID::Keys::R, GetButtonState(RETRO_DEVICE_ID_JOYPAD_R)); - hid.setKey(HID::Keys::Start, GetButtonState(RETRO_DEVICE_ID_JOYPAD_START)); - hid.setKey(HID::Keys::Select, GetButtonState(RETRO_DEVICE_ID_JOYPAD_SELECT)); - hid.setKey(HID::Keys::Up, GetButtonState(RETRO_DEVICE_ID_JOYPAD_UP)); - hid.setKey(HID::Keys::Down, GetButtonState(RETRO_DEVICE_ID_JOYPAD_DOWN)); - hid.setKey(HID::Keys::Left, GetButtonState(RETRO_DEVICE_ID_JOYPAD_LEFT)); - hid.setKey(HID::Keys::Right, GetButtonState(RETRO_DEVICE_ID_JOYPAD_RIGHT)); - - float x_left = GetAxisState(RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X); - float y_left = GetAxisState(RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y); - - hid.setCirclepadX((x_left / +32767) * 0x9C); - hid.setCirclepadY((y_left / -32767) * 0x9C); - - bool touch = input_state_cb(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT); - auto pos_x = input_state_cb(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X); - auto pos_y = input_state_cb(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y); - - auto new_x = static_cast((pos_x + 0x7fff) / (float)(0x7fff * 2) * emulator->width); - auto new_y = static_cast((pos_y + 0x7fff) / (float)(0x7fff * 2) * emulator->height); - - auto off_x = 40; - auto off_y = emulator->height / 2; - - bool scr_x = new_x >= off_x && new_x < emulator->width - off_x; - bool scr_y = new_y >= off_y && new_y <= emulator->height; - - if (touch && scr_y && scr_x) { - u16 x = static_cast(new_x - off_x); - u16 y = static_cast(new_y - off_y); - - hid.setTouchScreenPress(x, y); - } else { - hid.releaseTouchScreen(); - } - - hid.updateInputs(emulator->getTicks()); - - emulator->runFrame(); - video_cb(RETRO_HW_FRAME_BUFFER_VALID, emulator->width, emulator->height, 0); +size_t retro_serialize_size() { + size_t size = 0; + return size; } -void retro_set_controller_port_device(unsigned port, unsigned device) { +bool retro_serialize(void* data, size_t size) { return false; } +bool retro_unserialize(const void* data, size_t size) { return false; } + +uint retro_get_region() { return RETRO_REGION_NTSC; } +uint retro_api_version() { return RETRO_API_VERSION; } + +size_t retro_get_memory_size(uint id) { + if (id == RETRO_MEMORY_SYSTEM_RAM) { + return 0; + } + + return 0; } -size_t retro_serialize_size(void) { - size_t size = 0; - return size; +void* retro_get_memory_data(uint id) { + if (id == RETRO_MEMORY_SYSTEM_RAM) { + return 0; + } + + return nullptr; } -bool retro_serialize(void* data, size_t size) { - return false; -} - -bool retro_unserialize(const void* data, size_t size) { - return false; -} - -unsigned retro_get_region(void) { - return RETRO_REGION_NTSC; -} - -unsigned retro_api_version() { - return RETRO_API_VERSION; -} - -size_t retro_get_memory_size(unsigned id) { - if (id == RETRO_MEMORY_SYSTEM_RAM) { - return 0; - } - return 0; -} - -void* retro_get_memory_data(unsigned id) { - if (id == RETRO_MEMORY_SYSTEM_RAM) { - return 0; - } - return NULL; -} - -void retro_cheat_set(unsigned index, bool enabled, const char* code) { -} - -void retro_cheat_reset(void) { -} +void retro_cheat_set(uint index, bool enabled, const char* code) {} +void retro_cheat_reset() {} From a12b721c957e1684d44213e176a7e5eb42889567 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 9 Jul 2024 16:52:09 +0300 Subject: [PATCH 046/251] More formatting --- CMakeLists.txt | 2 +- src/libretro_core.cpp | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 92a939fa..85a915e2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,7 +50,7 @@ if(BUILD_LIBRETRO_CORE) set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(ENABLE_DISCORD_RPC OFF) set(ENABLE_LUAJIT OFF) - add_definitions(-D__LIBRETRO__) + add_compile_definitions(__LIBRETRO__) endif() add_library(AlberCore STATIC) diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index c329b881..3bf0f95f 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -264,7 +264,7 @@ bool retro_load_game(const retro_game_info* game) { return emulator->loadROM(game->path); } -bool retro_load_game_special(uint type, const retro_game_info* info, size_t num) { return false; } +bool retro_load_game_special(uint type, const retro_game_info* info, usize num) { return false; } void retro_unload_game() { renderer->setFBO(0); @@ -335,18 +335,18 @@ void retro_run() { void retro_set_controller_port_device(uint port, uint device) {} -size_t retro_serialize_size() { - size_t size = 0; +usize retro_serialize_size() { + usize size = 0; return size; } -bool retro_serialize(void* data, size_t size) { return false; } -bool retro_unserialize(const void* data, size_t size) { return false; } +bool retro_serialize(void* data, usize size) { return false; } +bool retro_unserialize(const void* data, usize size) { return false; } uint retro_get_region() { return RETRO_REGION_NTSC; } uint retro_api_version() { return RETRO_API_VERSION; } -size_t retro_get_memory_size(uint id) { +usize retro_get_memory_size(uint id) { if (id == RETRO_MEMORY_SYSTEM_RAM) { return 0; } From a3886a948fd6d7b54f94f3896e3e7d1ef841b7d3 Mon Sep 17 00:00:00 2001 From: offtkp Date: Tue, 9 Jul 2024 20:51:09 +0300 Subject: [PATCH 047/251] Switch to GL_TEXTURE_2D for lighting LUT --- include/renderer_gl/renderer_gl.hpp | 2 +- src/core/renderer_gl/renderer_gl.cpp | 21 ++++++++++---------- src/host_shaders/opengl_fragment_shader.frag | 8 ++++---- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index 92f02662..057f0d3b 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -53,7 +53,7 @@ class RendererGL final : public Renderer { OpenGL::VertexBuffer dummyVBO; OpenGL::Texture screenTexture; - GLuint lightLUTTextureArray; + OpenGL::Texture lightLUTTexture; OpenGL::Framebuffer screenFramebuffer; OpenGL::Texture blankTexture; diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index a11a6ffa..9de9f8d8 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -124,7 +124,10 @@ void RendererGL::initGraphicsContextInternal() { const u32 screenTextureWidth = 400; // Top screen is 400 pixels wide, bottom is 320 const u32 screenTextureHeight = 2 * 240; // Both screens are 240 pixels tall - glGenTextures(1, &lightLUTTextureArray); + lightLUTTexture.create(256, Lights::LUT_Count, GL_R32F); + lightLUTTexture.bind(); + lightLUTTexture.setMinFilter(OpenGL::Linear); + lightLUTTexture.setMagFilter(OpenGL::Linear); auto prevTexture = OpenGL::getTex2D(); @@ -357,26 +360,22 @@ void RendererGL::bindTexturesToSlots() { } glActiveTexture(GL_TEXTURE0 + 3); - glBindTexture(GL_TEXTURE_1D_ARRAY, lightLUTTextureArray); + lightLUTTexture.bind(); glActiveTexture(GL_TEXTURE0); } void RendererGL::updateLightingLUT() { gpu.lightingLUTDirty = false; - std::array u16_lightinglut; + std::array lightingLut; for (int i = 0; i < gpu.lightingLUT.size(); i++) { - uint64_t value = gpu.lightingLUT[i] & ((1 << 12) - 1); - u16_lightinglut[i] = value * 65535 / 4095; + uint64_t value = gpu.lightingLUT[i] & 0xFFF; + lightingLut[i] = (float)(value << 4) / 65535.0f; } glActiveTexture(GL_TEXTURE0 + 3); - glBindTexture(GL_TEXTURE_1D_ARRAY, lightLUTTextureArray); - glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_R16, 256, Lights::LUT_Count, 0, GL_RED, GL_UNSIGNED_SHORT, u16_lightinglut.data()); - glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + lightLUTTexture.bind(); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 256, Lights::LUT_Count, GL_RED, GL_FLOAT, lightingLut.data()); glActiveTexture(GL_TEXTURE0); } diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index f6fa6c55..6b728ace 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -27,7 +27,7 @@ uniform bool u_depthmapEnable; uniform sampler2D u_tex0; uniform sampler2D u_tex1; uniform sampler2D u_tex2; -uniform sampler1DArray u_tex_lighting_lut; +uniform sampler2D u_tex_lighting_lut; uniform uint u_picaRegs[0x200 - 0x48]; @@ -145,9 +145,9 @@ vec4 tevCalculateCombiner(int tev_id) { #define RR_LUT 6u float lutLookup(uint lut, uint light, float value) { - if (lut >= FR_LUT && lut <= RR_LUT) lut -= 1; - if (lut == SP_LUT) lut = light + 8; - return texture(u_tex_lighting_lut, vec2(value, lut)).r; + if (lut >= FR_LUT && lut <= RR_LUT) lut -= 1u; + if (lut == SP_LUT) lut = light + 8u; + return texelFetch(u_tex_lighting_lut, ivec2(int(value * 256.0), lut), 0).r; } vec3 regToColor(uint reg) { From 6f6167a20125f3259c2de97ffccbcd26947785c3 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 9 Jul 2024 20:56:47 +0300 Subject: [PATCH 048/251] Fix LR variable fetch error --- src/libretro_core.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index 3bf0f95f..f9772b37 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -132,7 +132,7 @@ static std::string FetchVariable(std::string key, std::string def) { var.key = key.c_str(); if (!envCallbacks(RETRO_ENVIRONMENT_GET_VARIABLE, &var) || var.value == nullptr) { - Helpers::warn("Fetching variable %s failed.", key); + Helpers::warn("Fetching variable %s failed.", key.c_str()); return def; } From fe566e960b17471fa7bcbd11f43c1ca22368d25c Mon Sep 17 00:00:00 2001 From: offtkp Date: Tue, 9 Jul 2024 20:57:56 +0300 Subject: [PATCH 049/251] Update GL ES patch to work with latest changes --- .github/gles.patch | 103 +++++++++------------------------------------ 1 file changed, 19 insertions(+), 84 deletions(-) diff --git a/.github/gles.patch b/.github/gles.patch index f1dc2c73..3d6c96fe 100644 --- a/.github/gles.patch +++ b/.github/gles.patch @@ -1,52 +1,3 @@ -diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp -index a11a6ffa..77486a09 100644 ---- a/src/core/renderer_gl/renderer_gl.cpp -+++ b/src/core/renderer_gl/renderer_gl.cpp -@@ -357,27 +357,27 @@ void RendererGL::bindTexturesToSlots() { - } - - glActiveTexture(GL_TEXTURE0 + 3); -- glBindTexture(GL_TEXTURE_1D_ARRAY, lightLUTTextureArray); -+ // glBindTexture(GL_TEXTURE_1D_ARRAY, lightLUTTextureArray); - glActiveTexture(GL_TEXTURE0); - } - - void RendererGL::updateLightingLUT() { -- gpu.lightingLUTDirty = false; -- std::array u16_lightinglut; -- -- for (int i = 0; i < gpu.lightingLUT.size(); i++) { -- uint64_t value = gpu.lightingLUT[i] & ((1 << 12) - 1); -- u16_lightinglut[i] = value * 65535 / 4095; -- } -- -- glActiveTexture(GL_TEXTURE0 + 3); -- glBindTexture(GL_TEXTURE_1D_ARRAY, lightLUTTextureArray); -- glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_R16, 256, Lights::LUT_Count, 0, GL_RED, GL_UNSIGNED_SHORT, u16_lightinglut.data()); -- glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); -- glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); -- glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); -- glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); -- glActiveTexture(GL_TEXTURE0); -+ // gpu.lightingLUTDirty = false; -+ // std::array u16_lightinglut; -+ -+ // for (int i = 0; i < gpu.lightingLUT.size(); i++) { -+ // uint64_t value = gpu.lightingLUT[i] & ((1 << 12) - 1); -+ // u16_lightinglut[i] = value * 65535 / 4095; -+ // } -+ -+ // glActiveTexture(GL_TEXTURE0 + 3); -+ // glBindTexture(GL_TEXTURE_1D_ARRAY, lightLUTTextureArray); -+ // glTexImage2D(GL_TEXTURE_1D_ARRAY, 0, GL_R16, 256, Lights::LUT_Count, 0, GL_RED, GL_UNSIGNED_SHORT, u16_lightinglut.data()); -+ // glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_LINEAR); -+ // glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_LINEAR); -+ // glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); -+ // glTexParameteri(GL_TEXTURE_1D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); -+ // glActiveTexture(GL_TEXTURE0); - } - - void RendererGL::drawVertices(PICA::PrimType primType, std::span vertices) { diff --git a/src/host_shaders/opengl_display.frag b/src/host_shaders/opengl_display.frag index 612671c8..1937f711 100644 --- a/src/host_shaders/opengl_display.frag @@ -70,7 +21,7 @@ index 990e2f80..2e7842ac 100644 void main() { diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag -index f6fa6c55..bb88e278 100644 +index 6b728ace..eaac1484 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -1,4 +1,5 @@ @@ -80,34 +31,16 @@ index f6fa6c55..bb88e278 100644 in vec3 v_tangent; in vec3 v_normal; -@@ -27,7 +28,7 @@ uniform bool u_depthmapEnable; - uniform sampler2D u_tex0; - uniform sampler2D u_tex1; - uniform sampler2D u_tex2; --uniform sampler1DArray u_tex_lighting_lut; -+// uniform sampler1DArray u_tex_lighting_lut; +@@ -150,11 +151,17 @@ float lutLookup(uint lut, uint light, float value) { + return texelFetch(u_tex_lighting_lut, ivec2(int(value * 256.0), lut), 0).r; + } - uniform uint u_picaRegs[0x200 - 0x48]; - -@@ -145,16 +146,23 @@ vec4 tevCalculateCombiner(int tev_id) { - #define RR_LUT 6u - - float lutLookup(uint lut, uint light, float value) { -- if (lut >= FR_LUT && lut <= RR_LUT) lut -= 1; -- if (lut == SP_LUT) lut = light + 8; -- return texture(u_tex_lighting_lut, vec2(value, lut)).r; -+ // if (lut >= FR_LUT && lut <= RR_LUT) lut -= 1; -+ // if (lut == SP_LUT) lut = light + 8; -+ // return texture(u_tex_lighting_lut, vec2(value, lut)).r; -+ return 0.0; -+} -+ +// some gles versions have bitfieldExtract and complain if you redefine it, some don't and compile error, using this instead +uint bitfieldExtractCompat(uint val, int off, int size) { + uint mask = uint((1 << size) - 1); + return uint(val >> off) & mask; - } - ++} ++ vec3 regToColor(uint reg) { // Normalization scale to convert from [0...255] to [0.0...1.0] const float scale = 1.0 / 255.0; @@ -117,7 +50,7 @@ index f6fa6c55..bb88e278 100644 } // Convert an arbitrary-width floating point literal to an f32 -@@ -189,7 +197,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -189,7 +196,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { vec3 view = normalize(v_view); uint GPUREG_LIGHTING_ENABLE = readPicaReg(0x008Fu); @@ -126,7 +59,7 @@ index f6fa6c55..bb88e278 100644 primary_color = secondary_color = vec4(1.0); return; } -@@ -213,7 +221,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -213,7 +220,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { bool error_unimpl = false; for (uint i = 0u; i < GPUREG_LIGHTING_NUM_LIGHTS; i++) { @@ -135,7 +68,7 @@ index f6fa6c55..bb88e278 100644 uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140u + 0x10u * light_id); uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141u + 0x10u * light_id); -@@ -224,14 +232,14 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -224,14 +231,14 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { uint GPUREG_LIGHTi_CONFIG = readPicaReg(0x0149u + 0x10u * light_id); vec3 light_vector = normalize(vec3( @@ -153,7 +86,7 @@ index f6fa6c55..bb88e278 100644 // error_unimpl = true; half_vector = normalize(normalize(light_vector + v_view) + view); } -@@ -242,12 +250,12 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -242,12 +249,12 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { } for (int c = 0; c < 7; c++) { @@ -169,7 +102,7 @@ index f6fa6c55..bb88e278 100644 if (input_id == 0u) d[c] = dot(normal, half_vector); else if (input_id == 1u) -@@ -260,9 +268,9 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -260,9 +267,9 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { uint GPUREG_LIGHTi_SPOTDIR_LOW = readPicaReg(0x0146u + 0x10u * light_id); uint GPUREG_LIGHTi_SPOTDIR_HIGH = readPicaReg(0x0147u + 0x10u * light_id); vec3 spot_light_vector = normalize(vec3( @@ -182,7 +115,7 @@ index f6fa6c55..bb88e278 100644 )); d[c] = dot(-light_vector, spot_light_vector); // -L dot P (aka Spotlight aka SP); } else if (input_id == 5u) { -@@ -273,13 +281,13 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -273,13 +280,13 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { } d[c] = lutLookup(uint(c), light_id, d[c] * 0.5 + 0.5) * scale; @@ -198,7 +131,7 @@ index f6fa6c55..bb88e278 100644 if (lookup_config == 0u) { d[D1_LUT] = 0.0; d[FR_LUT] = 0.0; -@@ -310,7 +318,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -310,7 +317,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { float NdotL = dot(normal, light_vector); // Li dot N // Two sided diffuse @@ -207,7 +140,7 @@ index f6fa6c55..bb88e278 100644 NdotL = max(0.0, NdotL); else NdotL = abs(NdotL); -@@ -321,8 +329,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -321,8 +328,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { secondary_color.rgb += light_factor * (regToColor(GPUREG_LIGHTi_SPECULAR0) * d[D0_LUT] + regToColor(GPUREG_LIGHTi_SPECULAR1) * d[D1_LUT] * vec3(d[RR_LUT], d[RG_LUT], d[RB_LUT])); } @@ -249,14 +182,16 @@ index a25d7a6d..7cf40398 100644 + // gl_ClipDistance[1] = dot(clipData, a_coords); } diff --git a/third_party/opengl/opengl.hpp b/third_party/opengl/opengl.hpp -index f368f573..5ead7f63 100644 +index 9997e63b..5d9d7804 100644 --- a/third_party/opengl/opengl.hpp +++ b/third_party/opengl/opengl.hpp -@@ -520,21 +520,21 @@ namespace OpenGL { +@@ -561,22 +561,22 @@ namespace OpenGL { + static void disableScissor() { glDisable(GL_SCISSOR_TEST); } static void enableBlend() { glEnable(GL_BLEND); } static void disableBlend() { glDisable(GL_BLEND); } - static void enableLogicOp() { glEnable(GL_COLOR_LOGIC_OP); } +- static void enableLogicOp() { glEnable(GL_COLOR_LOGIC_OP); } - static void disableLogicOp() { glDisable(GL_COLOR_LOGIC_OP); } ++ static void enableLogicOp() { /* glEnable(GL_COLOR_LOGIC_OP); */ } + static void disableLogicOp() { /* glDisable(GL_COLOR_LOGIC_OP); */ } static void enableDepth() { glEnable(GL_DEPTH_TEST); } static void disableDepth() { glDisable(GL_DEPTH_TEST); } From a1ff34d41759f95138c03f49bfc3f77aa05b7fa8 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 9 Jul 2024 20:58:07 +0000 Subject: [PATCH 050/251] Add LR core to CI (#530) * Add LR core to CI * Update Hydra_Build.yml * Update Hydra_Build.yml * Update Hydra_Build.yml * Update Hydra_Build.yml * Update Hydra_Build.yml * Update Hydra_Build.yml * Update Hydra_Build.yml * Update Hydra_Build.yml --- .github/workflows/Hydra_Build.yml | 65 ++++++++++++++++++++++++++----- 1 file changed, 56 insertions(+), 9 deletions(-) diff --git a/.github/workflows/Hydra_Build.yml b/.github/workflows/Hydra_Build.yml index a19974fb..645f2f7a 100644 --- a/.github/workflows/Hydra_Build.yml +++ b/.github/workflows/Hydra_Build.yml @@ -32,12 +32,27 @@ jobs: - name: Build run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - - name: Upload core - uses: actions/upload-artifact@v2 + - name: Upload Hydra core + uses: actions/upload-artifact@v4 with: - name: Windows core + name: Windows Hydra core path: '${{github.workspace}}/build/${{ env.BUILD_TYPE }}/Alber.dll' + - name: Configure CMake (Again) + run: | + rm -r -fo ${{github.workspace}}/build + cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_USER_BUILD=ON -DBUILD_LIBRETRO_CORE=ON + + - name: Build (Again) + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + + - name: Upload Libretro core + uses: actions/upload-artifact@v4 + with: + name: Windows Libretro core + path: | + ${{github.workspace}}/build/panda3ds_libretro.dll + ${{github.workspace}}/docs/libretro/panda3ds_libretro.info MacOS: runs-on: macos-13 @@ -61,11 +76,27 @@ jobs: run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - name: Upload core - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: - name: MacOS core + name: MacOS Hydra core path: '${{github.workspace}}/build/libAlber.dylib' + - name: Configure CMake (Again) + run: | + rm -rf ${{github.workspace}}/build + cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DENABLE_USER_BUILD=ON -DBUILD_LIBRETRO_CORE=ON + + - name: Build (Again) + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} && ls -R ${{github.workspace}}/build + + - name: Upload Libretro core + uses: actions/upload-artifact@v4 + with: + name: MacOS Libretro core + path: | + ${{github.workspace}}/build/panda3ds_libretro.dylib + ${{github.workspace}}/docs/libretro/panda3ds_libretro.info + Linux: runs-on: ubuntu-latest @@ -98,11 +129,27 @@ jobs: run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - name: Upload core - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: - name: Linux core + name: Linux Hydra core path: '${{github.workspace}}/build/libAlber.so' + - name: Configure CMake (Again) + run: | + rm -rf ${{github.workspace}}/build + cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang-17 -DCMAKE_CXX_COMPILER=clang++-17 -DENABLE_USER_BUILD=ON -DBUILD_LIBRETRO_CORE=ON + + - name: Build (Again) + run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} + + - name: Upload Libretro core + uses: actions/upload-artifact@v4 + with: + name: Linux Libretro core + path: | + ${{github.workspace}}/build/panda3ds_libretro.so + ${{github.workspace}}/docs/libretro/panda3ds_libretro.info + Android-x64: runs-on: ubuntu-latest @@ -129,7 +176,7 @@ jobs: run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - name: Upload core - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: - name: Android core + name: Android Hydra core path: '${{github.workspace}}/build/libAlber.so' From 096d0a89ee4d6fc6163d5db103d15a27fa12689d Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 11 Jul 2024 22:22:33 +0300 Subject: [PATCH 051/251] Fix AES-CTR decryption for non-NCCHKey0 games --- include/loader/ncch.hpp | 2 ++ src/core/loader/ncch.cpp | 22 ++++++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/include/loader/ncch.hpp b/include/loader/ncch.hpp index 42ce1590..8e35643b 100644 --- a/include/loader/ncch.hpp +++ b/include/loader/ncch.hpp @@ -60,6 +60,8 @@ struct NCCH { CodeSetInfo text, data, rodata; FSInfo partitionInfo; + std::optional primaryKey, secondaryKey; + // Contents of the .code file in the ExeFS std::vector codeFile; // Contains of the cart's save data diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 3bf73e5d..a8e50101 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -29,6 +29,9 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn smdh.clear(); partitionInfo = info; + primaryKey = {}; + secondaryKey = {}; + size = u64(*(u32*)&header[0x104]) * mediaUnit; // TODO: Maybe don't type pun because big endian will break exheaderSize = *(u32*)&header[0x180]; @@ -78,11 +81,11 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn if (!primaryResult.first || !secondaryResult.first) { gotCryptoKeys = false; } else { - Crypto::AESKey primaryKey = primaryResult.second; - Crypto::AESKey secondaryKey = secondaryResult.second; + primaryKey = primaryResult.second; + secondaryKey = secondaryResult.second; EncryptionInfo encryptionInfoTmp; - encryptionInfoTmp.normalKey = primaryKey; + encryptionInfoTmp.normalKey = *primaryKey; encryptionInfoTmp.initialCounter.fill(0); for (std::size_t i = 1; i <= sizeof(std::uint64_t) - 1; i++) { @@ -94,7 +97,7 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn encryptionInfoTmp.initialCounter[8] = 2; exeFS.encryptionInfo = encryptionInfoTmp; - encryptionInfoTmp.normalKey = secondaryKey; + encryptionInfoTmp.normalKey = *secondaryKey; encryptionInfoTmp.initialCounter[8] = 3; romFS.encryptionInfo = encryptionInfoTmp; } @@ -201,13 +204,20 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn Helpers::panic("Second code file in a single NCCH partition. What should this do?\n"); } + // All files in ExeFS use the same IV, though .code uses the secondary key for decryption + // whereas .icon/.banner use the primary key. + FSInfo info = exeFS; + if (secondaryKey.has_value() && info.encryptionInfo.has_value()) { + info.encryptionInfo->normalKey = secondaryKey.value(); + } + if (compressCode) { std::vector tmp; tmp.resize(fileSize); // A file offset of 0 means our file is located right after the ExeFS header // So in the ROM, files are located at (file offset + exeFS offset + exeFS header size) - readFromFile(file, exeFS, tmp.data(), fileOffset + exeFSHeaderSize, fileSize); + readFromFile(file, info, tmp.data(), fileOffset + exeFSHeaderSize, fileSize); // Decompress .code file from the tmp vector to the "code" vector if (!CartLZ77::decompress(codeFile, tmp)) { @@ -216,7 +226,7 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn } } else { codeFile.resize(fileSize); - readFromFile(file, exeFS, codeFile.data(), fileOffset + exeFSHeaderSize, fileSize); + readFromFile(file, info, codeFile.data(), fileOffset + exeFSHeaderSize, fileSize); } } else if (std::strcmp(name, "icon") == 0) { // Parse icon file to extract region info and more in the future (logo, etc) From e6084363152edff610951ec81fda0add720a47e1 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 11 Jul 2024 22:27:05 +0300 Subject: [PATCH 052/251] Sanity check: Assert .code is encrypted before setting normal key --- src/core/loader/ncch.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index a8e50101..47d5a4c2 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -207,8 +207,8 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn // All files in ExeFS use the same IV, though .code uses the secondary key for decryption // whereas .icon/.banner use the primary key. FSInfo info = exeFS; - if (secondaryKey.has_value() && info.encryptionInfo.has_value()) { - info.encryptionInfo->normalKey = secondaryKey.value(); + if (encrypted && secondaryKey.has_value() && info.encryptionInfo.has_value()) { + info.encryptionInfo->normalKey = *secondaryKey; } if (compressCode) { From 276cf9e06f4fd2ef76b97ced83e53c22c914a698 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 12 Jul 2024 18:23:49 +0300 Subject: [PATCH 053/251] Build LuaJIT/Discord RPC even in LR core --- CMakeLists.txt | 2 -- 1 file changed, 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 85a915e2..1a876e58 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -48,8 +48,6 @@ endif() if(BUILD_LIBRETRO_CORE) set(CMAKE_POSITION_INDEPENDENT_CODE ON) - set(ENABLE_DISCORD_RPC OFF) - set(ENABLE_LUAJIT OFF) add_compile_definitions(__LIBRETRO__) endif() From d87477832b82f3543a4766030cb0f706a2dfe6d0 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 14 Jul 2024 15:32:26 +0300 Subject: [PATCH 054/251] Qt: Initial shader editor support --- CMakeLists.txt | 4 +- include/panda_qt/main_window.hpp | 7 +-- include/panda_qt/shader_editor.hpp | 28 ++++++++++ include/renderer.hpp | 8 +++ include/renderer_gl/renderer_gl.hpp | 6 ++- src/core/renderer_gl/renderer_gl.cpp | 5 +- src/host_shaders/opengl_fragment_shader.frag | 20 ++++---- src/panda_qt/main_window.cpp | 22 +++++--- src/panda_qt/shader_editor.cpp | 54 ++++++++++++++++++++ 9 files changed, 130 insertions(+), 24 deletions(-) create mode 100644 include/panda_qt/shader_editor.hpp create mode 100644 src/panda_qt/shader_editor.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 85a915e2..23c591c3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -457,11 +457,11 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE) set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp src/panda_qt/mappings.cpp - src/panda_qt/patch_window.cpp src/panda_qt/elided_label.cpp + src/panda_qt/patch_window.cpp src/panda_qt/elided_label.cpp src/panda_qt/shader_editor.cpp ) set(FRONTEND_HEADER_FILES include/panda_qt/screen.hpp include/panda_qt/main_window.hpp include/panda_qt/about_window.hpp include/panda_qt/config_window.hpp include/panda_qt/text_editor.hpp include/panda_qt/cheats_window.hpp - include/panda_qt/patch_window.hpp include/panda_qt/elided_label.hpp + include/panda_qt/patch_window.hpp include/panda_qt/elided_label.hpp include/panda_qt/shader_editor.hpp ) source_group("Source Files\\Qt" FILES ${FRONTEND_SOURCE_FILES}) diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index 72725257..831074a2 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -19,6 +19,7 @@ #include "panda_qt/config_window.hpp" #include "panda_qt/patch_window.hpp" #include "panda_qt/screen.hpp" +#include "panda_qt/shader_editor.hpp" #include "panda_qt/text_editor.hpp" #include "services/hid.hpp" @@ -48,6 +49,7 @@ class MainWindow : public QMainWindow { EditCheat, PressTouchscreen, ReleaseTouchscreen, + ReloadUbershader, }; // Tagged union representing our message queue messages @@ -99,6 +101,7 @@ class MainWindow : public QMainWindow { CheatsWindow* cheatsEditor; TextEditorWindow* luaEditor; PatchWindow* patchWindow; + ShaderEditorWindow* shaderEditor; // We use SDL's game controller API since it's the sanest API that supports as many controllers as possible SDL_GameController* gameController = nullptr; @@ -110,9 +113,6 @@ class MainWindow : public QMainWindow { void selectROM(); void dumpDspFirmware(); void dumpRomFS(); - void openLuaEditor(); - void openCheatsEditor(); - void openPatchWindow(); void showAboutMenu(); void initControllers(); void pollControllers(); @@ -139,5 +139,6 @@ class MainWindow : public QMainWindow { void mouseReleaseEvent(QMouseEvent* event) override; void loadLuaScript(const std::string& code); + void reloadShader(const std::string& shader); void editCheat(u32 handle, const std::vector& cheat, const std::function& callback); }; diff --git a/include/panda_qt/shader_editor.hpp b/include/panda_qt/shader_editor.hpp new file mode 100644 index 00000000..009381a0 --- /dev/null +++ b/include/panda_qt/shader_editor.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include +#include + +#include "zep.h" +#include "zep/mode_repl.h" +#include "zep/regress.h" + +class ShaderEditorWindow : public QDialog { + Q_OBJECT + + private: + Zep::ZepWidget_Qt zepWidget; + Zep::IZepReplProvider replProvider; + static constexpr float fontSize = 14.0f; + + // Whether this backend supports shader editor + bool shaderEditorSupported = true; + + public: + ShaderEditorWindow(QWidget* parent, const std::string& filename, const std::string& initialText); + void setText(const std::string& text) { zepWidget.GetEditor().GetMRUBuffer()->SetText(text); } + + void setEnable(bool enable); +}; \ No newline at end of file diff --git a/include/renderer.hpp b/include/renderer.hpp index 8888b41e..17812bcf 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -1,6 +1,7 @@ #pragma once #include #include +#include #include #include "PICA/pica_vertex.hpp" @@ -66,6 +67,13 @@ class Renderer { // This function does things like write back or cache necessary state before we delete our context virtual void deinitGraphicsContext() = 0; + // Functions for hooking up the renderer core to the frontend's shader editor for editing ubershaders in real time + // SupportsShaderReload: Indicates whether the backend offers ubershader reload support or not + // GetUbershader/SetUbershader: Gets or sets the renderer's current ubershader + virtual bool supportsShaderReload() { return false; } + virtual std::string getUbershader() { return ""; } + virtual void setUbershader(const std::string& shader) {} + // Functions for initializing the graphics context for the Qt frontend, where we don't have the convenience of SDL_Window #ifdef PANDA3DS_FRONTEND_QT virtual void initGraphicsContext(GL::Context* context) { Helpers::panic("Tried to initialize incompatible renderer with GL context"); } diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index 92f02662..4c2d9e66 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -82,7 +82,11 @@ class RendererGL final : public Renderer { void textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) override; void drawVertices(PICA::PrimType primType, std::span vertices) override; // Draw the given vertices void deinitGraphicsContext() override; - + + virtual bool supportsShaderReload() override { return true; } + virtual std::string getUbershader() override; + virtual void setUbershader(const std::string& shader) override; + std::optional getColourBuffer(u32 addr, PICA::ColorFmt format, u32 width, u32 height, bool createIfnotFound = true); // Note: The caller is responsible for deleting the currently bound FBO before calling this diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index a11a6ffa..3c68b8f9 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -812,4 +812,7 @@ void RendererGL::deinitGraphicsContext() { // All other GL objects should be invalidated automatically and be recreated by the next call to initGraphicsContext // TODO: Make it so that depth and colour buffers get written back to 3DS memory printf("RendererGL::DeinitGraphicsContext called\n"); -} \ No newline at end of file +} + +std::string RendererGL::getUbershader() { return ""; } +void RendererGL::setUbershader(const std::string& shader) {} \ No newline at end of file diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index f6fa6c55..303a27b6 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -279,26 +279,26 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { } } - uint lookup_config = bitfieldExtract(GPUREG_LIGHTi_CONFIG, 4, 4); + uint lookup_config = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 4, 4); if (lookup_config == 0u) { - d[D1_LUT] = 0.0; - d[FR_LUT] = 0.0; + d[D1_LUT] = 1.0; + d[FR_LUT] = 1.0; d[RG_LUT] = d[RB_LUT] = d[RR_LUT]; } else if (lookup_config == 1u) { - d[D0_LUT] = 0.0; - d[D1_LUT] = 0.0; + d[D0_LUT] = 1.0; + d[D1_LUT] = 1.0; d[RG_LUT] = d[RB_LUT] = d[RR_LUT]; } else if (lookup_config == 2u) { - d[FR_LUT] = 0.0; - d[SP_LUT] = 0.0; + d[FR_LUT] = 1.0; + d[SP_LUT] = 1.0; d[RG_LUT] = d[RB_LUT] = d[RR_LUT]; } else if (lookup_config == 3u) { - d[SP_LUT] = 0.0; + d[SP_LUT] = 1.0; d[RG_LUT] = d[RB_LUT] = d[RR_LUT] = 1.0; } else if (lookup_config == 4u) { - d[FR_LUT] = 0.0; + d[FR_LUT] = 1.0; } else if (lookup_config == 5u) { - d[D1_LUT] = 0.0; + d[D1_LUT] = 1.0; } else if (lookup_config == 6u) { d[RG_LUT] = d[RB_LUT] = d[RR_LUT]; } diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 54e4fabe..d8aab126 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -55,12 +55,14 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) auto luaEditorAction = toolsMenu->addAction(tr("Open Lua Editor")); auto cheatsEditorAction = toolsMenu->addAction(tr("Open Cheats Editor")); auto patchWindowAction = toolsMenu->addAction(tr("Open Patch Window")); + auto shaderEditorAction = toolsMenu->addAction(tr("Open Shader Editor")); auto dumpDspFirmware = toolsMenu->addAction(tr("Dump loaded DSP firmware")); connect(dumpRomFSAction, &QAction::triggered, this, &MainWindow::dumpRomFS); - connect(luaEditorAction, &QAction::triggered, this, &MainWindow::openLuaEditor); - connect(cheatsEditorAction, &QAction::triggered, this, &MainWindow::openCheatsEditor); - connect(patchWindowAction, &QAction::triggered, this, &MainWindow::openPatchWindow); + connect(luaEditorAction, &QAction::triggered, this, [this]() { luaEditor->show(); }); + connect(shaderEditorAction, &QAction::triggered, this, [this]() { shaderEditor->show(); }); + connect(cheatsEditorAction, &QAction::triggered, this, [this]() { cheatsEditor->show(); }); + connect(patchWindowAction, &QAction::triggered, this, [this]() { patchWindow->show(); }); connect(dumpDspFirmware, &QAction::triggered, this, &MainWindow::dumpDspFirmware); auto aboutAction = aboutMenu->addAction(tr("About Panda3DS")); @@ -75,6 +77,8 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) cheatsEditor = new CheatsWindow(emu, {}, this); patchWindow = new PatchWindow(this); luaEditor = new TextEditorWindow(this, "script.lua", ""); + shaderEditor = new ShaderEditorWindow(this, "shader.glsl", ""); + shaderEditor->setEnable(emu->getRenderer()->supportsShaderReload()); auto args = QCoreApplication::arguments(); if (args.size() > 1) { @@ -294,10 +298,6 @@ void MainWindow::showAboutMenu() { about.exec(); } -void MainWindow::openLuaEditor() { luaEditor->show(); } -void MainWindow::openCheatsEditor() { cheatsEditor->show(); } -void MainWindow::openPatchWindow() { patchWindow->show(); } - void MainWindow::dispatchMessage(const EmulatorMessage& message) { switch (message.type) { case MessageType::LoadROM: @@ -453,6 +453,14 @@ void MainWindow::loadLuaScript(const std::string& code) { sendMessage(message); } +void MainWindow::reloadShader(const std::string& shader) { + EmulatorMessage message{.type = MessageType::ReloadUbershader}; + + // Make a copy of the code on the heap to send via the message queue + message.string.str = new std::string(shader); + sendMessage(message); +} + void MainWindow::editCheat(u32 handle, const std::vector& cheat, const std::function& callback) { EmulatorMessage message{.type = MessageType::EditCheat}; diff --git a/src/panda_qt/shader_editor.cpp b/src/panda_qt/shader_editor.cpp new file mode 100644 index 00000000..8a23c854 --- /dev/null +++ b/src/panda_qt/shader_editor.cpp @@ -0,0 +1,54 @@ +#include +#include + +#include "panda_qt/main_window.hpp" +#include "panda_qt/shader_editor.hpp" + +using namespace Zep; + +ShaderEditorWindow::ShaderEditorWindow(QWidget* parent, const std::string& filename, const std::string& initialText) + : QDialog(parent), zepWidget(this, qApp->applicationDirPath().toStdString(), fontSize) { + resize(600, 600); + + // Register our extensions + ZepRegressExCommand::Register(zepWidget.GetEditor()); + ZepReplExCommand::Register(zepWidget.GetEditor(), &replProvider); + + // Default to standard mode instead of vim mode, initialize text box + zepWidget.GetEditor().InitWithText(filename, initialText); + zepWidget.GetEditor().SetGlobalMode(Zep::ZepMode_Standard::StaticName()); + + // Layout for widgets + QVBoxLayout* mainLayout = new QVBoxLayout(); + setLayout(mainLayout); + + QPushButton* button = new QPushButton(tr("Reload shader"), this); + button->setFixedSize(100, 20); + + // When the Load Script button is pressed, send the current text to the MainWindow, which will upload it to the emulator's lua object + connect(button, &QPushButton::pressed, this, [this]() { + if (parentWidget()) { + auto buffer = zepWidget.GetEditor().GetMRUBuffer(); + const std::string text = buffer->GetBufferText(buffer->Begin(), buffer->End()); + + static_cast(parentWidget())->reloadShader(text); + } else { + // This should be unreachable, only here for safety purposes + printf("Text editor does not have any parent widget, click doesn't work :(\n"); + } + }); + + mainLayout->addWidget(button); + mainLayout->addWidget(&zepWidget); +} + +void ShaderEditorWindow::setEnable(bool enable) { + shaderEditorSupported = enable; + + if (enable) { + setDisabled(false); + } else { + setDisabled(true); + setText("Shader editor window is not available for this renderer backend"); + } +} From 186fd3b94b48cc70514553b94e115e27901e2925 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 14 Jul 2024 15:49:35 +0300 Subject: [PATCH 055/251] Qt: Shader editor now works --- include/panda_qt/shader_editor.hpp | 7 +-- include/renderer_gl/renderer_gl.hpp | 1 + src/core/renderer_gl/renderer_gl.cpp | 59 +++++++++++++------- src/host_shaders/opengl_fragment_shader.frag | 20 +++---- src/panda_qt/main_window.cpp | 9 +++ src/panda_qt/shader_editor.cpp | 2 +- 6 files changed, 63 insertions(+), 35 deletions(-) diff --git a/include/panda_qt/shader_editor.hpp b/include/panda_qt/shader_editor.hpp index 009381a0..86bc1149 100644 --- a/include/panda_qt/shader_editor.hpp +++ b/include/panda_qt/shader_editor.hpp @@ -17,12 +17,11 @@ class ShaderEditorWindow : public QDialog { Zep::IZepReplProvider replProvider; static constexpr float fontSize = 14.0f; - // Whether this backend supports shader editor - bool shaderEditorSupported = true; - public: + // Whether this backend supports shader editor + bool supported = true; + ShaderEditorWindow(QWidget* parent, const std::string& filename, const std::string& initialText); void setText(const std::string& text) { zepWidget.GetEditor().GetMRUBuffer()->SetText(text); } - void setEnable(bool enable); }; \ No newline at end of file diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index 4c2d9e66..c947583e 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -92,6 +92,7 @@ class RendererGL final : public Renderer { // Note: The caller is responsible for deleting the currently bound FBO before calling this void setFBO(uint handle) { screenFramebuffer.m_handle = handle; } void resetStateManager() { gl.reset(); } + void initUbershader(OpenGL::Program& program); #ifdef PANDA3DS_FRONTEND_QT virtual void initGraphicsContext([[maybe_unused]] GL::Context* context) override { initGraphicsContextInternal(); } diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 3c68b8f9..cfa32319 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -57,24 +57,7 @@ void RendererGL::initGraphicsContextInternal() { OpenGL::Shader vert({vertexShaderSource.begin(), vertexShaderSource.size()}, OpenGL::Vertex); OpenGL::Shader frag({fragmentShaderSource.begin(), fragmentShaderSource.size()}, OpenGL::Fragment); triangleProgram.create({vert, frag}); - gl.useProgram(triangleProgram); - - textureEnvSourceLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvSource"); - textureEnvOperandLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvOperand"); - textureEnvCombinerLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvCombiner"); - textureEnvColorLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvColor"); - textureEnvScaleLoc = OpenGL::uniformLocation(triangleProgram, "u_textureEnvScale"); - - depthScaleLoc = OpenGL::uniformLocation(triangleProgram, "u_depthScale"); - depthOffsetLoc = OpenGL::uniformLocation(triangleProgram, "u_depthOffset"); - depthmapEnableLoc = OpenGL::uniformLocation(triangleProgram, "u_depthmapEnable"); - picaRegLoc = OpenGL::uniformLocation(triangleProgram, "u_picaRegs"); - - // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 - glUniform1i(OpenGL::uniformLocation(triangleProgram, "u_tex0"), 0); - glUniform1i(OpenGL::uniformLocation(triangleProgram, "u_tex1"), 1); - glUniform1i(OpenGL::uniformLocation(triangleProgram, "u_tex2"), 2); - glUniform1i(OpenGL::uniformLocation(triangleProgram, "u_tex_lighting_lut"), 3); + initUbershader(triangleProgram); auto displayVertexShaderSource = gl_resources.open("opengl_display.vert"); auto displayFragmentShaderSource = gl_resources.open("opengl_display.frag"); @@ -814,5 +797,41 @@ void RendererGL::deinitGraphicsContext() { printf("RendererGL::DeinitGraphicsContext called\n"); } -std::string RendererGL::getUbershader() { return ""; } -void RendererGL::setUbershader(const std::string& shader) {} \ No newline at end of file +std::string RendererGL::getUbershader() { + auto gl_resources = cmrc::RendererGL::get_filesystem(); + auto fragmentShader = gl_resources.open("opengl_fragment_shader.frag"); + + return std::string(fragmentShader.begin(), fragmentShader.end()); +} + +void RendererGL::setUbershader(const std::string& shader) { + auto gl_resources = cmrc::RendererGL::get_filesystem(); + auto vertexShaderSource = gl_resources.open("opengl_vertex_shader.vert"); + + OpenGL::Shader vert({vertexShaderSource.begin(), vertexShaderSource.size()}, OpenGL::Vertex); + OpenGL::Shader frag(shader, OpenGL::Fragment); + triangleProgram.create({vert, frag}); + + initUbershader(triangleProgram); +} + +void RendererGL::initUbershader(OpenGL::Program& program) { + gl.useProgram(program); + + textureEnvSourceLoc = OpenGL::uniformLocation(program, "u_textureEnvSource"); + textureEnvOperandLoc = OpenGL::uniformLocation(program, "u_textureEnvOperand"); + textureEnvCombinerLoc = OpenGL::uniformLocation(program, "u_textureEnvCombiner"); + textureEnvColorLoc = OpenGL::uniformLocation(program, "u_textureEnvColor"); + textureEnvScaleLoc = OpenGL::uniformLocation(program, "u_textureEnvScale"); + + depthScaleLoc = OpenGL::uniformLocation(program, "u_depthScale"); + depthOffsetLoc = OpenGL::uniformLocation(program, "u_depthOffset"); + depthmapEnableLoc = OpenGL::uniformLocation(program, "u_depthmapEnable"); + picaRegLoc = OpenGL::uniformLocation(program, "u_picaRegs"); + + // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 + glUniform1i(OpenGL::uniformLocation(program, "u_tex0"), 0); + glUniform1i(OpenGL::uniformLocation(program, "u_tex1"), 1); + glUniform1i(OpenGL::uniformLocation(program, "u_tex2"), 2); + glUniform1i(OpenGL::uniformLocation(program, "u_tex_lighting_lut"), 3); +} \ No newline at end of file diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index 303a27b6..f6fa6c55 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -279,26 +279,26 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { } } - uint lookup_config = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 4, 4); + uint lookup_config = bitfieldExtract(GPUREG_LIGHTi_CONFIG, 4, 4); if (lookup_config == 0u) { - d[D1_LUT] = 1.0; - d[FR_LUT] = 1.0; + d[D1_LUT] = 0.0; + d[FR_LUT] = 0.0; d[RG_LUT] = d[RB_LUT] = d[RR_LUT]; } else if (lookup_config == 1u) { - d[D0_LUT] = 1.0; - d[D1_LUT] = 1.0; + d[D0_LUT] = 0.0; + d[D1_LUT] = 0.0; d[RG_LUT] = d[RB_LUT] = d[RR_LUT]; } else if (lookup_config == 2u) { - d[FR_LUT] = 1.0; - d[SP_LUT] = 1.0; + d[FR_LUT] = 0.0; + d[SP_LUT] = 0.0; d[RG_LUT] = d[RB_LUT] = d[RR_LUT]; } else if (lookup_config == 3u) { - d[SP_LUT] = 1.0; + d[SP_LUT] = 0.0; d[RG_LUT] = d[RB_LUT] = d[RR_LUT] = 1.0; } else if (lookup_config == 4u) { - d[FR_LUT] = 1.0; + d[FR_LUT] = 0.0; } else if (lookup_config == 5u) { - d[D1_LUT] = 1.0; + d[D1_LUT] = 0.0; } else if (lookup_config == 6u) { d[RG_LUT] = d[RB_LUT] = d[RR_LUT]; } diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index d8aab126..cfa45e85 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -78,7 +78,11 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) patchWindow = new PatchWindow(this); luaEditor = new TextEditorWindow(this, "script.lua", ""); shaderEditor = new ShaderEditorWindow(this, "shader.glsl", ""); + shaderEditor->setEnable(emu->getRenderer()->supportsShaderReload()); + if (shaderEditor->supported) { + shaderEditor->setText(emu->getRenderer()->getUbershader()); + } auto args = QCoreApplication::arguments(); if (args.size() > 1) { @@ -351,6 +355,11 @@ void MainWindow::dispatchMessage(const EmulatorMessage& message) { emu->getServiceManager().getHID().setTouchScreenPress(message.touchscreen.x, message.touchscreen.y); break; case MessageType::ReleaseTouchscreen: emu->getServiceManager().getHID().releaseTouchScreen(); break; + + case MessageType::ReloadUbershader: + emu->getRenderer()->setUbershader(*message.string.str); + delete message.string.str; + break; } } diff --git a/src/panda_qt/shader_editor.cpp b/src/panda_qt/shader_editor.cpp index 8a23c854..122d841f 100644 --- a/src/panda_qt/shader_editor.cpp +++ b/src/panda_qt/shader_editor.cpp @@ -43,7 +43,7 @@ ShaderEditorWindow::ShaderEditorWindow(QWidget* parent, const std::string& filen } void ShaderEditorWindow::setEnable(bool enable) { - shaderEditorSupported = enable; + supported = enable; if (enable) { setDisabled(false); From c4e45ee6b8749750cf398dfe3a7c82f958fc910a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 14 Jul 2024 18:20:59 +0300 Subject: [PATCH 056/251] Renderer GL: Fix hotswapping shaders --- src/core/renderer_gl/renderer_gl.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index cfa32319..2d29e682 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -813,6 +813,10 @@ void RendererGL::setUbershader(const std::string& shader) { triangleProgram.create({vert, frag}); initUbershader(triangleProgram); + + glUniform1f(depthScaleLoc, oldDepthScale); + glUniform1f(depthOffsetLoc, oldDepthOffset); + glUniform1i(depthmapEnableLoc, oldDepthmapEnable); } void RendererGL::initUbershader(OpenGL::Program& program) { @@ -834,4 +838,4 @@ void RendererGL::initUbershader(OpenGL::Program& program) { glUniform1i(OpenGL::uniformLocation(program, "u_tex1"), 1); glUniform1i(OpenGL::uniformLocation(program, "u_tex2"), 2); glUniform1i(OpenGL::uniformLocation(program, "u_tex_lighting_lut"), 3); -} \ No newline at end of file +} From bee414a4f81bc4dd3019754a808667d371787bd3 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 14 Jul 2024 23:05:49 +0300 Subject: [PATCH 057/251] Downgrade SetFileSize failure to warning --- src/core/kernel/file_operations.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/kernel/file_operations.cpp b/src/core/kernel/file_operations.cpp index 972190fa..2b2020d1 100644 --- a/src/core/kernel/file_operations.cpp +++ b/src/core/kernel/file_operations.cpp @@ -184,7 +184,8 @@ void Kernel::setFileSize(u32 messagePointer, Handle fileHandle) { if (success) { mem.write32(messagePointer + 4, Result::Success); } else { - Helpers::panic("FileOp::SetFileSize failed"); + Helpers::warn("FileOp::SetFileSize failed"); + mem.write32(messagePointer + 4, Result::FailurePlaceholder); } } else { Helpers::panic("Tried to set file size of file without file descriptor"); From b384cb8ad9601197ea4162d200c9fcdf2a6fbfa9 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 15 Jul 2024 00:13:22 +0300 Subject: [PATCH 058/251] Fix build --- src/core/renderer_gl/renderer_gl.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 706d52ac..0b26f004 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -872,24 +872,24 @@ void RendererGL::setUbershader(const std::string& shader) { initUbershader(triangleProgram); - glUniform1f(depthScaleLoc, oldDepthScale); - glUniform1f(depthOffsetLoc, oldDepthOffset); - glUniform1i(depthmapEnableLoc, oldDepthmapEnable); + glUniform1f(ubershaderData.depthScaleLoc, oldDepthScale); + glUniform1f(ubershaderData.depthOffsetLoc, oldDepthOffset); + glUniform1i(ubershaderData.depthmapEnableLoc, oldDepthmapEnable); } void RendererGL::initUbershader(OpenGL::Program& program) { gl.useProgram(program); - textureEnvSourceLoc = OpenGL::uniformLocation(program, "u_textureEnvSource"); - textureEnvOperandLoc = OpenGL::uniformLocation(program, "u_textureEnvOperand"); - textureEnvCombinerLoc = OpenGL::uniformLocation(program, "u_textureEnvCombiner"); - textureEnvColorLoc = OpenGL::uniformLocation(program, "u_textureEnvColor"); - textureEnvScaleLoc = OpenGL::uniformLocation(program, "u_textureEnvScale"); + ubershaderData.textureEnvSourceLoc = OpenGL::uniformLocation(program, "u_textureEnvSource"); + ubershaderData.textureEnvOperandLoc = OpenGL::uniformLocation(program, "u_textureEnvOperand"); + ubershaderData.textureEnvCombinerLoc = OpenGL::uniformLocation(program, "u_textureEnvCombiner"); + ubershaderData.textureEnvColorLoc = OpenGL::uniformLocation(program, "u_textureEnvColor"); + ubershaderData.textureEnvScaleLoc = OpenGL::uniformLocation(program, "u_textureEnvScale"); - depthScaleLoc = OpenGL::uniformLocation(program, "u_depthScale"); - depthOffsetLoc = OpenGL::uniformLocation(program, "u_depthOffset"); - depthmapEnableLoc = OpenGL::uniformLocation(program, "u_depthmapEnable"); - picaRegLoc = OpenGL::uniformLocation(program, "u_picaRegs"); + ubershaderData.depthScaleLoc = OpenGL::uniformLocation(program, "u_depthScale"); + ubershaderData.depthOffsetLoc = OpenGL::uniformLocation(program, "u_depthOffset"); + ubershaderData.depthmapEnableLoc = OpenGL::uniformLocation(program, "u_depthmapEnable"); + ubershaderData.picaRegLoc = OpenGL::uniformLocation(program, "u_picaRegs"); // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 glUniform1i(OpenGL::uniformLocation(program, "u_tex0"), 0); From ea59933b187732ec4dd2dffc52f9c3ab00c970d9 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 15 Jul 2024 00:46:15 +0300 Subject: [PATCH 059/251] Simplify alpha test code --- src/core/PICA/shader_gen_glsl.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 50be94f0..0e51ad93 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -358,13 +358,13 @@ void FragmentGenerator::getAlphaOperation(std::string& shader, TexEnvConfig::Ope void FragmentGenerator::applyAlphaTest(std::string& shader, const PICARegs& regs) { const u32 alphaConfig = regs[InternalRegs::AlphaTestConfig]; + const auto function = static_cast(Helpers::getBits<4, 3>(alphaConfig)); + // Alpha test disabled - if (Helpers::getBit<0>(alphaConfig) == 0) { + if (Helpers::getBit<0>(alphaConfig) == 0 || function == CompareFunction::Always) { return; } - const auto function = static_cast(Helpers::getBits<4, 3>(alphaConfig)); - shader += "if ("; switch (function) { case CompareFunction::Never: shader += "true"; break; From 133082c2322a97e3e96b6e1eb906ca01951f80b8 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 15 Jul 2024 02:12:04 +0300 Subject: [PATCH 060/251] x64 shader rec: Add support for PICA non-IEEE multiplication --- .../PICA/dynapica/shader_rec_emitter_x64.hpp | 5 + .../PICA/dynapica/shader_rec_emitter_x64.cpp | 160 +++++++++++++++--- 2 files changed, 142 insertions(+), 23 deletions(-) diff --git a/include/PICA/dynapica/shader_rec_emitter_x64.hpp b/include/PICA/dynapica/shader_rec_emitter_x64.hpp index 0338911c..1052d6a0 100644 --- a/include/PICA/dynapica/shader_rec_emitter_x64.hpp +++ b/include/PICA/dynapica/shader_rec_emitter_x64.hpp @@ -32,6 +32,8 @@ class ShaderEmitter : public Xbyak::CodeGenerator { Label negateVector; // Vector value of (1.0, 1.0, 1.0, 1.0) for SLT(i)/SGE(i) Label onesVector; + // Vector value of (0xFF, 0xFF, 0xFF, 0) for setting the w component to 0 in DP3 + Label dp3Vector; u32 recompilerPC = 0; // PC the recompiler is currently recompiling @ u32 loopLevel = 0; // The current loop nesting level (0 = not in a loop) @@ -49,6 +51,9 @@ class ShaderEmitter : public Xbyak::CodeGenerator { Xbyak::Label emitExp2Func(); Xbyak::util::Cpu cpuCaps; + // Emit a PICA200-compliant multiplication that handles "0 * inf = 0" + void emitSafeMUL(Xbyak::Xmm src1, Xbyak::Xmm src2, Xbyak::Xmm scratch); + // Compile all instructions from [current recompiler PC, end) void compileUntil(const PICAShader& shaderUnit, u32 endPC); // Compile instruction "instr" diff --git a/src/core/PICA/dynapica/shader_rec_emitter_x64.cpp b/src/core/PICA/dynapica/shader_rec_emitter_x64.cpp index c134b72f..e7bafe9f 100644 --- a/src/core/PICA/dynapica/shader_rec_emitter_x64.cpp +++ b/src/core/PICA/dynapica/shader_rec_emitter_x64.cpp @@ -12,6 +12,9 @@ using namespace Xbyak; using namespace Xbyak::util; using namespace Helpers; +// TODO: Expose safe/unsafe optimizations to the user +constexpr bool useSafeMUL = false; + // The shader recompiler uses quite an odd internal ABI // We make use of the fact that in regular conditions, we should pretty much never be calling C++ code from recompiled shader code // This allows us to establish an ABI that's optimized for this sort of workflow, statically allocating volatile host registers @@ -45,6 +48,16 @@ void ShaderEmitter::compile(const PICAShader& shaderUnit) { L(onesVector); dd(0x3f800000); dd(0x3f800000); dd(0x3f800000); dd(0x3f800000); // 1.0 4 times + if (useSafeMUL) { + // When doing safe mul, we need a vector to set only the w component to 0 for DP3 + L(dp3Vector); + + dd(0xFFFFFFFF); + dd(0xFFFFFFFF); + dd(0xFFFFFFFF); + dd(0); + } + // Emit prologue first align(16); prologueCb = getCurr(); @@ -523,24 +536,60 @@ void ShaderEmitter::recDP3(const PICAShader& shader, u32 instruction) { const u32 idx = getBits<19, 2>(instruction); const u32 dest = getBits<21, 5>(instruction); - // TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA) loadRegister<1>(src1_xmm, shader, src1, idx, operandDescriptor); loadRegister<2>(src2_xmm, shader, src2, 0, operandDescriptor); - dpps(src1_xmm, src2_xmm, 0b01111111); // 3-lane dot product between the 2 registers, store the result in all lanes of scratch1 similarly to PICA + + if (!useSafeMUL) { + dpps(src1_xmm, src2_xmm, 0b01111111); + } else { + const u32 writeMask = operandDescriptor & 0xf; + + // Set w component to 0 and do a DP4 + andps(src1_xmm, xword[rip + dp3Vector]); + + // Set src1 to src1 * src2, then get the dot product by doing 2 horizontal adds + emitSafeMUL(src1_xmm, src2_xmm, scratch1); + haddps(src1_xmm, src1_xmm); + haddps(src1_xmm, src1_xmm); + + // If we only write back the x component to the result, we needn't perform a shuffle to do res = res.xxxx + // Otherwise we do + if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x + shufps(src1_xmm, src1_xmm, 0); // src1_xmm = src1_xmm.xxxx + } + } + storeRegister(src1_xmm, shader, dest, operandDescriptor); } void ShaderEmitter::recDP4(const PICAShader& shader, u32 instruction) { const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; const u32 src1 = getBits<12, 7>(instruction); - const u32 src2 = getBits<7, 5>(instruction); // src2 coming first because PICA moment + const u32 src2 = getBits<7, 5>(instruction); // src2 coming first because PICA moment const u32 idx = getBits<19, 2>(instruction); const u32 dest = getBits<21, 5>(instruction); - // TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA) loadRegister<1>(src1_xmm, shader, src1, idx, operandDescriptor); loadRegister<2>(src2_xmm, shader, src2, 0, operandDescriptor); - dpps(src1_xmm, src2_xmm, 0b11111111); // 4-lane dot product between the 2 registers, store the result in all lanes of scratch1 similarly to PICA + + if (!useSafeMUL) { + // 4-lane dot product between the 2 registers, store the result in all lanes of scratch1 similarly to PICA + dpps(src1_xmm, src2_xmm, 0b11111111); + } else { + const u32 writeMask = operandDescriptor & 0xf; + + // Set src1 to src1 * src2, then get the dot product by doing 2 horizontal adds + emitSafeMUL(src1_xmm, src2_xmm, scratch1); + haddps(src1_xmm, src1_xmm); + haddps(src1_xmm, src1_xmm); + + // If we only write back the x component to the result, we needn't perform a shuffle to do res = res.xxxx + // Otherwise we do + if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x + shufps(src1_xmm, src1_xmm, 0); // src1_xmm = src1_xmm.xxxx + } + } + storeRegister(src1_xmm, shader, dest, operandDescriptor); } @@ -553,7 +602,6 @@ void ShaderEmitter::recDPH(const PICAShader& shader, u32 instruction) { const u32 idx = getBits<19, 2>(instruction); const u32 dest = getBits<21, 5>(instruction); - // TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA) loadRegister<1>(src1_xmm, shader, src1, isDPHI ? 0 : idx, operandDescriptor); loadRegister<2>(src2_xmm, shader, src2, isDPHI ? idx : 0, operandDescriptor); @@ -566,7 +614,25 @@ void ShaderEmitter::recDPH(const PICAShader& shader, u32 instruction) { unpcklpd(src1_xmm, scratch1); } - dpps(src1_xmm, src2_xmm, 0b11111111); // 4-lane dot product between the 2 registers, store the result in all lanes of scratch1 similarly to PICA + // Now perform a DP4 + if (!useSafeMUL) { + // 4-lane dot product between the 2 registers, store the result in all lanes of scratch1 similarly to PICA + dpps(src1_xmm, src2_xmm, 0b11111111); + } else { + const u32 writeMask = operandDescriptor & 0xf; + + // Set src1 to src1 * src2, then get the dot product by doing 2 horizontal adds + emitSafeMUL(src1_xmm, src2_xmm, scratch1); + haddps(src1_xmm, src1_xmm); + haddps(src1_xmm, src1_xmm); + + // If we only write back the x component to the result, we needn't perform a shuffle to do res = res.xxxx + // Otherwise we do + if (writeMask != 0x8) { // Copy bottom lane to all lanes if we're not simply writing back x + shufps(src1_xmm, src1_xmm, 0); // src1_xmm = src1_xmm.xxxx + } + } + storeRegister(src1_xmm, shader, dest, operandDescriptor); } @@ -603,10 +669,15 @@ void ShaderEmitter::recMUL(const PICAShader& shader, u32 instruction) { const u32 idx = getBits<19, 2>(instruction); const u32 dest = getBits<21, 5>(instruction); - // TODO: Safe multiplication equivalent (Multiplication is not IEEE compliant on the PICA) loadRegister<1>(src1_xmm, shader, src1, idx, operandDescriptor); loadRegister<2>(src2_xmm, shader, src2, 0, operandDescriptor); - mulps(src1_xmm, src2_xmm); + + if (!useSafeMUL) { + mulps(src1_xmm, src2_xmm); + } else { + emitSafeMUL(src1_xmm, src2_xmm, scratch1); + } + storeRegister(src1_xmm, shader, dest, operandDescriptor); } @@ -662,23 +733,31 @@ void ShaderEmitter::recMAD(const PICAShader& shader, u32 instruction) { loadRegister<2>(src2_xmm, shader, src2, isMADI ? 0 : idx, operandDescriptor); loadRegister<3>(src3_xmm, shader, src3, isMADI ? idx : 0, operandDescriptor); - // TODO: Implement safe PICA mul // If we have FMA3, optimize MAD to use FMA - if (haveFMA3) { - vfmadd213ps(src1_xmm, src2_xmm, src3_xmm); - storeRegister(src1_xmm, shader, dest, operandDescriptor); - } - - // If we don't have FMA3, do a multiplication and addition - else { - // Multiply src1 * src2 - if (haveAVX) { - vmulps(scratch1, src1_xmm, src2_xmm); - } else { - movaps(scratch1, src1_xmm); - mulps(scratch1, src2_xmm); + if (!useSafeMUL) { + if (haveFMA3) { + vfmadd213ps(src1_xmm, src2_xmm, src3_xmm); + storeRegister(src1_xmm, shader, dest, operandDescriptor); } + // If we don't have FMA3, do a multiplication and addition + else { + // Multiply src1 * src2 + if (haveAVX) { + vmulps(scratch1, src1_xmm, src2_xmm); + } else { + movaps(scratch1, src1_xmm); + mulps(scratch1, src2_xmm); + } + + // Add src3 + addps(scratch1, src3_xmm); + storeRegister(scratch1, shader, dest, operandDescriptor); + } + } else { + movaps(scratch1, src1_xmm); + emitSafeMUL(scratch1, src2_xmm, src1_xmm); + // Add src3 addps(scratch1, src3_xmm); storeRegister(scratch1, shader, dest, operandDescriptor); @@ -1115,6 +1194,41 @@ Xbyak::Label ShaderEmitter::emitLog2Func() { return subroutine; } +void ShaderEmitter::emitSafeMUL(Xmm src1, Xmm src2, Xmm scratch) { + // 0 * inf and inf * 0 in the PICA should return 0 instead of NaN + // This can be done by checking for NaNs before and after a multiplication + // To do this we can create a mask of which components of src1/src2 are NOT NaN using cmpordsps (cmpps with imm = 7) + // Then we multiply src1 and src2 and reate a mask of which components of the result ARE NaN using cmpunordps + // If the NaNs didn't exist (ie they were created by 0 * inf) before then we set them to 0 by XORing the 2 masks and ANDing the multiplication + // result with the xor result + // Based on Citra implementation, particularly the AVX-512 version + + if (cpuCaps.has(Cpu::tAVX512F | Cpu::tAVX512VL)) { + const Xbyak::Opmask zeroMask = k1; + + vmulps(scratch, src1, src2); + // Mask of any NaN values found in the result + vcmpunordps(zeroMask, scratch, scratch); + // Mask of any non-NaN inputs producing NaN results + vcmpordps(zeroMask | zeroMask, src1, src2); + + knotb(zeroMask, zeroMask); + vmovaps(src1 | zeroMask | T_z, scratch); + } else { + if (haveAVX) { + vcmpordps(scratch, src1, src2); + } else { + movaps(scratch, src1); + cmpordps(scratch, src2); + } + + mulps(src1, src2); + cmpunordps(src2, src1); + xorps(src2, scratch); + andps(src1, src2); + } +} + Xbyak::Label ShaderEmitter::emitExp2Func() { Xbyak::Label subroutine; From c8eb1c1128581d7409464e98c2a672a394737da9 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 15 Jul 2024 04:10:47 +0300 Subject: [PATCH 061/251] Shader recompiler: Add UBO --- CMakeLists.txt | 1 + include/PICA/pica_frag_config.hpp | 4 +- include/PICA/pica_frag_uniforms.hpp | 18 +++++++++ include/renderer_gl/renderer_gl.hpp | 7 +++- src/core/PICA/shader_gen_glsl.cpp | 37 ++++++++++++----- src/core/renderer_gl/renderer_gl.cpp | 60 ++++++++++++++++++++++++---- 6 files changed, 105 insertions(+), 22 deletions(-) create mode 100644 include/PICA/pica_frag_uniforms.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 194200f0..c52ccd51 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -249,6 +249,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/audio/dsp_core.hpp include/audio/null_core.hpp include/audio/teakra_core.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp include/PICA/pica_frag_config.hpp + include/PICA/pica_frag_uniforms.hpp ) cmrc_add_resource_library( diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index c4d46b11..8352cba2 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -23,13 +23,11 @@ namespace PICA { u32 texUnitConfig; u32 texEnvUpdateBuffer; - // TODO: This should probably be a uniform - u32 texEnvBufferColor; - // There's 6 TEV stages, and each one is configured via 5 word-sized registers std::array tevConfigs; }; + // Config used for identifying unique fragment pipeline configurations struct FragmentConfig { OutputConfig outConfig; TextureConfig texConfig; diff --git a/include/PICA/pica_frag_uniforms.hpp b/include/PICA/pica_frag_uniforms.hpp new file mode 100644 index 00000000..b151ed42 --- /dev/null +++ b/include/PICA/pica_frag_uniforms.hpp @@ -0,0 +1,18 @@ +#pragma once +#include +#include + +#include "helpers.hpp" + +namespace PICA { + struct FragmentUniforms { + using vec3 = std::array; + using vec4 = std::array; + static constexpr usize tevStageCount = 6; + + s32 alphaReference; + + alignas(16) vec4 constantColors[tevStageCount]; + alignas(16) vec4 tevBufferColor; + }; +} // namespace PICA \ No newline at end of file diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index b4cf9c6f..a028bdd3 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -67,7 +67,12 @@ class RendererGL final : public Renderer { OpenGL::Framebuffer screenFramebuffer; OpenGL::Texture blankTexture; - std::unordered_map shaderCache; + // Cached recompiled fragment shader + struct CachedProgram { + OpenGL::Program program; + uint uboBinding; + }; + std::unordered_map shaderCache; OpenGL::Framebuffer getColourFBO(); OpenGL::Texture getTexture(Texture& tex); diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 0e51ad93..50e9c3de 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -38,8 +38,6 @@ std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { out vec2 v_texcoord1; out vec3 v_view; out vec2 v_texcoord2; - flat out vec4 v_textureEnvColor[6]; - flat out vec4 v_textureEnvBufferColor; //out float gl_ClipDistance[2]; @@ -103,8 +101,6 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { in vec2 v_texcoord1; in vec3 v_view; in vec2 v_texcoord2; - flat in vec4 v_textureEnvColor[6]; - flat in vec4 v_textureEnvBufferColor; out vec4 fragColor; uniform sampler2D u_tex0; @@ -115,18 +111,21 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { uniform sampler1DArray u_tex_lighting_lut; #endif - vec4 tevSources[16]; - vec4 tevNextPreviousBuffer; + layout(std140) uniform FragmentUniforms { + int alphaReference; + + vec4 constantColors[6]; + vec4 tevBufferColor; + }; )"; // Emit main function for fragment shader // When not initialized, source 13 is set to vec4(0.0) and 15 is set to the vertex colour ret += R"( void main() { - tevSources[0] = v_colour; - tevSources[13] = vec4(0.0); // Previous buffer colour - tevSources[15] = v_colour; // Previous combiner vec4 combinerOutput = v_colour; // Last TEV output + vec4 previousBuffer = vec4(0.0); // Previous buffer + vec4 tevNextPreviousBuffer = tevBufferColor; )"; ret += R"( @@ -148,7 +147,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { ret += "fragColor = combinerOutput;\n"; ret += "}"; // End of main function - ret += "\n\n\n\n\n\n\n\n\n\n\n\n\n"; + ret += "\n\n\n\n\n\n\n\n\n\n"; return ret; } @@ -201,6 +200,22 @@ void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICAReg shader += "combinerOutput = vec4(clamp(outputColor" + std::to_string(stage) + " * " + std::to_string(tev.getColorScale()) + ".0, vec3(0.0), vec3(1.0)), clamp(outputAlpha" + std::to_string(stage) + " * " + std::to_string(tev.getAlphaScale()) + ".0, 0.0, 1.0));\n"; + + shader += "previousBuffer = tevNextPreviousBuffer;\n"; + + // Update the "next previous buffer" if necessary + const u32 textureEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; + if (stage < 4) { + // Check whether to update rgb + if ((textureEnvUpdateBuffer & (0x100 << stage))) { + shader += "tevNextPreviousBuffer.rgb = combinerOutput.rgb;\n"; + } + + // And whether to update alpha + if ((textureEnvUpdateBuffer & (0x1000u << stage))) { + shader += "tevNextPreviousBuffer.a = combinerOutput.a;\n"; + } + } } } @@ -308,6 +323,8 @@ void FragmentGenerator::getSource(std::string& shader, TexEnvConfig::Source sour } case TexEnvConfig::Source::Previous: shader += "combinerOutput"; break; + case TexEnvConfig::Source::Constant: shader += "constantColors[" + std::to_string(index) + "]"; break; + case TexEnvConfig::Source::PreviousBuffer: shader += "previousBuffer"; break; default: Helpers::warn("Unimplemented TEV source: %d", static_cast(source)); diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 0b26f004..aa3bb61b 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -5,6 +5,7 @@ #include #include "PICA/float_types.hpp" +#include "PICA/pica_frag_uniforms.hpp" #include "PICA/gpu.hpp" #include "PICA/regs.hpp" #include "math_util.hpp" @@ -413,7 +414,7 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v const float depthOffset = f24::fromRaw(regs[PICA::InternalRegs::DepthOffset] & 0xffffff).toFloat32(); const bool depthMapEnable = regs[PICA::InternalRegs::DepthmapEnable] & 1; - // Update depth uniforms + // Update ubershader uniforms if (usingUbershader) { if (oldDepthScale != depthScale) { oldDepthScale = depthScale; @@ -429,17 +430,15 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v oldDepthmapEnable = depthMapEnable; glUniform1i(ubershaderData.depthmapEnableLoc, depthMapEnable); } - } - setupTextureEnvState(); - bindTexturesToSlots(); - - if (usingUbershader) { // Upload PICA Registers as a single uniform. The shader needs access to the rasterizer registers (for depth, starting from index 0x48) // The texturing and the fragment lighting registers. Therefore we upload them all in one go to avoid multiple slow uniform updates glUniform1uiv(ubershaderData.picaRegLoc, 0x200 - 0x48, ®s[0x48]); } + setupTextureEnvState(); + bindTexturesToSlots(); + if (gpu.lightingLUTDirty) { updateLightingLUT(); } @@ -778,6 +777,8 @@ std::optional RendererGL::getColourBuffer(u32 addr, PICA::ColorFmt } OpenGL::Program& RendererGL::getSpecializedShader() { + constexpr uint uboBlockBinding = 2; + PICA::FragmentConfig fsConfig; auto& outConfig = fsConfig.outConfig; auto& texConfig = fsConfig.texConfig; @@ -788,7 +789,6 @@ OpenGL::Program& RendererGL::getSpecializedShader() { texConfig.texUnitConfig = regs[InternalRegs::TexUnitCfg]; texConfig.texEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; - texConfig.texEnvBufferColor = 0; // Set up TEV stages std::memcpy(&texConfig.tevConfigs[0 * 5], ®s[InternalRegs::TexEnv0Source], 5 * sizeof(u32)); @@ -798,7 +798,9 @@ OpenGL::Program& RendererGL::getSpecializedShader() { std::memcpy(&texConfig.tevConfigs[4 * 5], ®s[InternalRegs::TexEnv4Source], 5 * sizeof(u32)); std::memcpy(&texConfig.tevConfigs[5 * 5], ®s[InternalRegs::TexEnv5Source], 5 * sizeof(u32)); - OpenGL::Program& program = shaderCache[fsConfig]; + CachedProgram& programEntry = shaderCache[fsConfig]; + OpenGL::Program& program = programEntry.program; + if (!program.exists()) { std::string vs = fragShaderGen.getVertexShader(regs); std::string fs = fragShaderGen.generate(regs); @@ -814,8 +816,50 @@ OpenGL::Program& RendererGL::getSpecializedShader() { glUniform1i(OpenGL::uniformLocation(program, "u_tex1"), 1); glUniform1i(OpenGL::uniformLocation(program, "u_tex2"), 2); glUniform1i(OpenGL::uniformLocation(program, "u_tex_lighting_lut"), 3); + + // Allocate memory for the program UBO + glGenBuffers(1, &programEntry.uboBinding); + glBindBuffer(GL_UNIFORM_BUFFER, programEntry.uboBinding); + glBufferData(GL_UNIFORM_BUFFER, sizeof(PICA::FragmentUniforms), nullptr, GL_DYNAMIC_DRAW); + + // Set up the binding for our UBO. Sadly we can't specify it in the shader like normal people, + // As it's an OpenGL 4.2 feature that MacOS doesn't support... + uint uboIndex = glGetUniformBlockIndex(program.handle(), "FragmentUniforms"); + glUniformBlockBinding(program.handle(), uboIndex, uboBlockBinding); + glBindBufferBase(GL_UNIFORM_BUFFER, uboBlockBinding, programEntry.uboBinding); } + // Upload uniform data to our shader's UBO + PICA::FragmentUniforms uniforms; + uniforms.alphaReference = Helpers::getBits<8, 8>(regs[InternalRegs::AlphaTestConfig]); + + // Set up the texenv buffer color + const u32 texEnvBufferColor = regs[InternalRegs::TexEnvBufferColor]; + uniforms.tevBufferColor[0] = float(texEnvBufferColor & 0xFF) / 255.0f; + uniforms.tevBufferColor[1] = float((texEnvBufferColor >> 8) & 0xFF) / 255.0f; + uniforms.tevBufferColor[2] = float((texEnvBufferColor >> 16) & 0xFF) / 255.0f; + uniforms.tevBufferColor[3] = float((texEnvBufferColor >> 24) & 0xFF) / 255.0f; + + // Set up the constant color for the 6 TEV stages + for (int i = 0; i < 6; i++) { + static constexpr std::array ioBases = { + PICA::InternalRegs::TexEnv0Source, PICA::InternalRegs::TexEnv1Source, PICA::InternalRegs::TexEnv2Source, + PICA::InternalRegs::TexEnv3Source, PICA::InternalRegs::TexEnv4Source, PICA::InternalRegs::TexEnv5Source, + }; + + auto& vec = uniforms.constantColors[i]; + u32 base = ioBases[i]; + u32 color = regs[base + 3]; + + vec[0] = float(color & 0xFF) / 255.0f; + vec[1] = float((color >> 8) & 0xFF) / 255.0f; + vec[2] = float((color >> 16) & 0xFF) / 255.0f; + vec[3] = float((color >> 24) & 0xFF) / 255.0f; + } + + glBindBuffer(GL_UNIFORM_BUFFER, programEntry.uboBinding); + glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(PICA::FragmentUniforms), &uniforms); + return program; } From 0878474e01aa6d982575ff63394d375e4fa1b13b Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 15 Jul 2024 04:30:04 +0300 Subject: [PATCH 062/251] Shader recompiler: Add depth mapping --- include/PICA/pica_frag_config.hpp | 1 + include/PICA/pica_frag_uniforms.hpp | 2 ++ src/core/PICA/shader_gen_glsl.cpp | 15 +++++++++++++++ src/core/renderer_gl/renderer_gl.cpp | 13 +++++++++---- 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index 8352cba2..59f13757 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -16,6 +16,7 @@ namespace PICA { // Merge the enable + compare function into 1 field to avoid duplicate shaders // enable == off means a CompareFunction of Always BitField<0, 3, CompareFunction> alphaTestFunction; + BitField<4, 1, u32> depthMapEnable; }; }; diff --git a/include/PICA/pica_frag_uniforms.hpp b/include/PICA/pica_frag_uniforms.hpp index b151ed42..616f1882 100644 --- a/include/PICA/pica_frag_uniforms.hpp +++ b/include/PICA/pica_frag_uniforms.hpp @@ -11,6 +11,8 @@ namespace PICA { static constexpr usize tevStageCount = 6; s32 alphaReference; + float depthScale; + float depthOffset; alignas(16) vec4 constantColors[tevStageCount]; alignas(16) vec4 tevBufferColor; diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 50e9c3de..f19c699d 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -113,6 +113,8 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { layout(std140) uniform FragmentUniforms { int alphaReference; + float depthScale; + float depthOffset; vec4 constantColors[6]; vec4 tevBufferColor; @@ -138,6 +140,19 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { float alphaOp3 = 0.0; )"; + ret += R"( + // Get original depth value by converting from [near, far] = [0, 1] to [-1, 1] + // We do this by converting to [0, 2] first and subtracting 1 to go to [-1, 1] + float z_over_w = gl_FragCoord.z * 2.0f - 1.0f; + float depth = z_over_w * depthScale + depthOffset; + )"; + + if ((regs[InternalRegs::DepthmapEnable] & 1) == 0) { + ret += "depth /= gl_FragCoord.w;\n"; + } + + ret += "gl_FragDepth = depth;\n"; + textureConfig = regs[InternalRegs::TexUnitCfg]; for (int i = 0; i < 6; i++) { compileTEV(ret, i, regs); diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index aa3bb61b..9c60ac5f 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -410,12 +410,12 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v static constexpr std::array depthModes = {GL_NEVER, GL_ALWAYS, GL_EQUAL, GL_NOTEQUAL, GL_LESS, GL_LEQUAL, GL_GREATER, GL_GEQUAL}; - const float depthScale = f24::fromRaw(regs[PICA::InternalRegs::DepthScale] & 0xffffff).toFloat32(); - const float depthOffset = f24::fromRaw(regs[PICA::InternalRegs::DepthOffset] & 0xffffff).toFloat32(); - const bool depthMapEnable = regs[PICA::InternalRegs::DepthmapEnable] & 1; - // Update ubershader uniforms if (usingUbershader) { + const float depthScale = f24::fromRaw(regs[PICA::InternalRegs::DepthScale] & 0xffffff).toFloat32(); + const float depthOffset = f24::fromRaw(regs[PICA::InternalRegs::DepthOffset] & 0xffffff).toFloat32(); + const bool depthMapEnable = regs[PICA::InternalRegs::DepthmapEnable] & 1; + if (oldDepthScale != depthScale) { oldDepthScale = depthScale; glUniform1f(ubershaderData.depthScaleLoc, depthScale); @@ -785,7 +785,9 @@ OpenGL::Program& RendererGL::getSpecializedShader() { auto alphaTestConfig = regs[InternalRegs::AlphaTestConfig]; auto alphaTestFunction = Helpers::getBits<4, 3>(alphaTestConfig); + outConfig.alphaTestFunction = (alphaTestConfig & 1) ? static_cast(alphaTestFunction) : PICA::CompareFunction::Always; + outConfig.depthMapEnable = regs[InternalRegs::DepthmapEnable] & 1; texConfig.texUnitConfig = regs[InternalRegs::TexUnitCfg]; texConfig.texEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; @@ -840,6 +842,9 @@ OpenGL::Program& RendererGL::getSpecializedShader() { uniforms.tevBufferColor[2] = float((texEnvBufferColor >> 16) & 0xFF) / 255.0f; uniforms.tevBufferColor[3] = float((texEnvBufferColor >> 24) & 0xFF) / 255.0f; + uniforms.depthScale = f24::fromRaw(regs[PICA::InternalRegs::DepthScale] & 0xffffff).toFloat32(); + uniforms.depthOffset = f24::fromRaw(regs[PICA::InternalRegs::DepthOffset] & 0xffffff).toFloat32(); + // Set up the constant color for the 6 TEV stages for (int i = 0; i < 6; i++) { static constexpr std::array ioBases = { From fe53214c863cf5fb5219319c9b2a8335ebf2f9b1 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 15 Jul 2024 04:51:08 +0300 Subject: [PATCH 063/251] Shader recompiler: Finish alpha test and stub lighting --- src/core/PICA/shader_gen_glsl.cpp | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index f19c699d..11030848 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -340,6 +340,10 @@ void FragmentGenerator::getSource(std::string& shader, TexEnvConfig::Source sour case TexEnvConfig::Source::Previous: shader += "combinerOutput"; break; case TexEnvConfig::Source::Constant: shader += "constantColors[" + std::to_string(index) + "]"; break; case TexEnvConfig::Source::PreviousBuffer: shader += "previousBuffer"; break; + + // Lighting + case TexEnvConfig::Source::PrimaryFragmentColor: + case TexEnvConfig::Source::SecondaryFragmentColor: shader += "vec4(0.0, 0.0, 0.0, 1.0)"; break; default: Helpers::warn("Unimplemented TEV source: %d", static_cast(source)); @@ -397,15 +401,23 @@ void FragmentGenerator::applyAlphaTest(std::string& shader, const PICARegs& regs return; } - shader += "if ("; + shader += "float alphaReferenceFloat = float(alphaReference) / 255.0;\n"; + shader += "if (!("; switch (function) { - case CompareFunction::Never: shader += "true"; break; - case CompareFunction::Always: shader += "false"; break; + case CompareFunction::Never: shader += "false"; break; + case CompareFunction::Always: shader += "true"; break; + case CompareFunction::Equal: shader += "combinerOutput.a == alphaReferenceFloat"; break; + case CompareFunction::NotEqual: shader += "combinerOutput.a != alphaReferenceFloat"; break; + case CompareFunction::Less: shader += "combinerOutput.a < alphaReferenceFloat"; break; + case CompareFunction::LessOrEqual: shader += "combinerOutput.a <= alphaReferenceFloat"; break; + case CompareFunction::Greater: shader += "combinerOutput.a > alphaReferenceFloat"; break; + case CompareFunction::GreaterOrEqual: shader += "combinerOutput.a >= alphaReferenceFloat"; break; + default: Helpers::warn("Unimplemented alpha test function"); shader += "false"; break; } - shader += ") { discard; }\n"; + shader += ")) { discard; }\n"; } From 11c927932978ea157ed3b7e851908f918733b036 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:29:49 +0300 Subject: [PATCH 064/251] Properly flush shader cache --- src/core/PICA/shader_gen_glsl.cpp | 6 ++-- src/core/renderer_gl/renderer_gl.cpp | 10 ++++++ third_party/opengl/opengl.hpp | 51 ++++++++++++++++------------ 3 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 11030848..6e682354 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -140,9 +140,9 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { float alphaOp3 = 0.0; )"; + // Get original depth value by converting from [near, far] = [0, 1] to [-1, 1] + // We do this by converting to [0, 2] first and subtracting 1 to go to [-1, 1] ret += R"( - // Get original depth value by converting from [near, far] = [0, 1] to [-1, 1] - // We do this by converting to [0, 2] first and subtracting 1 to go to [-1, 1] float z_over_w = gl_FragCoord.z * 2.0f - 1.0f; float depth = z_over_w * depthScale + depthOffset; )"; @@ -343,7 +343,7 @@ void FragmentGenerator::getSource(std::string& shader, TexEnvConfig::Source sour // Lighting case TexEnvConfig::Source::PrimaryFragmentColor: - case TexEnvConfig::Source::SecondaryFragmentColor: shader += "vec4(0.0, 0.0, 0.0, 1.0)"; break; + case TexEnvConfig::Source::SecondaryFragmentColor: shader += "vec4(1.0, 1.0, 1.0, 1.0)"; break; default: Helpers::warn("Unimplemented TEV source: %d", static_cast(source)); diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 9c60ac5f..d0e2bb31 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -23,6 +23,11 @@ void RendererGL::reset() { colourBufferCache.reset(); textureCache.reset(); + for (auto& shader : shaderCache) { + shader.second.program.free(); + } + shaderCache.clear(); + // Init the colour/depth buffer settings to some random defaults on reset colourBufferLoc = 0; colourBufferFormat = PICA::ColorFmt::RGBA8; @@ -899,6 +904,11 @@ void RendererGL::deinitGraphicsContext() { depthBufferCache.reset(); colourBufferCache.reset(); + for (auto& shader : shaderCache) { + shader.second.program.free(); + } + shaderCache.clear(); + // All other GL objects should be invalidated automatically and be recreated by the next call to initGraphicsContext // TODO: Make it so that depth and colour buffers get written back to 3DS memory printf("RendererGL::DeinitGraphicsContext called\n"); diff --git a/third_party/opengl/opengl.hpp b/third_party/opengl/opengl.hpp index 9997e63b..828fb784 100644 --- a/third_party/opengl/opengl.hpp +++ b/third_party/opengl/opengl.hpp @@ -397,34 +397,41 @@ namespace OpenGL { }; struct Program { - GLuint m_handle = 0; + GLuint m_handle = 0; - bool create(std::initializer_list> shaders) { - m_handle = glCreateProgram(); - for (const auto& shader : shaders) { - glAttachShader(m_handle, shader.get().handle()); - } + bool create(std::initializer_list> shaders) { + m_handle = glCreateProgram(); + for (const auto& shader : shaders) { + glAttachShader(m_handle, shader.get().handle()); + } - glLinkProgram(m_handle); - GLint success; - glGetProgramiv(m_handle, GL_LINK_STATUS, &success); + glLinkProgram(m_handle); + GLint success; + glGetProgramiv(m_handle, GL_LINK_STATUS, &success); - if (!success) { - char buf[4096]; - glGetProgramInfoLog(m_handle, 4096, nullptr, buf); - fprintf(stderr, "Failed to link program\nError: %s\n", buf); - glDeleteProgram(m_handle); + if (!success) { + char buf[4096]; + glGetProgramInfoLog(m_handle, 4096, nullptr, buf); + fprintf(stderr, "Failed to link program\nError: %s\n", buf); + glDeleteProgram(m_handle); - m_handle = 0; - } + m_handle = 0; + } - return m_handle != 0; - } + return m_handle != 0; + } - GLuint handle() const { return m_handle; } - bool exists() const { return m_handle != 0; } - void use() const { glUseProgram(m_handle); } - }; + GLuint handle() const { return m_handle; } + bool exists() const { return m_handle != 0; } + void use() const { glUseProgram(m_handle); } + + void free() { + if (exists()) { + glDeleteProgram(m_handle); + m_handle = 0; + } + } + }; static void dispatchCompute(GLuint groupsX = 1, GLuint groupsY = 1, GLuint groupsZ = 1) { glDispatchCompute(groupsX, groupsY, groupsZ); From c535ae43eed9898c065140cdc2bb00a9ad31ca32 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 15 Jul 2024 14:34:59 +0300 Subject: [PATCH 065/251] Shader recompiler: Fix dot3 RGBA --- src/core/PICA/shader_gen_glsl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 6e682354..56cdd936 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -194,7 +194,7 @@ void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICAReg if (tev.colorOp == TexEnvConfig::Operation::Dot3RGBA) { // Dot3 RGBA also writes to the alpha component so we don't need to do anything more - shader += "float outputAlpha" + std::to_string(stage) + " = colorOutput" + std::to_string(stage) + ".x;\n"; + shader += "float outputAlpha" + std::to_string(stage) + " = outputColor" + std::to_string(stage) + ".x;\n"; } else { // Get alpha operands shader += "alphaOp1 = "; From 2cd50e7f376e2b7c0594dd382bedd271d1253bc8 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:11:23 +0300 Subject: [PATCH 066/251] Clean up ubershader code --- include/renderer_gl/renderer_gl.hpp | 2 +- src/core/renderer_gl/renderer_gl.cpp | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index a028bdd3..55a730ec 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -84,7 +84,7 @@ class RendererGL final : public Renderer { void setupBlending(); void setupStencilTest(bool stencilEnable); void bindDepthBuffer(); - void setupTextureEnvState(); + void setupUbershaderTexEnv(); void bindTexturesToSlots(); void updateLightingLUT(); void initGraphicsContextInternal(); diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index d0e2bb31..207bfbe4 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -282,12 +282,8 @@ void RendererGL::setupStencilTest(bool stencilEnable) { glStencilOp(stencilOps[stencilFailOp], stencilOps[depthFailOp], stencilOps[passOp]); } -void RendererGL::setupTextureEnvState() { +void RendererGL::setupUbershaderTexEnv() { // TODO: Only update uniforms when the TEV config changed. Use an UBO potentially. - if (!usingUbershader) { - return; - } - static constexpr std::array ioBases = { PICA::InternalRegs::TexEnv0Source, PICA::InternalRegs::TexEnv1Source, PICA::InternalRegs::TexEnv2Source, PICA::InternalRegs::TexEnv3Source, PICA::InternalRegs::TexEnv4Source, PICA::InternalRegs::TexEnv5Source, @@ -439,9 +435,9 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v // Upload PICA Registers as a single uniform. The shader needs access to the rasterizer registers (for depth, starting from index 0x48) // The texturing and the fragment lighting registers. Therefore we upload them all in one go to avoid multiple slow uniform updates glUniform1uiv(ubershaderData.picaRegLoc, 0x200 - 0x48, ®s[0x48]); + setupUbershaderTexEnv(); } - setupTextureEnvState(); bindTexturesToSlots(); if (gpu.lightingLUTDirty) { @@ -811,7 +807,6 @@ OpenGL::Program& RendererGL::getSpecializedShader() { if (!program.exists()) { std::string vs = fragShaderGen.getVertexShader(regs); std::string fs = fragShaderGen.generate(regs); - std::cout << vs << "\n\n" << fs << "\n"; OpenGL::Shader vertShader({vs.c_str(), vs.size()}, OpenGL::Vertex); OpenGL::Shader fragShader({fs.c_str(), fs.size()}, OpenGL::Fragment); From a2649ffb76879408a174ac817bf875463a8bbcc3 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 15 Jul 2024 15:38:15 +0300 Subject: [PATCH 067/251] Simplify TEV code --- src/core/PICA/shader_gen_glsl.cpp | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 56cdd936..80dbf1ef 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -57,7 +57,6 @@ std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { vec4 colourAbs = abs(a_vertexColour); v_colour = min(colourAbs, vec4(1.f)); - // Flip y axis of UVs because OpenGL uses an inverted y for texture sampling compared to the PICA v_texcoord0 = vec3(a_texcoord0.x, 1.0 - a_texcoord0.y, a_texcoord0_w); v_texcoord1 = vec2(a_texcoord1.x, 1.0 - a_texcoord1.y); v_texcoord2 = vec2(a_texcoord2.x, 1.0 - a_texcoord2.y); @@ -125,8 +124,8 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { // When not initialized, source 13 is set to vec4(0.0) and 15 is set to the vertex colour ret += R"( void main() { - vec4 combinerOutput = v_colour; // Last TEV output - vec4 previousBuffer = vec4(0.0); // Previous buffer + vec4 combinerOutput = v_colour; + vec4 previousBuffer = vec4(0.0); vec4 tevNextPreviousBuffer = tevBufferColor; )"; @@ -162,7 +161,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { ret += "fragColor = combinerOutput;\n"; ret += "}"; // End of main function - ret += "\n\n\n\n\n\n\n\n\n\n"; + ret += "\n\n\n\n\n\n\n"; return ret; } @@ -188,9 +187,9 @@ void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICAReg shader += ";\ncolorOp3 = "; getColorOperand(shader, tev.colorSource3, tev.colorOperand3, stage); - shader += ";\nvec3 outputColor" + std::to_string(stage) + " = "; + shader += ";\nvec3 outputColor" + std::to_string(stage) + " = clamp("; getColorOperation(shader, tev.colorOp); - shader += ";\n"; + shader += ", vec3(0.0), vec3(1.0));\n"; if (tev.colorOp == TexEnvConfig::Operation::Dot3RGBA) { // Dot3 RGBA also writes to the alpha component so we don't need to do anything more @@ -206,10 +205,10 @@ void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICAReg shader += ";\nalphaOp3 = "; getAlphaOperand(shader, tev.alphaSource3, tev.alphaOperand3, stage); - shader += ";\nfloat outputAlpha" + std::to_string(stage) + " = "; + shader += ";\nfloat outputAlpha" + std::to_string(stage) + " = clamp("; getAlphaOperation(shader, tev.alphaOp); // Clamp the alpha value to [0.0, 1.0] - shader += ";\nclamp(outputAlpha" + std::to_string(stage) + ", 0.0, 1.0);\n"; + shader += ", 0.0, 1.0);\n"; } shader += "combinerOutput = vec4(clamp(outputColor" + std::to_string(stage) + " * " + std::to_string(tev.getColorScale()) + @@ -356,15 +355,15 @@ void FragmentGenerator::getColorOperation(std::string& shader, TexEnvConfig::Ope switch (op) { case TexEnvConfig::Operation::Replace: shader += "colorOp1"; break; case TexEnvConfig::Operation::Add: shader += "colorOp1 + colorOp2"; break; - case TexEnvConfig::Operation::AddSigned: shader += "clamp(colorOp1 + colorOp2 - 0.5, 0.0, 1.0);"; break; + case TexEnvConfig::Operation::AddSigned: shader += "colorOp1 + colorOp2 - vec3(0.5)"; break; case TexEnvConfig::Operation::Subtract: shader += "colorOp1 - colorOp2"; break; case TexEnvConfig::Operation::Modulate: shader += "colorOp1 * colorOp2"; break; - case TexEnvConfig::Operation::Lerp: shader += "colorOp1 * colorOp3 + colorOp2 * (vec3(1.0) - colorOp3)"; break; + case TexEnvConfig::Operation::Lerp: shader += "mix(colorOp2, colorOp1, colorOp3)"; break; - case TexEnvConfig::Operation::AddMultiply: shader += "min(colorOp1 + colorOp2, vec3(1.0)) * colorOp3"; break; - case TexEnvConfig::Operation::MultiplyAdd: shader += "colorOp1 * colorOp2 + colorOp3"; break; + case TexEnvConfig::Operation::AddMultiply: shader += "min(colorOp1 + colorOp2), vec3(1.0)) * colorOp3"; break; + case TexEnvConfig::Operation::MultiplyAdd: shader += "fma(colorOp1, colorOp2, colorOp3)"; break; case TexEnvConfig::Operation::Dot3RGB: - case TexEnvConfig::Operation::Dot3RGBA: shader += "vec3(4.0 * dot(colorOp1 - 0.5, colorOp2 - 0.5))"; break; + case TexEnvConfig::Operation::Dot3RGBA: shader += "vec3(4.0 * dot(colorOp1 - vec3(0.5), colorOp2 - vec3(0.5)))"; break; default: Helpers::warn("FragmentGenerator: Unimplemented color op"); shader += "vec3(1.0)"; @@ -376,13 +375,13 @@ void FragmentGenerator::getAlphaOperation(std::string& shader, TexEnvConfig::Ope switch (op) { case TexEnvConfig::Operation::Replace: shader += "alphaOp1"; break; case TexEnvConfig::Operation::Add: shader += "alphaOp1 + alphaOp2"; break; - case TexEnvConfig::Operation::AddSigned: shader += "clamp(alphaOp1 + alphaOp2 - 0.5, 0.0, 1.0);"; break; + case TexEnvConfig::Operation::AddSigned: shader += "alphaOp1 + alphaOp2 - 0.5"; break; case TexEnvConfig::Operation::Subtract: shader += "alphaOp1 - alphaOp2"; break; case TexEnvConfig::Operation::Modulate: shader += "alphaOp1 * alphaOp2"; break; - case TexEnvConfig::Operation::Lerp: shader += "alphaOp1 * alphaOp3 + alphaOp2 * (1.0 - alphaOp3)"; break; + case TexEnvConfig::Operation::Lerp: shader += "mix(alphaOp2, alphaOp1, alphaOp3)"; break; case TexEnvConfig::Operation::AddMultiply: shader += "min(alphaOp1 + alphaOp2, 1.0) * alphaOp3"; break; - case TexEnvConfig::Operation::MultiplyAdd: shader += "alphaOp1 * alphaOp2 + alphaOp3"; break; + case TexEnvConfig::Operation::MultiplyAdd: shader += "fma(alphaOp1, alphaOp2, alphaOp3)"; break; case TexEnvConfig::Operation::Dot3RGB: case TexEnvConfig::Operation::Dot3RGBA: shader += "vec3(4.0 * dot(alphaOp1 - 0.5, alphaOp2 - 0.5))"; break; default: From b8a186d5cd9a2a2db2f61c7ce38355cc22eb28ba Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:34:33 +0300 Subject: [PATCH 068/251] Shadergen: Fix add-multiply --- src/core/PICA/shader_gen_glsl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 80dbf1ef..86594023 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -360,7 +360,7 @@ void FragmentGenerator::getColorOperation(std::string& shader, TexEnvConfig::Ope case TexEnvConfig::Operation::Modulate: shader += "colorOp1 * colorOp2"; break; case TexEnvConfig::Operation::Lerp: shader += "mix(colorOp2, colorOp1, colorOp3)"; break; - case TexEnvConfig::Operation::AddMultiply: shader += "min(colorOp1 + colorOp2), vec3(1.0)) * colorOp3"; break; + case TexEnvConfig::Operation::AddMultiply: shader += "min(colorOp1 + colorOp2, vec3(1.0)) * colorOp3"; break; case TexEnvConfig::Operation::MultiplyAdd: shader += "fma(colorOp1, colorOp2, colorOp3)"; break; case TexEnvConfig::Operation::Dot3RGB: case TexEnvConfig::Operation::Dot3RGBA: shader += "vec3(4.0 * dot(colorOp1 - vec3(0.5), colorOp2 - vec3(0.5)))"; break; From db801312134cb9654e6afd932de1784af13d0081 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 15 Jul 2024 18:27:22 +0300 Subject: [PATCH 069/251] Shadergen: Previous buffer should be able to be set even for passthrough TEV stages --- src/core/PICA/shader_gen_glsl.cpp | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 86594023..556c0794 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -214,21 +214,21 @@ void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICAReg shader += "combinerOutput = vec4(clamp(outputColor" + std::to_string(stage) + " * " + std::to_string(tev.getColorScale()) + ".0, vec3(0.0), vec3(1.0)), clamp(outputAlpha" + std::to_string(stage) + " * " + std::to_string(tev.getAlphaScale()) + ".0, 0.0, 1.0));\n"; + } - shader += "previousBuffer = tevNextPreviousBuffer;\n"; + shader += "previousBuffer = tevNextPreviousBuffer;\n\n"; - // Update the "next previous buffer" if necessary - const u32 textureEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; - if (stage < 4) { - // Check whether to update rgb - if ((textureEnvUpdateBuffer & (0x100 << stage))) { - shader += "tevNextPreviousBuffer.rgb = combinerOutput.rgb;\n"; - } + // Update the "next previous buffer" if necessary + const u32 textureEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; + if (stage < 4) { + // Check whether to update rgb + if ((textureEnvUpdateBuffer & (0x100 << stage))) { + shader += "tevNextPreviousBuffer.rgb = combinerOutput.rgb;\n"; + } - // And whether to update alpha - if ((textureEnvUpdateBuffer & (0x1000u << stage))) { - shader += "tevNextPreviousBuffer.a = combinerOutput.a;\n"; - } + // And whether to update alpha + if ((textureEnvUpdateBuffer & (0x1000u << stage))) { + shader += "tevNextPreviousBuffer.a = combinerOutput.a;\n"; } } } @@ -382,8 +382,6 @@ void FragmentGenerator::getAlphaOperation(std::string& shader, TexEnvConfig::Ope case TexEnvConfig::Operation::AddMultiply: shader += "min(alphaOp1 + alphaOp2, 1.0) * alphaOp3"; break; case TexEnvConfig::Operation::MultiplyAdd: shader += "fma(alphaOp1, alphaOp2, alphaOp3)"; break; - case TexEnvConfig::Operation::Dot3RGB: - case TexEnvConfig::Operation::Dot3RGBA: shader += "vec3(4.0 * dot(alphaOp1 - 0.5, alphaOp2 - 0.5))"; break; default: Helpers::warn("FragmentGenerator: Unimplemented alpha op"); shader += "1.0"; From 9b4e5841e7154563a4dda0153c87a46250f46543 Mon Sep 17 00:00:00 2001 From: offtkp Date: Sun, 14 Jul 2024 00:56:55 +0300 Subject: [PATCH 070/251] Summary of the current state of lighting fragment_light.elf: works toon_shading.elf: works Cave story 3d: no longer too dark, but the intro has a bug Rabbids: positional lighting fixes, looks better Mario 3d land: ground is not too bright, mario is not yellow Kirby triple deluxe: Kirby is not shining like before Luigis mansion: better but luigi lighting is way off and spotlight sometimes turns off Captain Toad: bit better, still too bright Omega ruby: looks fine to me Pokemon Super Mystery Dungeon: looks fine to me Lego batman: didn't try but should work? --- src/host_shaders/opengl_fragment_shader.frag | 347 +++++++++++++------ 1 file changed, 244 insertions(+), 103 deletions(-) diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index 6b728ace..1b8e9751 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -38,6 +38,21 @@ vec4 tevSources[16]; vec4 tevNextPreviousBuffer; bool tevUnimplementedSourceFlag = false; +// Holds the enabled state of the lighting samples for various PICA configurations +// As explained in https://www.3dbrew.org/wiki/GPU/Internal_Registers#GPUREG_LIGHTING_CONFIG0 +const bool samplerEnabled[9 * 7] = bool[9 * 7]( + // D0 D1 SP FR RB RG RR + true, false, true, false, false, false, true, // Configuration 0: D0, SP, RR + false, false, true, true, false, false, true, // Configuration 1: FR, SP, RR + true, true, false, false, false, false, true, // Configuration 2: D0, D1, RR + true, true, false, true, false, false, false, // Configuration 3: D0, D1, FR + true, true, true, false, true, true, true, // Configuration 4: All except for FR + true, false, true, true, true, true, true, // Configuration 5: All except for D1 + true, true, true, true, false, false, true, // Configuration 6: All except for RB and RG + false, false, false, false, false, false, false, // Configuration 7: Unused + true, true, true, true, true, true, true // Configuration 8: All +); + // OpenGL ES 1.1 reference pages for TEVs (this is what the PICA200 implements): // https://registry.khronos.org/OpenGL-Refpages/es1.1/xhtml/glTexEnv.xml @@ -144,10 +159,16 @@ vec4 tevCalculateCombiner(int tev_id) { #define RG_LUT 5u #define RR_LUT 6u -float lutLookup(uint lut, uint light, float value) { - if (lut >= FR_LUT && lut <= RR_LUT) lut -= 1u; - if (lut == SP_LUT) lut = light + 8u; - return texelFetch(u_tex_lighting_lut, ivec2(int(value * 256.0), lut), 0).r; +uint GPUREG_LIGHTi_CONFIG; +uint GPUREG_LIGHTING_CONFIG1; +uint GPUREG_LIGHTING_LUTINPUT_SELECT; +uint GPUREG_LIGHTING_LUTINPUT_SCALE; +uint GPUREG_LIGHTING_LUTINPUT_ABS; +bool error_unimpl; +vec4 unimpl_color; + +float lutLookup(uint lut, int index) { + return texelFetch(u_tex_lighting_lut, ivec2(index, lut), 0).r; } vec3 regToColor(uint reg) { @@ -178,42 +199,155 @@ float decodeFP(uint hex, uint E, uint M) { return uintBitsToFloat(hex); } +bool isSamplerEnabled(uint environment_id, uint lut_id) { + return samplerEnabled[7 * environment_id + lut_id]; +} + +float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light_vector, vec3 half_vector) { + uint lut_index; + // lut_id is one of these values + // 0 D0 + // 1 D1 + // 2 SP + // 3 FR + // 4 RB + // 5 RG + // 6 RR + + // lut_index on the other hand represents the actual index of the LUT in the texture + // u_tex_lighting_lut has 24 LUTs and they are used like so: + // 0 D0 + // 1 D1 + // 2 is missing because SP uses LUTs 8-15 + // 3 FR + // 4 RB + // 5 RG + // 6 RR + // 8-15 SP0-7 + // 16-23 DA0-7, but this is not handled in this function as the lookup is a bit different + + int bit_in_config1; + if (lut_id == SP_LUT) { + // These are the spotlight attenuation LUTs + bit_in_config1 = 8 + int(light_id & 7u); + lut_index = 8u + light_id; + } else if (lut_id <= 6) { + bit_in_config1 = 16 + int(lut_id); + lut_index = lut_id; + } else { + error_unimpl = true; + } + + // The light environment configuration controls which LUTs are available for use + // If a LUT is not available in the selected configuration, its value will always read a constant 1.0 regardless of the enable state in GPUREG_LIGHTING_CONFIG1 + // If RR is enabled but not RG or RB, the output of RR is used for the three components; Red, Green and Blue. + bool current_sampler_enabled = isSamplerEnabled(environment_id, lut_id); // 7 luts per environment + + if (!current_sampler_enabled || (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, bit_in_config1, 1) != 0u)) { + return 1.0; + } + + uint scale_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SCALE, int(lut_id) * 4, 3); + float scale = float(1u << scale_id); + if (scale_id >= 6u) scale /= 256.0; + + float delta = 1.0; + uint input_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SELECT, int(lut_id) * 4, 3); + switch (input_id) { + case 0u: { + delta = dot(v_normal, normalize(half_vector)); + break; + } + case 1u: { + delta = dot(normalize(v_view), normalize(half_vector)); + break; + } + case 2u: { + delta = dot(v_normal, normalize(v_view)); + break; + } + case 3u: { + delta = dot(light_vector, v_normal); + break; + } + case 4u: { + // These are ints so that bitfieldExtract sign extends for us + int GPUREG_LIGHTi_SPOTDIR_LOW = int(readPicaReg(0x0146u + 0x10u * light_id)); + int GPUREG_LIGHTi_SPOTDIR_HIGH = int(readPicaReg(0x0147u + 0x10u * light_id)); + + // These are fixed point 1.1.11 values, so we need to convert them to float + float x = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13)) / 2047.0; + float y = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13)) / 2047.0; + float z = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13)) / 2047.0; + vec3 spotlight_vector = vec3(x, y, z); + delta = dot(light_vector, spotlight_vector); // spotlight direction is negated so we don't negate light_vector + break; + } + case 5u: { + delta = 1.0; // TODO: cos (aka CP); + error_unimpl = true; + break; + } + default: { + delta = 1.0; + error_unimpl = true; + break; + } + } + + // 0 = enabled + if (bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_ABS, 1 + 4 * int(lut_id), 1) == 0u) { + // Two sided diffuse + if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) { + delta = max(delta, 0.0); + } else { + delta = abs(delta); + } + int index = int(clamp(floor(delta * 256.0), 0.f, 255.f)); + return lutLookup(lut_index, index) * scale; + } else { + // Range is [-1, 1] so we need to map it to [0, 1] + int index = int(clamp(floor(delta * 128.0), -128.f, 127.f)); + if (index < 0) index += 256; + return lutLookup(lut_index, index) * scale; + } +} + // Implements the following algorthm: https://mathb.in/26766 void calcLighting(out vec4 primary_color, out vec4 secondary_color) { - // Quaternions describe a transformation from surface-local space to eye space. - // In surface-local space, by definition (and up to permutation) the normal vector is (0,0,1), - // the tangent vector is (1,0,0), and the bitangent vector is (0,1,0). - vec3 normal = normalize(v_normal); - vec3 tangent = normalize(v_tangent); - vec3 bitangent = normalize(v_bitangent); - vec3 view = normalize(v_view); + error_unimpl = false; + unimpl_color = vec4(1.0, 0.0, 1.0, 1.0); uint GPUREG_LIGHTING_ENABLE = readPicaReg(0x008Fu); if (bitfieldExtract(GPUREG_LIGHTING_ENABLE, 0, 1) == 0u) { - primary_color = secondary_color = vec4(1.0); + primary_color = secondary_color = vec4(0.0); return; } - uint GPUREG_LIGHTING_AMBIENT = readPicaReg(0x01C0u); uint GPUREG_LIGHTING_NUM_LIGHTS = (readPicaReg(0x01C2u) & 0x7u) + 1u; uint GPUREG_LIGHTING_LIGHT_PERMUTATION = readPicaReg(0x01D9u); primary_color = vec4(vec3(0.0), 1.0); secondary_color = vec4(vec3(0.0), 1.0); - primary_color.rgb += regToColor(GPUREG_LIGHTING_AMBIENT); - - uint GPUREG_LIGHTING_LUTINPUT_ABS = readPicaReg(0x01D0u); - uint GPUREG_LIGHTING_LUTINPUT_SELECT = readPicaReg(0x01D1u); - uint GPUREG_LIGHTING_CONFIG0 = readPicaReg(0x01C3u); - uint GPUREG_LIGHTING_CONFIG1 = readPicaReg(0x01C4u); uint GPUREG_LIGHTING_LUTINPUT_SCALE = readPicaReg(0x01D2u); - float d[7]; + uint GPUREG_LIGHTING_CONFIG0 = readPicaReg(0x01C3u); + GPUREG_LIGHTING_CONFIG1 = readPicaReg(0x01C4u); + GPUREG_LIGHTING_LUTINPUT_ABS = readPicaReg(0x01D0u); + GPUREG_LIGHTING_LUTINPUT_SELECT = readPicaReg(0x01D1u); - bool error_unimpl = false; + vec4 diffuse_sum = vec4(0.0, 0.0, 0.0, 1.0); + vec4 specular_sum = vec4(0.0, 0.0, 0.0, 1.0); + + uint environment_id = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 4, 4); + bool clamp_highlights = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 27, 1) == 1u; + + uint light_id; + vec3 light_vector; + vec3 half_vector; for (uint i = 0u; i < GPUREG_LIGHTING_NUM_LIGHTS; i++) { - uint light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i * 3u), 3); + light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i * 3u), 3); uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140u + 0x10u * light_id); uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141u + 0x10u * light_id); @@ -221,93 +355,29 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { uint GPUREG_LIGHTi_AMBIENT = readPicaReg(0x0143u + 0x10u * light_id); uint GPUREG_LIGHTi_VECTOR_LOW = readPicaReg(0x0144u + 0x10u * light_id); uint GPUREG_LIGHTi_VECTOR_HIGH = readPicaReg(0x0145u + 0x10u * light_id); - uint GPUREG_LIGHTi_CONFIG = readPicaReg(0x0149u + 0x10u * light_id); + GPUREG_LIGHTi_CONFIG = readPicaReg(0x0149u + 0x10u * light_id); - vec3 light_vector = normalize(vec3( + float light_distance; + vec3 light_position = vec3( decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_LOW, 0, 16), 5u, 10u), decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_LOW, 16, 16), 5u, 10u), decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_HIGH, 0, 16), 5u, 10u) - )); - - vec3 half_vector; + ); // Positional Light if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 0, 1) == 0u) { - // error_unimpl = true; - half_vector = normalize(normalize(light_vector + v_view) + view); + light_vector = light_position + v_view; } // Directional light else { - half_vector = normalize(normalize(light_vector) + view); + light_vector = light_position; } - for (int c = 0; c < 7; c++) { - if (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, 16 + c, 1) == 0u) { - uint scale_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SCALE, c * 4, 3); - float scale = float(1u << scale_id); - if (scale_id >= 6u) scale /= 256.0; + light_distance = length(light_vector); + light_vector = normalize(light_vector); + half_vector = light_vector + normalize(v_view); - uint input_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SELECT, c * 4, 3); - if (input_id == 0u) - d[c] = dot(normal, half_vector); - else if (input_id == 1u) - d[c] = dot(view, half_vector); - else if (input_id == 2u) - d[c] = dot(normal, view); - else if (input_id == 3u) - d[c] = dot(light_vector, normal); - else if (input_id == 4u) { - uint GPUREG_LIGHTi_SPOTDIR_LOW = readPicaReg(0x0146u + 0x10u * light_id); - uint GPUREG_LIGHTi_SPOTDIR_HIGH = readPicaReg(0x0147u + 0x10u * light_id); - vec3 spot_light_vector = normalize(vec3( - decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 16), 1u, 11u), - decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 16), 1u, 11u), - decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 16), 1u, 11u) - )); - d[c] = dot(-light_vector, spot_light_vector); // -L dot P (aka Spotlight aka SP); - } else if (input_id == 5u) { - d[c] = 1.0; // TODO: cos (aka CP); - error_unimpl = true; - } else { - d[c] = 1.0; - } - - d[c] = lutLookup(uint(c), light_id, d[c] * 0.5 + 0.5) * scale; - if (bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_ABS, 2 * c, 1) != 0u) d[c] = abs(d[c]); - } else { - d[c] = 1.0; - } - } - - uint lookup_config = bitfieldExtract(GPUREG_LIGHTi_CONFIG, 4, 4); - if (lookup_config == 0u) { - d[D1_LUT] = 0.0; - d[FR_LUT] = 0.0; - d[RG_LUT] = d[RB_LUT] = d[RR_LUT]; - } else if (lookup_config == 1u) { - d[D0_LUT] = 0.0; - d[D1_LUT] = 0.0; - d[RG_LUT] = d[RB_LUT] = d[RR_LUT]; - } else if (lookup_config == 2u) { - d[FR_LUT] = 0.0; - d[SP_LUT] = 0.0; - d[RG_LUT] = d[RB_LUT] = d[RR_LUT]; - } else if (lookup_config == 3u) { - d[SP_LUT] = 0.0; - d[RG_LUT] = d[RB_LUT] = d[RR_LUT] = 1.0; - } else if (lookup_config == 4u) { - d[FR_LUT] = 0.0; - } else if (lookup_config == 5u) { - d[D1_LUT] = 0.0; - } else if (lookup_config == 6u) { - d[RG_LUT] = d[RB_LUT] = d[RR_LUT]; - } - - float distance_factor = 1.0; // a - float indirect_factor = 1.0; // fi - float shadow_factor = 1.0; // o - - float NdotL = dot(normal, light_vector); // Li dot N + float NdotL = dot(v_normal, light_vector); // N dot Li // Two sided diffuse if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) @@ -315,20 +385,91 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { else NdotL = abs(NdotL); - float light_factor = distance_factor * d[SP_LUT] * indirect_factor * shadow_factor; + float geometric_factor; + bool use_geo_0 = bitfieldExtract(GPUREG_LIGHTi_CONFIG, 2, 1) == 1u; + bool use_geo_1 = bitfieldExtract(GPUREG_LIGHTi_CONFIG, 3, 1) == 1u; + if (use_geo_0 || use_geo_1) { + geometric_factor = dot(half_vector, half_vector); + geometric_factor = geometric_factor == 0.0 ? 0.0 : min(NdotL / geometric_factor, 1.0); + } - primary_color.rgb += light_factor * (regToColor(GPUREG_LIGHTi_AMBIENT) + regToColor(GPUREG_LIGHTi_DIFFUSE) * NdotL); - secondary_color.rgb += light_factor * (regToColor(GPUREG_LIGHTi_SPECULAR0) * d[D0_LUT] + - regToColor(GPUREG_LIGHTi_SPECULAR1) * d[D1_LUT] * vec3(d[RR_LUT], d[RG_LUT], d[RB_LUT])); + // Distance attenuation is computed differently from the other factors, for example + // it doesn't store its scale in GPUREG_LIGHTING_LUTINPUT_SCALE and it doesn't use + // GPUREG_LIGHTING_LUTINPUT_SELECT. Instead, it uses the distance from the light to the + // fragment and the distance attenuation scale and bias to calculate where in the LUT to look up. + // See: https://www.3dbrew.org/wiki/GPU/Internal_Registers#GPUREG_LIGHTi_ATTENUATION_SCALE + float distance_attenuation = 1.0; + if (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, 24 + int(light_id), 1) == 0u) { + uint GPUREG_LIGHTi_ATTENUATION_BIAS = bitfieldExtract(readPicaReg(0x014Au), 0, 20); + uint GPUREG_LIGHTi_ATTENUATION_SCALE = bitfieldExtract(readPicaReg(0x014Bu), 0, 20); + + float distance_attenuation_bias = decodeFP(GPUREG_LIGHTi_ATTENUATION_BIAS, 7u, 12u); + float distance_attenuation_scale = decodeFP(GPUREG_LIGHTi_ATTENUATION_SCALE, 7u, 12u); + + float delta = light_distance * distance_attenuation_scale + distance_attenuation_bias; + delta = clamp(delta, 0.0, 1.0); + int index = int(clamp(floor(delta * 255.0), 0.0, 255.0)); + distance_attenuation = lutLookup(16u + light_id, index); + } + + float spotlight_attenuation = lightLutLookup(environment_id, SP_LUT, light_id, light_vector, half_vector); + float specular0_distribution = lightLutLookup(environment_id, D0_LUT, light_id, light_vector, half_vector); + float specular1_distribution = lightLutLookup(environment_id, D1_LUT, light_id, light_vector, half_vector); + vec3 reflected_color; + reflected_color.r = lightLutLookup(environment_id, RR_LUT, light_id, light_vector, half_vector); + + if (isSamplerEnabled(environment_id, RG_LUT)) { + reflected_color.g = lightLutLookup(environment_id, RG_LUT, light_id, light_vector, half_vector); + } else { + reflected_color.g = reflected_color.r; + } + + if (isSamplerEnabled(environment_id, RB_LUT)) { + reflected_color.b = lightLutLookup(environment_id, RB_LUT, light_id, light_vector, half_vector); + } else { + reflected_color.b = reflected_color.r; + } + + vec3 specular0 = regToColor(GPUREG_LIGHTi_SPECULAR0) * specular0_distribution; + vec3 specular1 = regToColor(GPUREG_LIGHTi_SPECULAR1) * specular1_distribution * reflected_color; + + specular0 *= use_geo_0 ? geometric_factor : 1.0; + specular1 *= use_geo_1 ? geometric_factor : 1.0; + + float clamp_factor = 1.0; + if (clamp_highlights && NdotL == 0.0) { + clamp_factor = 0.0; + } + + float light_factor = distance_attenuation * spotlight_attenuation; + diffuse_sum.rgb += light_factor * (regToColor(GPUREG_LIGHTi_AMBIENT) + regToColor(GPUREG_LIGHTi_DIFFUSE) * NdotL); + specular_sum.rgb += light_factor * clamp_factor * (specular0 + specular1); } + uint fresnel_output1 = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 2, 1); uint fresnel_output2 = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 3, 1); + // Uses parameters from the last light as Fresnel is only applied to the last light + float fresnel_factor; + + if (fresnel_output1 == 1u || fresnel_output2 == 1u) { + fresnel_factor = lightLutLookup(environment_id, FR_LUT, light_id, light_vector, half_vector); + } + + if (fresnel_output1 == 1u) { + diffuse_sum.a = fresnel_factor; + } - if (fresnel_output1 == 1u) primary_color.a = d[FR_LUT]; - if (fresnel_output2 == 1u) secondary_color.a = d[FR_LUT]; + if (fresnel_output2 == 1u) { + specular_sum.a = fresnel_factor; + } + + uint GPUREG_LIGHTING_AMBIENT = readPicaReg(0x01C0u); + vec4 global_ambient = vec4(regToColor(GPUREG_LIGHTING_AMBIENT), 1.0); + primary_color = clamp(global_ambient + diffuse_sum, vec4(0.0), vec4(1.0)); + secondary_color = clamp(specular_sum, vec4(0.0), vec4(1.0)); if (error_unimpl) { - // secondary_color = primary_color = vec4(1.0, 0., 1.0, 1.0); + secondary_color = primary_color = unimpl_color; } } From f6ebf8398230928a95101de021989e6a64a36804 Mon Sep 17 00:00:00 2001 From: offtkp Date: Tue, 16 Jul 2024 00:18:53 +0300 Subject: [PATCH 071/251] Update gles.patch --- .github/gles.patch | 176 +++++++++++++++++++++++++++------------------ 1 file changed, 106 insertions(+), 70 deletions(-) diff --git a/.github/gles.patch b/.github/gles.patch index 3d6c96fe..f5270518 100644 --- a/.github/gles.patch +++ b/.github/gles.patch @@ -21,7 +21,7 @@ index 990e2f80..2e7842ac 100644 void main() { diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag -index 6b728ace..eaac1484 100644 +index 1b8e9751..96238000 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -1,4 +1,5 @@ @@ -31,8 +31,8 @@ index 6b728ace..eaac1484 100644 in vec3 v_tangent; in vec3 v_normal; -@@ -150,11 +151,17 @@ float lutLookup(uint lut, uint light, float value) { - return texelFetch(u_tex_lighting_lut, ivec2(int(value * 256.0), lut), 0).r; +@@ -171,11 +172,17 @@ float lutLookup(uint lut, int index) { + return texelFetch(u_tex_lighting_lut, ivec2(index, lut), 0).r; } +// some gles versions have bitfieldExtract and complain if you redefine it, some don't and compile error, using this instead @@ -50,89 +50,103 @@ index 6b728ace..eaac1484 100644 } // Convert an arbitrary-width floating point literal to an f32 -@@ -189,7 +196,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { - vec3 view = normalize(v_view); +@@ -243,16 +250,16 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light + // If RR is enabled but not RG or RB, the output of RR is used for the three components; Red, Green and Blue. + bool current_sampler_enabled = isSamplerEnabled(environment_id, lut_id); // 7 luts per environment + +- if (!current_sampler_enabled || (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, bit_in_config1, 1) != 0u)) { ++ if (!current_sampler_enabled || (bitfieldExtractCompat(GPUREG_LIGHTING_CONFIG1, bit_in_config1, 1) != 0u)) { + return 1.0; + } + +- uint scale_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SCALE, int(lut_id) * 4, 3); ++ uint scale_id = bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_SCALE, int(lut_id) * 4, 3); + float scale = float(1u << scale_id); + if (scale_id >= 6u) scale /= 256.0; + + float delta = 1.0; +- uint input_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SELECT, int(lut_id) * 4, 3); ++ uint input_id = bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_SELECT, int(lut_id) * 4, 3); + switch (input_id) { + case 0u: { + delta = dot(v_normal, normalize(half_vector)); +@@ -271,14 +278,14 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light + break; + } + case 4u: { +- // These are ints so that bitfieldExtract sign extends for us ++ // These are ints so that bitfieldExtractCompat sign extends for us + int GPUREG_LIGHTi_SPOTDIR_LOW = int(readPicaReg(0x0146u + 0x10u * light_id)); + int GPUREG_LIGHTi_SPOTDIR_HIGH = int(readPicaReg(0x0147u + 0x10u * light_id)); + + // These are fixed point 1.1.11 values, so we need to convert them to float +- float x = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13)) / 2047.0; +- float y = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13)) / 2047.0; +- float z = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13)) / 2047.0; ++ float x = float(bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13)) / 2047.0; ++ float y = float(bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13)) / 2047.0; ++ float z = float(bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13)) / 2047.0; + vec3 spotlight_vector = vec3(x, y, z); + delta = dot(light_vector, spotlight_vector); // spotlight direction is negated so we don't negate light_vector + break; +@@ -296,9 +303,9 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light + } + + // 0 = enabled +- if (bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_ABS, 1 + 4 * int(lut_id), 1) == 0u) { ++ if (bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_ABS, 1 + 4 * int(lut_id), 1) == 0u) { + // Two sided diffuse +- if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) { ++ if (bitfieldExtractCompat(GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) { + delta = max(delta, 0.0); + } else { + delta = abs(delta); +@@ -319,7 +326,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + unimpl_color = vec4(1.0, 0.0, 1.0, 1.0); uint GPUREG_LIGHTING_ENABLE = readPicaReg(0x008Fu); - if (bitfieldExtract(GPUREG_LIGHTING_ENABLE, 0, 1) == 0u) { + if (bitfieldExtractCompat(GPUREG_LIGHTING_ENABLE, 0, 1) == 0u) { - primary_color = secondary_color = vec4(1.0); + primary_color = secondary_color = vec4(0.0); return; } -@@ -213,7 +220,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { - bool error_unimpl = false; +@@ -339,15 +346,15 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + vec4 diffuse_sum = vec4(0.0, 0.0, 0.0, 1.0); + vec4 specular_sum = vec4(0.0, 0.0, 0.0, 1.0); + +- uint environment_id = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 4, 4); +- bool clamp_highlights = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 27, 1) == 1u; ++ uint environment_id = bitfieldExtractCompat(GPUREG_LIGHTING_CONFIG0, 4, 4); ++ bool clamp_highlights = bitfieldExtractCompat(GPUREG_LIGHTING_CONFIG0, 27, 1) == 1u; + + uint light_id; + vec3 light_vector; + vec3 half_vector; for (uint i = 0u; i < GPUREG_LIGHTING_NUM_LIGHTS; i++) { -- uint light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i * 3u), 3); -+ uint light_id = bitfieldExtractCompat(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i * 3u), 3); +- light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i * 3u), 3); ++ light_id = bitfieldExtractCompat(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i * 3u), 3); uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140u + 0x10u * light_id); uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141u + 0x10u * light_id); -@@ -224,14 +231,14 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { - uint GPUREG_LIGHTi_CONFIG = readPicaReg(0x0149u + 0x10u * light_id); +@@ -359,12 +366,12 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { - vec3 light_vector = normalize(vec3( + float light_distance; + vec3 light_position = vec3( - decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_LOW, 0, 16), 5u, 10u), decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_LOW, 16, 16), 5u, 10u), - decodeFP(bitfieldExtract(GPUREG_LIGHTi_VECTOR_HIGH, 0, 16), 5u, 10u) + decodeFP(bitfieldExtractCompat(GPUREG_LIGHTi_VECTOR_LOW, 0, 16), 5u, 10u), decodeFP(bitfieldExtractCompat(GPUREG_LIGHTi_VECTOR_LOW, 16, 16), 5u, 10u), + decodeFP(bitfieldExtractCompat(GPUREG_LIGHTi_VECTOR_HIGH, 0, 16), 5u, 10u) - )); - - vec3 half_vector; + ); // Positional Light - if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 0, 1) == 0u) { + if (bitfieldExtractCompat(GPUREG_LIGHTi_CONFIG, 0, 1) == 0u) { - // error_unimpl = true; - half_vector = normalize(normalize(light_vector + v_view) + view); - } -@@ -242,12 +249,12 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + light_vector = light_position + v_view; } - for (int c = 0; c < 7; c++) { -- if (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, 16 + c, 1) == 0u) { -- uint scale_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SCALE, c * 4, 3); -+ if (bitfieldExtractCompat(GPUREG_LIGHTING_CONFIG1, 16 + c, 1) == 0u) { -+ uint scale_id = bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_SCALE, c * 4, 3); - float scale = float(1u << scale_id); - if (scale_id >= 6u) scale /= 256.0; - -- uint input_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SELECT, c * 4, 3); -+ uint input_id = bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_SELECT, c * 4, 3); - if (input_id == 0u) - d[c] = dot(normal, half_vector); - else if (input_id == 1u) -@@ -260,9 +267,9 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { - uint GPUREG_LIGHTi_SPOTDIR_LOW = readPicaReg(0x0146u + 0x10u * light_id); - uint GPUREG_LIGHTi_SPOTDIR_HIGH = readPicaReg(0x0147u + 0x10u * light_id); - vec3 spot_light_vector = normalize(vec3( -- decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 16), 1u, 11u), -- decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 16), 1u, 11u), -- decodeFP(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 16), 1u, 11u) -+ decodeFP(bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 16), 1u, 11u), -+ decodeFP(bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 16), 1u, 11u), -+ decodeFP(bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 16), 1u, 11u) - )); - d[c] = dot(-light_vector, spot_light_vector); // -L dot P (aka Spotlight aka SP); - } else if (input_id == 5u) { -@@ -273,13 +280,13 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { - } - - d[c] = lutLookup(uint(c), light_id, d[c] * 0.5 + 0.5) * scale; -- if (bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_ABS, 2 * c, 1) != 0u) d[c] = abs(d[c]); -+ if (bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_ABS, 2 * c, 1) != 0u) d[c] = abs(d[c]); - } else { - d[c] = 1.0; - } - } - -- uint lookup_config = bitfieldExtract(GPUREG_LIGHTi_CONFIG, 4, 4); -+ uint lookup_config = bitfieldExtractCompat(GPUREG_LIGHTi_CONFIG, 4, 4); - if (lookup_config == 0u) { - d[D1_LUT] = 0.0; - d[FR_LUT] = 0.0; -@@ -310,7 +317,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { - float NdotL = dot(normal, light_vector); // Li dot N +@@ -380,14 +387,14 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + float NdotL = dot(v_normal, light_vector); // N dot Li // Two sided diffuse - if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) @@ -140,17 +154,39 @@ index 6b728ace..eaac1484 100644 NdotL = max(0.0, NdotL); else NdotL = abs(NdotL); -@@ -321,8 +328,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { - secondary_color.rgb += light_factor * (regToColor(GPUREG_LIGHTi_SPECULAR0) * d[D0_LUT] + - regToColor(GPUREG_LIGHTi_SPECULAR1) * d[D1_LUT] * vec3(d[RR_LUT], d[RG_LUT], d[RB_LUT])); + + float geometric_factor; +- bool use_geo_0 = bitfieldExtract(GPUREG_LIGHTi_CONFIG, 2, 1) == 1u; +- bool use_geo_1 = bitfieldExtract(GPUREG_LIGHTi_CONFIG, 3, 1) == 1u; ++ bool use_geo_0 = bitfieldExtractCompat(GPUREG_LIGHTi_CONFIG, 2, 1) == 1u; ++ bool use_geo_1 = bitfieldExtractCompat(GPUREG_LIGHTi_CONFIG, 3, 1) == 1u; + if (use_geo_0 || use_geo_1) { + geometric_factor = dot(half_vector, half_vector); + geometric_factor = geometric_factor == 0.0 ? 0.0 : min(NdotL / geometric_factor, 1.0); +@@ -399,9 +406,9 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + // fragment and the distance attenuation scale and bias to calculate where in the LUT to look up. + // See: https://www.3dbrew.org/wiki/GPU/Internal_Registers#GPUREG_LIGHTi_ATTENUATION_SCALE + float distance_attenuation = 1.0; +- if (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, 24 + int(light_id), 1) == 0u) { +- uint GPUREG_LIGHTi_ATTENUATION_BIAS = bitfieldExtract(readPicaReg(0x014Au), 0, 20); +- uint GPUREG_LIGHTi_ATTENUATION_SCALE = bitfieldExtract(readPicaReg(0x014Bu), 0, 20); ++ if (bitfieldExtractCompat(GPUREG_LIGHTING_CONFIG1, 24 + int(light_id), 1) == 0u) { ++ uint GPUREG_LIGHTi_ATTENUATION_BIAS = bitfieldExtractCompat(readPicaReg(0x014Au), 0, 20); ++ uint GPUREG_LIGHTi_ATTENUATION_SCALE = bitfieldExtractCompat(readPicaReg(0x014Bu), 0, 20); + + float distance_attenuation_bias = decodeFP(GPUREG_LIGHTi_ATTENUATION_BIAS, 7u, 12u); + float distance_attenuation_scale = decodeFP(GPUREG_LIGHTi_ATTENUATION_SCALE, 7u, 12u); +@@ -446,8 +453,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + specular_sum.rgb += light_factor * clamp_factor * (specular0 + specular1); } + - uint fresnel_output1 = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 2, 1); - uint fresnel_output2 = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 3, 1); + uint fresnel_output1 = bitfieldExtractCompat(GPUREG_LIGHTING_CONFIG0, 2, 1); + uint fresnel_output2 = bitfieldExtractCompat(GPUREG_LIGHTING_CONFIG0, 3, 1); - - if (fresnel_output1 == 1u) primary_color.a = d[FR_LUT]; - if (fresnel_output2 == 1u) secondary_color.a = d[FR_LUT]; + // Uses parameters from the last light as Fresnel is only applied to the last light + float fresnel_factor; + diff --git a/src/host_shaders/opengl_vertex_shader.vert b/src/host_shaders/opengl_vertex_shader.vert index a25d7a6d..7cf40398 100644 --- a/src/host_shaders/opengl_vertex_shader.vert From c02b3822623372d9598d38da4dd66f8f7e8a090c Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 16 Jul 2024 00:58:52 +0300 Subject: [PATCH 072/251] Perform alpha test with integers instead of floats --- src/core/PICA/shader_gen_glsl.cpp | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 556c0794..d4a4bf8e 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -398,17 +398,17 @@ void FragmentGenerator::applyAlphaTest(std::string& shader, const PICARegs& regs return; } - shader += "float alphaReferenceFloat = float(alphaReference) / 255.0;\n"; - shader += "if (!("; + shader += "int testingAlpha = int(combinerOutput.a * 255.0);\n"; + shader += "if ("; switch (function) { - case CompareFunction::Never: shader += "false"; break; - case CompareFunction::Always: shader += "true"; break; - case CompareFunction::Equal: shader += "combinerOutput.a == alphaReferenceFloat"; break; - case CompareFunction::NotEqual: shader += "combinerOutput.a != alphaReferenceFloat"; break; - case CompareFunction::Less: shader += "combinerOutput.a < alphaReferenceFloat"; break; - case CompareFunction::LessOrEqual: shader += "combinerOutput.a <= alphaReferenceFloat"; break; - case CompareFunction::Greater: shader += "combinerOutput.a > alphaReferenceFloat"; break; - case CompareFunction::GreaterOrEqual: shader += "combinerOutput.a >= alphaReferenceFloat"; break; + case CompareFunction::Never: shader += "true"; break; + case CompareFunction::Always: shader += "false"; break; + case CompareFunction::Equal: shader += "testingAlpha != alphaReference"; break; + case CompareFunction::NotEqual: shader += "testingAlpha == alphaReference"; break; + case CompareFunction::Less: shader += "testingAlpha >= alphaReference"; break; + case CompareFunction::LessOrEqual: shader += "testingAlpha > alphaReference"; break; + case CompareFunction::Greater: shader += "testingAlpha <= alphaReference"; break; + case CompareFunction::GreaterOrEqual: shader += "testingAlpha < alphaReference"; break; default: Helpers::warn("Unimplemented alpha test function"); @@ -416,5 +416,5 @@ void FragmentGenerator::applyAlphaTest(std::string& shader, const PICARegs& regs break; } - shader += ")) { discard; }\n"; + shader += ") { discard; }\n"; } From 441aa2346c6ebd004865a324f4e683fa814949b1 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 16 Jul 2024 02:20:37 +0300 Subject: [PATCH 073/251] Shadergen: Add clipping --- include/PICA/pica_frag_uniforms.hpp | 1 + src/core/PICA/shader_gen_glsl.cpp | 34 ++++++++++++++++++++-------- src/core/renderer_gl/renderer_gl.cpp | 7 ++++++ 3 files changed, 32 insertions(+), 10 deletions(-) diff --git a/include/PICA/pica_frag_uniforms.hpp b/include/PICA/pica_frag_uniforms.hpp index 616f1882..332acd4e 100644 --- a/include/PICA/pica_frag_uniforms.hpp +++ b/include/PICA/pica_frag_uniforms.hpp @@ -16,5 +16,6 @@ namespace PICA { alignas(16) vec4 constantColors[tevStageCount]; alignas(16) vec4 tevBufferColor; + alignas(16) vec4 clipCoords; }; } // namespace PICA \ No newline at end of file diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index d4a4bf8e..e135ac8e 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -2,6 +2,18 @@ using namespace PICA; using namespace PICA::ShaderGen; +static constexpr const char* uniformDefinition = R"( + layout(std140) uniform FragmentUniforms { + int alphaReference; + float depthScale; + float depthOffset; + + vec4 constantColors[6]; + vec4 tevBufferColor; + vec4 clipCoords; + }; +)"; + std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { std::string ret = ""; @@ -20,6 +32,8 @@ std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { )"; } + ret += uniformDefinition; + ret += R"( layout(location = 0) in vec4 a_coords; layout(location = 1) in vec4 a_quaternion; @@ -39,7 +53,9 @@ std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { out vec3 v_view; out vec2 v_texcoord2; - //out float gl_ClipDistance[2]; + #ifndef USING_GLES + out float gl_ClipDistance[2]; + #endif vec4 abgr8888ToVec4(uint abgr) { const float scale = 1.0 / 255.0; @@ -65,6 +81,11 @@ std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { v_normal = normalize(rotateVec3ByQuaternion(vec3(0.0, 0.0, 1.0), a_quaternion)); v_tangent = normalize(rotateVec3ByQuaternion(vec3(1.0, 0.0, 0.0), a_quaternion)); v_bitangent = normalize(rotateVec3ByQuaternion(vec3(0.0, 1.0, 0.0), a_quaternion)); + + #ifndef USING_GLES + gl_ClipDistance[0] = -a_coords.z; + gl_ClipDistance[1] = dot(clipCoords, a_coords); + #endif } )"; @@ -109,17 +130,10 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { #ifndef USING_GLES uniform sampler1DArray u_tex_lighting_lut; #endif - - layout(std140) uniform FragmentUniforms { - int alphaReference; - float depthScale; - float depthOffset; - - vec4 constantColors[6]; - vec4 tevBufferColor; - }; )"; + ret += uniformDefinition; + // Emit main function for fragment shader // When not initialized, source 13 is set to vec4(0.0) and 15 is set to the vertex colour ret += R"( diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 207bfbe4..97b642c7 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -845,6 +845,13 @@ OpenGL::Program& RendererGL::getSpecializedShader() { uniforms.depthScale = f24::fromRaw(regs[PICA::InternalRegs::DepthScale] & 0xffffff).toFloat32(); uniforms.depthOffset = f24::fromRaw(regs[PICA::InternalRegs::DepthOffset] & 0xffffff).toFloat32(); + if (regs[InternalRegs::ClipEnable] & 1) { + uniforms.clipCoords[0] = f24::fromRaw(regs[PICA::InternalRegs::ClipData0] & 0xffffff).toFloat32(); + uniforms.clipCoords[1] = f24::fromRaw(regs[PICA::InternalRegs::ClipData1] & 0xffffff).toFloat32(); + uniforms.clipCoords[2] = f24::fromRaw(regs[PICA::InternalRegs::ClipData2] & 0xffffff).toFloat32(); + uniforms.clipCoords[3] = f24::fromRaw(regs[PICA::InternalRegs::ClipData3] & 0xffffff).toFloat32(); + } + // Set up the constant color for the 6 TEV stages for (int i = 0; i < 6; i++) { static constexpr std::array ioBases = { From e5bed23cee4d0223639167edeaa5e17058588ab2 Mon Sep 17 00:00:00 2001 From: offtkp Date: Tue, 16 Jul 2024 15:48:34 +0300 Subject: [PATCH 074/251] Fix Luigi's flashlight in Luigi's Mansion --- src/host_shaders/opengl_fragment_shader.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index 1b8e9751..c3c7cf0b 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -347,7 +347,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { vec3 half_vector; for (uint i = 0u; i < GPUREG_LIGHTING_NUM_LIGHTS; i++) { - light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i * 3u), 3); + light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i << 2u), 3); uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140u + 0x10u * light_id); uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141u + 0x10u * light_id); From 139f35588d160928e37aa25b6de1c846d8543f59 Mon Sep 17 00:00:00 2001 From: offtkp Date: Tue, 16 Jul 2024 16:23:42 +0300 Subject: [PATCH 075/251] Switch to shifts in some places instead of multiplication --- src/host_shaders/opengl_fragment_shader.frag | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index c3c7cf0b..32f4c1ec 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -247,12 +247,12 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light return 1.0; } - uint scale_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SCALE, int(lut_id) * 4, 3); + uint scale_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SCALE, int(lut_id) << 2, 3); float scale = float(1u << scale_id); if (scale_id >= 6u) scale /= 256.0; float delta = 1.0; - uint input_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SELECT, int(lut_id) * 4, 3); + uint input_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SELECT, int(lut_id) << 2, 3); switch (input_id) { case 0u: { delta = dot(v_normal, normalize(half_vector)); @@ -296,7 +296,7 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light } // 0 = enabled - if (bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_ABS, 1 + 4 * int(lut_id), 1) == 0u) { + if (bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_ABS, 1 + (int(lut_id) << 2), 1) == 0u) { // Two sided diffuse if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) { delta = max(delta, 0.0); @@ -347,7 +347,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { vec3 half_vector; for (uint i = 0u; i < GPUREG_LIGHTING_NUM_LIGHTS; i++) { - light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i << 2u), 3); + light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i) << 2, 3); uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140u + 0x10u * light_id); uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141u + 0x10u * light_id); From 8b4eacc7b6982c0f254baa223cf3291d50e55e49 Mon Sep 17 00:00:00 2001 From: offtkp Date: Tue, 16 Jul 2024 20:32:35 +0300 Subject: [PATCH 076/251] More luigi mansion fixes --- src/host_shaders/opengl_fragment_shader.frag | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index 32f4c1ec..ae43d993 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -272,8 +272,8 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light } case 4u: { // These are ints so that bitfieldExtract sign extends for us - int GPUREG_LIGHTi_SPOTDIR_LOW = int(readPicaReg(0x0146u + 0x10u * light_id)); - int GPUREG_LIGHTi_SPOTDIR_HIGH = int(readPicaReg(0x0147u + 0x10u * light_id)); + int GPUREG_LIGHTi_SPOTDIR_LOW = int(readPicaReg(0x0146u + (light_id << 4u))); + int GPUREG_LIGHTi_SPOTDIR_HIGH = int(readPicaReg(0x0147u + (light_id << 4u))); // These are fixed point 1.1.11 values, so we need to convert them to float float x = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13)) / 2047.0; @@ -349,13 +349,13 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { for (uint i = 0u; i < GPUREG_LIGHTING_NUM_LIGHTS; i++) { light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i) << 2, 3); - uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140u + 0x10u * light_id); - uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141u + 0x10u * light_id); - uint GPUREG_LIGHTi_DIFFUSE = readPicaReg(0x0142u + 0x10u * light_id); - uint GPUREG_LIGHTi_AMBIENT = readPicaReg(0x0143u + 0x10u * light_id); - uint GPUREG_LIGHTi_VECTOR_LOW = readPicaReg(0x0144u + 0x10u * light_id); - uint GPUREG_LIGHTi_VECTOR_HIGH = readPicaReg(0x0145u + 0x10u * light_id); - GPUREG_LIGHTi_CONFIG = readPicaReg(0x0149u + 0x10u * light_id); + uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140u + (light_id << 4u)); + uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141u + (light_id << 4u)); + uint GPUREG_LIGHTi_DIFFUSE = readPicaReg(0x0142u + (light_id << 4u)); + uint GPUREG_LIGHTi_AMBIENT = readPicaReg(0x0143u + (light_id << 4u)); + uint GPUREG_LIGHTi_VECTOR_LOW = readPicaReg(0x0144u + (light_id << 4u)); + uint GPUREG_LIGHTi_VECTOR_HIGH = readPicaReg(0x0145u + (light_id << 4u)); + GPUREG_LIGHTi_CONFIG = readPicaReg(0x0149u + (light_id << 4u)); float light_distance; vec3 light_position = vec3( @@ -400,8 +400,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { // See: https://www.3dbrew.org/wiki/GPU/Internal_Registers#GPUREG_LIGHTi_ATTENUATION_SCALE float distance_attenuation = 1.0; if (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, 24 + int(light_id), 1) == 0u) { - uint GPUREG_LIGHTi_ATTENUATION_BIAS = bitfieldExtract(readPicaReg(0x014Au), 0, 20); - uint GPUREG_LIGHTi_ATTENUATION_SCALE = bitfieldExtract(readPicaReg(0x014Bu), 0, 20); + uint GPUREG_LIGHTi_ATTENUATION_BIAS = bitfieldExtract(readPicaReg(0x014Au + (light_id << 4u)), 0, 20); + uint GPUREG_LIGHTi_ATTENUATION_SCALE = bitfieldExtract(readPicaReg(0x014Bu + (light_id << 4u)), 0, 20); float distance_attenuation_bias = decodeFP(GPUREG_LIGHTi_ATTENUATION_BIAS, 7u, 12u); float distance_attenuation_scale = decodeFP(GPUREG_LIGHTi_ATTENUATION_SCALE, 7u, 12u); From 0ecdf00e643fcc220224a30c492d67c2da4dc84a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:14:01 +0300 Subject: [PATCH 077/251] Add accurate shader multiplication option --- include/PICA/dynapica/shader_rec.hpp | 7 +++++-- include/PICA/dynapica/shader_rec_emitter_arm64.hpp | 4 +++- include/PICA/dynapica/shader_rec_emitter_x64.hpp | 4 +++- include/config.hpp | 1 + src/config.cpp | 2 ++ src/core/PICA/dynapica/shader_rec.cpp | 2 +- src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp | 3 --- src/core/PICA/dynapica/shader_rec_emitter_x64.cpp | 3 --- src/core/PICA/gpu.cpp | 2 ++ src/libretro_core.cpp | 4 +++- 10 files changed, 20 insertions(+), 12 deletions(-) diff --git a/include/PICA/dynapica/shader_rec.hpp b/include/PICA/dynapica/shader_rec.hpp index 2dabc128..a242d02f 100644 --- a/include/PICA/dynapica/shader_rec.hpp +++ b/include/PICA/dynapica/shader_rec.hpp @@ -22,8 +22,11 @@ class ShaderJIT { ShaderCache cache; #endif + bool accurateMul = false; public: + void setAccurateMul(bool value) { accurateMul = value; } + #ifdef PANDA3DS_SHADER_JIT_SUPPORTED // Call this before starting to process a batch of vertices // This will read the PICA config (uploaded shader and shader operand descriptors) and search if we've already compiled this shader @@ -36,11 +39,11 @@ class ShaderJIT { static constexpr bool isAvailable() { return true; } #else void prepare(PICAShader& shaderUnit) { - Helpers::panic("Vertex Loader JIT: Tried to run ShaderJIT::Prepare on platform that does not support shader jit"); + Helpers::panic("Shader JIT: Tried to run ShaderJIT::Prepare on platform that does not support shader jit"); } void run(PICAShader& shaderUnit) { - Helpers::panic("Vertex Loader JIT: Tried to run ShaderJIT::Run on platform that does not support shader jit"); + Helpers::panic("Shader JIT: Tried to run ShaderJIT::Run on platform that does not support shader jit"); } // Define dummy callback. This should never be called if the shader JIT is not supported diff --git a/include/PICA/dynapica/shader_rec_emitter_arm64.hpp b/include/PICA/dynapica/shader_rec_emitter_arm64.hpp index 7411c430..9351f383 100644 --- a/include/PICA/dynapica/shader_rec_emitter_arm64.hpp +++ b/include/PICA/dynapica/shader_rec_emitter_arm64.hpp @@ -37,6 +37,8 @@ class ShaderEmitter : private oaknut::CodeBlock, public oaknut::CodeGenerator { // Shows whether the loaded shader has any log2 and exp2 instructions bool codeHasLog2 = false; bool codeHasExp2 = false; + // Whether to compile this shader using accurate, safe, non-IEEE multiplication (slow) or faster but less accurate mul + bool useSafeMUL = false; oaknut::Label log2Func, exp2Func; oaknut::Label emitLog2Func(); @@ -123,7 +125,7 @@ class ShaderEmitter : private oaknut::CodeBlock, public oaknut::CodeGenerator { PrologueCallback prologueCb = nullptr; // Initialize our emitter with "allocSize" bytes of memory allocated for the code buffer - ShaderEmitter() : oaknut::CodeBlock(allocSize), oaknut::CodeGenerator(oaknut::CodeBlock::ptr()) {} + ShaderEmitter(bool useSafeMUL) : oaknut::CodeBlock(allocSize), oaknut::CodeGenerator(oaknut::CodeBlock::ptr()), useSafeMUL(useSafeMUL) {} // PC must be a valid entrypoint here. It doesn't have that much overhead in this case, so we use std::array<>::at() to assert it does InstructionCallback getInstructionCallback(u32 pc) { return getLabelPointer(instructionLabels.at(pc)); } diff --git a/include/PICA/dynapica/shader_rec_emitter_x64.hpp b/include/PICA/dynapica/shader_rec_emitter_x64.hpp index 1052d6a0..a43bd2dc 100644 --- a/include/PICA/dynapica/shader_rec_emitter_x64.hpp +++ b/include/PICA/dynapica/shader_rec_emitter_x64.hpp @@ -45,6 +45,8 @@ class ShaderEmitter : public Xbyak::CodeGenerator { // Shows whether the loaded shader has any log2 and exp2 instructions bool codeHasLog2 = false; bool codeHasExp2 = false; + // Whether to compile this shader using accurate, safe, non-IEEE multiplication (slow) or faster but less accurate mul + bool useSafeMUL = false; Xbyak::Label log2Func, exp2Func; Xbyak::Label emitLog2Func(); @@ -130,7 +132,7 @@ class ShaderEmitter : public Xbyak::CodeGenerator { PrologueCallback prologueCb = nullptr; // Initialize our emitter with "allocSize" bytes of RWX memory - ShaderEmitter() : Xbyak::CodeGenerator(allocSize) { + ShaderEmitter(bool useSafeMUL) : Xbyak::CodeGenerator(allocSize), useSafeMUL(useSafeMUL) { cpuCaps = Xbyak::util::Cpu(); haveSSE4_1 = cpuCaps.has(Xbyak::util::Cpu::tSSE41); diff --git a/include/config.hpp b/include/config.hpp index 339e651c..6dbae9e3 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -15,6 +15,7 @@ struct EmulatorConfig { bool shaderJitEnabled = shaderJitDefault; bool discordRpcEnabled = false; + bool accurateShaderMul = false; RendererType rendererType = RendererType::OpenGL; Audio::DSPCore::Type dspType = Audio::DSPCore::Type::Null; diff --git a/src/config.cpp b/src/config.cpp index 2f9b7e00..5af4d654 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -62,6 +62,7 @@ void EmulatorConfig::load() { shaderJitEnabled = toml::find_or(gpu, "EnableShaderJIT", shaderJitDefault); vsyncEnabled = toml::find_or(gpu, "EnableVSync", true); + accurateShaderMul = toml::find_or(gpu, "AccurateShaderMultiplication", false); } } @@ -125,6 +126,7 @@ void EmulatorConfig::save() { data["GPU"]["EnableShaderJIT"] = shaderJitEnabled; data["GPU"]["Renderer"] = std::string(Renderer::typeToString(rendererType)); data["GPU"]["EnableVSync"] = vsyncEnabled; + data["GPU"]["AccurateShaderMultiplication"] = accurateShaderMul; data["Audio"]["DSPEmulation"] = std::string(Audio::DSPCore::typeToString(dspType)); data["Audio"]["EnableAudio"] = audioEnabled; diff --git a/src/core/PICA/dynapica/shader_rec.cpp b/src/core/PICA/dynapica/shader_rec.cpp index 20e171d7..e3c13c1e 100644 --- a/src/core/PICA/dynapica/shader_rec.cpp +++ b/src/core/PICA/dynapica/shader_rec.cpp @@ -16,7 +16,7 @@ void ShaderJIT::prepare(PICAShader& shaderUnit) { auto it = cache.find(hash); if (it == cache.end()) { // Block has not been compiled yet - auto emitter = std::make_unique(); + auto emitter = std::make_unique(accurateMul); emitter->compile(shaderUnit); // Get pointer to callbacks entrypointCallback = emitter->getInstructionCallback(shaderUnit.entrypoint); diff --git a/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp b/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp index 15200e76..8bc460fd 100644 --- a/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp +++ b/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp @@ -7,9 +7,6 @@ using namespace Helpers; using namespace oaknut; using namespace oaknut::util; -// TODO: Expose safe/unsafe optimizations to the user -constexpr bool useSafeMUL = true; - // Similar to the x64 recompiler, we use an odd internal ABI, which abuses the fact that we'll very rarely be calling C++ functions // So to avoid pushing and popping, we'll be making use of volatile registers as much as possible static constexpr QReg src1Vec = Q1; diff --git a/src/core/PICA/dynapica/shader_rec_emitter_x64.cpp b/src/core/PICA/dynapica/shader_rec_emitter_x64.cpp index e7bafe9f..142ff8c8 100644 --- a/src/core/PICA/dynapica/shader_rec_emitter_x64.cpp +++ b/src/core/PICA/dynapica/shader_rec_emitter_x64.cpp @@ -12,9 +12,6 @@ using namespace Xbyak; using namespace Xbyak::util; using namespace Helpers; -// TODO: Expose safe/unsafe optimizations to the user -constexpr bool useSafeMUL = false; - // The shader recompiler uses quite an odd internal ABI // We make use of the fact that in regular conditions, we should pretty much never be calling C++ code from recompiled shader code // This allows us to establish an ABI that's optimized for this sort of workflow, statically allocating volatile host registers diff --git a/src/core/PICA/gpu.cpp b/src/core/PICA/gpu.cpp index a777d0a3..ed0e5420 100644 --- a/src/core/PICA/gpu.cpp +++ b/src/core/PICA/gpu.cpp @@ -64,6 +64,8 @@ void GPU::reset() { regs.fill(0); shaderUnit.reset(); shaderJIT.reset(); + shaderJIT.setAccurateMul(config.accurateShaderMul); + std::memset(vram, 0, vramSize); lightingLUT.fill(0); lightingLUTDirty = true; diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index f9772b37..3825d3ed 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -146,6 +146,7 @@ static bool FetchVariableBool(std::string key, bool def) { static void configInit() { static const retro_variable values[] = { {"panda3ds_use_shader_jit", "Enable shader JIT; enabled|disabled"}, + {"panda3ds_accurate_shader_mul", "Enable accurate shader multiplication; disabled|enabled"}, {"panda3ds_use_vsync", "Enable VSync; enabled|disabled"}, {"panda3ds_dsp_emulation", "DSP emulation; Null|HLE|LLE"}, {"panda3ds_use_audio", "Enable audio; disabled|enabled"}, @@ -153,7 +154,7 @@ static void configInit() { {"panda3ds_write_protect_virtual_sd", "Write protect virtual SD card; disabled|enabled"}, {"panda3ds_battery_level", "Battery percentage; 5|10|20|30|50|70|90|100"}, {"panda3ds_use_charger", "Charger plugged; enabled|disabled"}, - {nullptr, nullptr} + {nullptr, nullptr}, }; envCallbacks(RETRO_ENVIRONMENT_SET_VARIABLES, (void*)values); @@ -171,6 +172,7 @@ static void configUpdate() { config.audioEnabled = FetchVariableBool("panda3ds_use_audio", false); config.sdCardInserted = FetchVariableBool("panda3ds_use_virtual_sd", true); config.sdWriteProtected = FetchVariableBool("panda3ds_write_protect_virtual_sd", false); + config.accurateShaderMul = FetchVariableBool("panda3ds_accurate_shader_mul", false); config.discordRpcEnabled = false; config.save(); From 967d9398ce6b38f19bc9471bba49c581d7ea8db7 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:28:20 +0300 Subject: [PATCH 078/251] Fix arm64 build --- src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp b/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp index 8bc460fd..296ec932 100644 --- a/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp +++ b/src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp @@ -488,7 +488,7 @@ void ShaderEmitter::recDP3(const PICAShader& shader, u32 instruction) { // Now do a full DP4 // Do a piecewise multiplication of the vectors first - if constexpr (useSafeMUL) { + if (useSafeMUL) { emitSafeMUL(src1Vec, src2Vec, scratch1Vec); } else { FMUL(src1Vec.S4(), src1Vec.S4(), src2Vec.S4()); @@ -515,7 +515,7 @@ void ShaderEmitter::recDP4(const PICAShader& shader, u32 instruction) { loadRegister<2>(src2Vec, shader, src2, 0, operandDescriptor); // Do a piecewise multiplication of the vectors first - if constexpr (useSafeMUL) { + if (useSafeMUL) { emitSafeMUL(src1Vec, src2Vec, scratch1Vec); } else { FMUL(src1Vec.S4(), src1Vec.S4(), src2Vec.S4()); @@ -548,7 +548,7 @@ void ShaderEmitter::recDPH(const PICAShader& shader, u32 instruction) { // Now perform a DP4 // Do a piecewise multiplication of the vectors first - if constexpr (useSafeMUL) { + if (useSafeMUL) { emitSafeMUL(src1Vec, src2Vec, scratch1Vec); } else { FMUL(src1Vec.S4(), src1Vec.S4(), src2Vec.S4()); @@ -831,7 +831,7 @@ void ShaderEmitter::recMUL(const PICAShader& shader, u32 instruction) { loadRegister<1>(src1Vec, shader, src1, idx, operandDescriptor); loadRegister<2>(src2Vec, shader, src2, 0, operandDescriptor); - if constexpr (useSafeMUL) { + if (useSafeMUL) { emitSafeMUL(src1Vec, src2Vec, scratch1Vec); } else { FMUL(src1Vec.S4(), src1Vec.S4(), src2Vec.S4()); @@ -904,7 +904,7 @@ void ShaderEmitter::recMAD(const PICAShader& shader, u32 instruction) { loadRegister<2>(src2Vec, shader, src2, isMADI ? 0 : idx, operandDescriptor); loadRegister<3>(src3Vec, shader, src3, isMADI ? idx : 0, operandDescriptor); - if constexpr (useSafeMUL) { + if (useSafeMUL) { emitSafeMUL(src1Vec, src2Vec, scratch1Vec); FADD(src3Vec.S4(), src3Vec.S4(), src1Vec.S4()); } else { From 27ddb1272ae5508610269d7f58a0c5beab785a0f Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 16 Jul 2024 23:39:48 +0300 Subject: [PATCH 079/251] Fix CI artifacts --- .github/workflows/Hydra_Build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Hydra_Build.yml b/.github/workflows/Hydra_Build.yml index 645f2f7a..a269e839 100644 --- a/.github/workflows/Hydra_Build.yml +++ b/.github/workflows/Hydra_Build.yml @@ -51,7 +51,7 @@ jobs: with: name: Windows Libretro core path: | - ${{github.workspace}}/build/panda3ds_libretro.dll + ${{github.workspace}}/build/${{ env.BUILD_TYPE }}/panda3ds_libretro.dll ${{github.workspace}}/docs/libretro/panda3ds_libretro.info MacOS: From a4ec7705878c56dbc6f2aaf1a70a0dc384c04566 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 17 Jul 2024 01:32:55 +0300 Subject: [PATCH 080/251] Add UBO/BlendEquation/BlendFunc to GL state manager --- include/renderer_gl/gl_state.hpp | 48 +++++++++++++++++++++++++++- src/core/renderer_gl/gl_state.cpp | 18 +++++++++-- src/core/renderer_gl/renderer_gl.cpp | 8 ++--- 3 files changed, 67 insertions(+), 7 deletions(-) diff --git a/include/renderer_gl/gl_state.hpp b/include/renderer_gl/gl_state.hpp index 69960f1e..e5591ea0 100644 --- a/include/renderer_gl/gl_state.hpp +++ b/include/renderer_gl/gl_state.hpp @@ -40,9 +40,13 @@ struct GLStateManager { GLuint boundVAO; GLuint boundVBO; GLuint currentProgram; + GLuint boundUBO; GLenum depthFunc; GLenum logicOp; + GLenum blendEquationRGB, blendEquationAlpha; + GLenum blendFuncSourceRGB, blendFuncSourceAlpha; + GLenum blendFuncDestRGB, blendFuncDestAlpha; void reset(); void resetBlend(); @@ -51,7 +55,7 @@ struct GLStateManager { void resetColourMask(); void resetDepth(); void resetVAO(); - void resetVBO(); + void resetBuffers(); void resetProgram(); void resetScissor(); void resetStencil(); @@ -183,6 +187,13 @@ struct GLStateManager { } } + void bindUBO(GLuint handle) { + if (boundUBO != handle) { + boundUBO = handle; + glBindBuffer(GL_UNIFORM_BUFFER, boundUBO); + } + } + void bindVAO(const OpenGL::VertexArray& vao) { bindVAO(vao.handle()); } void bindVBO(const OpenGL::VertexBuffer& vbo) { bindVBO(vbo.handle()); } void useProgram(const OpenGL::Program& program) { useProgram(program.handle()); } @@ -224,6 +235,41 @@ struct GLStateManager { } void setDepthFunc(OpenGL::DepthFunc func) { setDepthFunc(static_cast(func)); } + + // Counterpart to glBlendEquationSeparate + void setBlendEquation(GLenum modeRGB, GLenum modeAlpha) { + if (blendEquationRGB != modeRGB || blendEquationAlpha != modeAlpha) { + blendEquationRGB = modeRGB; + blendEquationAlpha = modeAlpha; + + glBlendEquationSeparate(modeRGB, modeAlpha); + } + } + + // Counterpart to glBlendFuncSeparate + void setBlendFunc(GLenum sourceRGB, GLenum destRGB, GLenum sourceAlpha, GLenum destAlpha) { + if (blendFuncSourceRGB != sourceRGB || blendFuncDestRGB != destRGB || blendFuncSourceAlpha != sourceAlpha || + blendFuncDestAlpha != destAlpha) { + + blendFuncSourceRGB = sourceRGB; + blendFuncDestRGB = destRGB; + blendFuncSourceAlpha = sourceAlpha; + blendFuncDestAlpha = destAlpha; + + glBlendFuncSeparate(sourceRGB, destRGB,sourceAlpha, destAlpha); + } + } + + // Counterpart to regular glBlendEquation + void setBlendEquation(GLenum mode) { setBlendEquation(mode, mode); } + + void setBlendEquation(OpenGL::BlendEquation modeRGB, OpenGL::BlendEquation modeAlpha) { + setBlendEquation(static_cast(modeRGB), static_cast(modeAlpha)); + } + + void setBlendEquation(OpenGL::BlendEquation mode) { + setBlendEquation(static_cast(mode)); + } }; static_assert(std::is_trivially_constructible(), "OpenGL State Manager class is not trivially constructible!"); diff --git a/src/core/renderer_gl/gl_state.cpp b/src/core/renderer_gl/gl_state.cpp index d2eec0d5..3d1c0681 100644 --- a/src/core/renderer_gl/gl_state.cpp +++ b/src/core/renderer_gl/gl_state.cpp @@ -5,9 +5,20 @@ void GLStateManager::resetBlend() { logicOpEnabled = false; logicOp = GL_COPY; + blendEquationRGB = GL_FUNC_ADD; + blendEquationAlpha = GL_FUNC_ADD; + + blendFuncSourceRGB = GL_SRC_COLOR; + blendFuncDestRGB = GL_DST_COLOR; + blendFuncSourceAlpha = GL_SRC_ALPHA; + blendFuncDestAlpha = GL_DST_ALPHA; + OpenGL::disableBlend(); OpenGL::disableLogicOp(); OpenGL::setLogicOp(GL_COPY); + + glBlendEquationSeparate(blendEquationRGB, blendEquationAlpha); + glBlendFuncSeparate(blendFuncSourceRGB, blendFuncDestRGB, blendFuncSourceAlpha, blendFuncDestAlpha); } void GLStateManager::resetClearing() { @@ -61,9 +72,12 @@ void GLStateManager::resetVAO() { glBindVertexArray(0); } -void GLStateManager::resetVBO() { +void GLStateManager::resetBuffers() { boundVBO = 0; + boundUBO = 0; + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindBuffer(GL_UNIFORM_BUFFER, 0); } void GLStateManager::resetProgram() { @@ -79,7 +93,7 @@ void GLStateManager::reset() { resetDepth(); resetVAO(); - resetVBO(); + resetBuffers(); resetProgram(); resetScissor(); resetStencil(); diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 97b642c7..b9a2c7ae 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -229,8 +229,8 @@ void RendererGL::setupBlending() { OpenGL::setBlendColor(float(r) / 255.f, float(g) / 255.f, float(b) / 255.f, float(a) / 255.f); // Translate equations and funcs to their GL equivalents and set them - glBlendEquationSeparate(blendingEquations[rgbEquation], blendingEquations[alphaEquation]); - glBlendFuncSeparate(blendingFuncs[rgbSourceFunc], blendingFuncs[rgbDestFunc], blendingFuncs[alphaSourceFunc], blendingFuncs[alphaDestFunc]); + gl.setBlendEquation(blendingEquations[rgbEquation], blendingEquations[alphaEquation]); + gl.setBlendFunc(blendingFuncs[rgbSourceFunc], blendingFuncs[rgbDestFunc], blendingFuncs[alphaSourceFunc], blendingFuncs[alphaDestFunc]); } } @@ -821,7 +821,7 @@ OpenGL::Program& RendererGL::getSpecializedShader() { // Allocate memory for the program UBO glGenBuffers(1, &programEntry.uboBinding); - glBindBuffer(GL_UNIFORM_BUFFER, programEntry.uboBinding); + gl.bindUBO(programEntry.uboBinding); glBufferData(GL_UNIFORM_BUFFER, sizeof(PICA::FragmentUniforms), nullptr, GL_DYNAMIC_DRAW); // Set up the binding for our UBO. Sadly we can't specify it in the shader like normal people, @@ -869,7 +869,7 @@ OpenGL::Program& RendererGL::getSpecializedShader() { vec[3] = float((color >> 24) & 0xFF) / 255.0f; } - glBindBuffer(GL_UNIFORM_BUFFER, programEntry.uboBinding); + gl.bindUBO(programEntry.uboBinding); glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(PICA::FragmentUniforms), &uniforms); return program; From aad7bb817eaf92a2632315b580ac9508d105fac5 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 17 Jul 2024 02:25:38 +0300 Subject: [PATCH 081/251] Add setting for ubershaders --- include/config.hpp | 3 +++ include/renderer.hpp | 2 ++ include/renderer_gl/renderer_gl.hpp | 4 +++- src/config.cpp | 4 ++++ src/core/PICA/gpu.cpp | 1 + src/libretro_core.cpp | 2 ++ 6 files changed, 15 insertions(+), 1 deletion(-) diff --git a/include/config.hpp b/include/config.hpp index 6dbae9e3..8aa695aa 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -13,8 +13,11 @@ struct EmulatorConfig { static constexpr bool shaderJitDefault = false; #endif + static constexpr bool ubershaderDefault = true; + bool shaderJitEnabled = shaderJitDefault; bool discordRpcEnabled = false; + bool useUbershaders = ubershaderDefault; bool accurateShaderMul = false; RendererType rendererType = RendererType::OpenGL; Audio::DSPCore::Type dspType = Audio::DSPCore::Type::Null; diff --git a/include/renderer.hpp b/include/renderer.hpp index 17812bcf..e64d49e3 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -74,6 +74,8 @@ class Renderer { virtual std::string getUbershader() { return ""; } virtual void setUbershader(const std::string& shader) {} + virtual void setUbershaderSetting(bool value) {} + // Functions for initializing the graphics context for the Qt frontend, where we don't have the convenience of SDL_Window #ifdef PANDA3DS_FRONTEND_QT virtual void initGraphicsContext(GL::Context* context) { Helpers::panic("Tried to initialize incompatible renderer with GL context"); } diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index 55a730ec..6414a7cf 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -30,6 +30,7 @@ class RendererGL final : public Renderer { OpenGL::VertexArray vao; OpenGL::VertexBuffer vbo; + bool usingUbershader = true; // Data struct { @@ -56,7 +57,6 @@ class RendererGL final : public Renderer { SurfaceCache depthBufferCache; SurfaceCache colourBufferCache; SurfaceCache textureCache; - bool usingUbershader = false; // Dummy VAO/VBO for blitting the final output OpenGL::VertexArray dummyVAO; @@ -107,6 +107,8 @@ class RendererGL final : public Renderer { virtual std::string getUbershader() override; virtual void setUbershader(const std::string& shader) override; + virtual void setUbershaderSetting(bool value) override { usingUbershader = value; } + std::optional getColourBuffer(u32 addr, PICA::ColorFmt format, u32 width, u32 height, bool createIfnotFound = true); // Note: The caller is responsible for deleting the currently bound FBO before calling this diff --git a/src/config.cpp b/src/config.cpp index 5af4d654..cc34d148 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -62,6 +62,7 @@ void EmulatorConfig::load() { shaderJitEnabled = toml::find_or(gpu, "EnableShaderJIT", shaderJitDefault); vsyncEnabled = toml::find_or(gpu, "EnableVSync", true); + useUbershaders = toml::find_or(gpu, "UseUbershaders", ubershaderDefault); accurateShaderMul = toml::find_or(gpu, "AccurateShaderMultiplication", false); } } @@ -123,10 +124,13 @@ void EmulatorConfig::save() { data["General"]["EnableDiscordRPC"] = discordRpcEnabled; data["General"]["UsePortableBuild"] = usePortableBuild; data["General"]["DefaultRomPath"] = defaultRomPath.string(); + data["GPU"]["EnableShaderJIT"] = shaderJitEnabled; data["GPU"]["Renderer"] = std::string(Renderer::typeToString(rendererType)); data["GPU"]["EnableVSync"] = vsyncEnabled; data["GPU"]["AccurateShaderMultiplication"] = accurateShaderMul; + data["GPU"]["UseUbershaders"] = useUbershaders; + data["Audio"]["DSPEmulation"] = std::string(Audio::DSPCore::typeToString(dspType)); data["Audio"]["EnableAudio"] = audioEnabled; diff --git a/src/core/PICA/gpu.cpp b/src/core/PICA/gpu.cpp index ed0e5420..a54fe6eb 100644 --- a/src/core/PICA/gpu.cpp +++ b/src/core/PICA/gpu.cpp @@ -110,6 +110,7 @@ void GPU::reset() { externalRegs[Framebuffer1Config] = static_cast(PICA::ColorFmt::RGB8); externalRegs[Framebuffer1Select] = 0; + renderer->setUbershaderSetting(config.useUbershaders); renderer->reset(); } diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index 3825d3ed..a6a1ff00 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -147,6 +147,7 @@ static void configInit() { static const retro_variable values[] = { {"panda3ds_use_shader_jit", "Enable shader JIT; enabled|disabled"}, {"panda3ds_accurate_shader_mul", "Enable accurate shader multiplication; disabled|enabled"}, + {"panda3ds_use_ubershader", "Use ubershaders (No stutter, maybe slower); enabled|disabled"}, {"panda3ds_use_vsync", "Enable VSync; enabled|disabled"}, {"panda3ds_dsp_emulation", "DSP emulation; Null|HLE|LLE"}, {"panda3ds_use_audio", "Enable audio; disabled|enabled"}, @@ -173,6 +174,7 @@ static void configUpdate() { config.sdCardInserted = FetchVariableBool("panda3ds_use_virtual_sd", true); config.sdWriteProtected = FetchVariableBool("panda3ds_write_protect_virtual_sd", false); config.accurateShaderMul = FetchVariableBool("panda3ds_accurate_shader_mul", false); + config.useUbershaders = FetchVariableBool("panda3ds_use_ubershader", true); config.discordRpcEnabled = false; config.save(); From cb0e69847cc8f3eccf38f56481ea2e96151911c3 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 17 Jul 2024 21:17:54 +0300 Subject: [PATCH 082/251] Hotfix UBO binding --- src/core/PICA/shader_gen_glsl.cpp | 2 +- src/core/renderer_gl/renderer_gl.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index e135ac8e..9b467e63 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -175,7 +175,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { ret += "fragColor = combinerOutput;\n"; ret += "}"; // End of main function - ret += "\n\n\n\n\n\n\n"; + ret += "\n\n\n"; return ret; } diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index b9a2c7ae..0c33b898 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -828,8 +828,8 @@ OpenGL::Program& RendererGL::getSpecializedShader() { // As it's an OpenGL 4.2 feature that MacOS doesn't support... uint uboIndex = glGetUniformBlockIndex(program.handle(), "FragmentUniforms"); glUniformBlockBinding(program.handle(), uboIndex, uboBlockBinding); - glBindBufferBase(GL_UNIFORM_BUFFER, uboBlockBinding, programEntry.uboBinding); } + glBindBufferBase(GL_UNIFORM_BUFFER, uboBlockBinding, programEntry.uboBinding); // Upload uniform data to our shader's UBO PICA::FragmentUniforms uniforms; From d013582223d24619a302eca32cfa72f6284366ab Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 17 Jul 2024 21:53:19 +0300 Subject: [PATCH 083/251] Shadergen: Optimize caching --- include/PICA/pica_frag_config.hpp | 5 +++-- src/core/renderer_gl/renderer_gl.cpp | 20 +++++++++++++------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index 59f13757..9e13b3b5 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -24,8 +24,9 @@ namespace PICA { u32 texUnitConfig; u32 texEnvUpdateBuffer; - // There's 6 TEV stages, and each one is configured via 5 word-sized registers - std::array tevConfigs; + // There's 6 TEV stages, and each one is configured via 4 word-sized registers + // (+ the constant color register, which we don't include here, otherwise we'd generate too many shaders) + std::array tevConfigs; }; // Config used for identifying unique fragment pipeline configurations diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 0c33b898..249d8484 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -793,13 +793,19 @@ OpenGL::Program& RendererGL::getSpecializedShader() { texConfig.texUnitConfig = regs[InternalRegs::TexUnitCfg]; texConfig.texEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; - // Set up TEV stages - std::memcpy(&texConfig.tevConfigs[0 * 5], ®s[InternalRegs::TexEnv0Source], 5 * sizeof(u32)); - std::memcpy(&texConfig.tevConfigs[1 * 5], ®s[InternalRegs::TexEnv1Source], 5 * sizeof(u32)); - std::memcpy(&texConfig.tevConfigs[2 * 5], ®s[InternalRegs::TexEnv2Source], 5 * sizeof(u32)); - std::memcpy(&texConfig.tevConfigs[3 * 5], ®s[InternalRegs::TexEnv3Source], 5 * sizeof(u32)); - std::memcpy(&texConfig.tevConfigs[4 * 5], ®s[InternalRegs::TexEnv4Source], 5 * sizeof(u32)); - std::memcpy(&texConfig.tevConfigs[5 * 5], ®s[InternalRegs::TexEnv5Source], 5 * sizeof(u32)); + // Set up TEV stages. Annoyingly we can't just memcpy as the TEV registers are arranged like + // {Source, Operand, Combiner, Color, Scale} and we want to skip the color register since it's uploaded via UBO +#define setupTevStage(stage) \ + std::memcpy(&texConfig.tevConfigs[stage * 4], ®s[InternalRegs::TexEnv##stage##Source], 3 * sizeof(u32)); \ + texConfig.tevConfigs[stage * 4 + 3] = regs[InternalRegs::TexEnv##stage##Source + 5]; + + setupTevStage(0); + setupTevStage(1); + setupTevStage(2); + setupTevStage(3); + setupTevStage(4); + setupTevStage(5); +#undef setupTevStage CachedProgram& programEntry = shaderCache[fsConfig]; OpenGL::Program& program = programEntry.program; From 0fc95ae8ef32aaad11e8d53bce392eb1f6aa531a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 17 Jul 2024 21:54:56 +0300 Subject: [PATCH 084/251] Shadergen: Remove trailing newlines --- src/core/PICA/shader_gen_glsl.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 9b467e63..0877e5f2 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -173,9 +173,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { applyAlphaTest(ret, regs); - ret += "fragColor = combinerOutput;\n"; - ret += "}"; // End of main function - ret += "\n\n\n"; + ret += "fragColor = combinerOutput;\n}"; // End of main function return ret; } From 801d14e4635d35f27a60f390cc6542904b05668e Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:56:57 +0000 Subject: [PATCH 085/251] Shadergen: Fix UBO uploads and optimize shader caching (#538) * Hotfix UBO binding * Shadergen: Optimize caching * Shadergen: Remove trailing newlines --- include/PICA/pica_frag_config.hpp | 5 +++-- src/core/PICA/shader_gen_glsl.cpp | 4 +--- src/core/renderer_gl/renderer_gl.cpp | 22 ++++++++++++++-------- 3 files changed, 18 insertions(+), 13 deletions(-) diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index 59f13757..9e13b3b5 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -24,8 +24,9 @@ namespace PICA { u32 texUnitConfig; u32 texEnvUpdateBuffer; - // There's 6 TEV stages, and each one is configured via 5 word-sized registers - std::array tevConfigs; + // There's 6 TEV stages, and each one is configured via 4 word-sized registers + // (+ the constant color register, which we don't include here, otherwise we'd generate too many shaders) + std::array tevConfigs; }; // Config used for identifying unique fragment pipeline configurations diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index e135ac8e..0877e5f2 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -173,9 +173,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs) { applyAlphaTest(ret, regs); - ret += "fragColor = combinerOutput;\n"; - ret += "}"; // End of main function - ret += "\n\n\n\n\n\n\n"; + ret += "fragColor = combinerOutput;\n}"; // End of main function return ret; } diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index b9a2c7ae..249d8484 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -793,13 +793,19 @@ OpenGL::Program& RendererGL::getSpecializedShader() { texConfig.texUnitConfig = regs[InternalRegs::TexUnitCfg]; texConfig.texEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; - // Set up TEV stages - std::memcpy(&texConfig.tevConfigs[0 * 5], ®s[InternalRegs::TexEnv0Source], 5 * sizeof(u32)); - std::memcpy(&texConfig.tevConfigs[1 * 5], ®s[InternalRegs::TexEnv1Source], 5 * sizeof(u32)); - std::memcpy(&texConfig.tevConfigs[2 * 5], ®s[InternalRegs::TexEnv2Source], 5 * sizeof(u32)); - std::memcpy(&texConfig.tevConfigs[3 * 5], ®s[InternalRegs::TexEnv3Source], 5 * sizeof(u32)); - std::memcpy(&texConfig.tevConfigs[4 * 5], ®s[InternalRegs::TexEnv4Source], 5 * sizeof(u32)); - std::memcpy(&texConfig.tevConfigs[5 * 5], ®s[InternalRegs::TexEnv5Source], 5 * sizeof(u32)); + // Set up TEV stages. Annoyingly we can't just memcpy as the TEV registers are arranged like + // {Source, Operand, Combiner, Color, Scale} and we want to skip the color register since it's uploaded via UBO +#define setupTevStage(stage) \ + std::memcpy(&texConfig.tevConfigs[stage * 4], ®s[InternalRegs::TexEnv##stage##Source], 3 * sizeof(u32)); \ + texConfig.tevConfigs[stage * 4 + 3] = regs[InternalRegs::TexEnv##stage##Source + 5]; + + setupTevStage(0); + setupTevStage(1); + setupTevStage(2); + setupTevStage(3); + setupTevStage(4); + setupTevStage(5); +#undef setupTevStage CachedProgram& programEntry = shaderCache[fsConfig]; OpenGL::Program& program = programEntry.program; @@ -828,8 +834,8 @@ OpenGL::Program& RendererGL::getSpecializedShader() { // As it's an OpenGL 4.2 feature that MacOS doesn't support... uint uboIndex = glGetUniformBlockIndex(program.handle(), "FragmentUniforms"); glUniformBlockBinding(program.handle(), uboIndex, uboBlockBinding); - glBindBufferBase(GL_UNIFORM_BUFFER, uboBlockBinding, programEntry.uboBinding); } + glBindBufferBase(GL_UNIFORM_BUFFER, uboBlockBinding, programEntry.uboBinding); // Upload uniform data to our shader's UBO PICA::FragmentUniforms uniforms; From 2ca886f64f56f47fffc9b4125458b728352c9e7e Mon Sep 17 00:00:00 2001 From: offtkp Date: Wed, 17 Jul 2024 22:08:48 +0300 Subject: [PATCH 086/251] Move normal calculation to the fragment shader --- src/host_shaders/opengl_fragment_shader.frag | 30 +++++++++++++++----- src/host_shaders/opengl_vertex_shader.vert | 16 ++--------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index ae43d993..582d6eef 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -1,8 +1,6 @@ #version 410 core -in vec3 v_tangent; -in vec3 v_normal; -in vec3 v_bitangent; +in vec4 v_quaternion; in vec4 v_colour; in vec3 v_texcoord0; in vec2 v_texcoord1; @@ -37,6 +35,7 @@ uint readPicaReg(uint reg_addr) { return u_picaRegs[reg_addr - 0x48u]; } vec4 tevSources[16]; vec4 tevNextPreviousBuffer; bool tevUnimplementedSourceFlag = false; +vec3 normal; // Holds the enabled state of the lighting samples for various PICA configurations // As explained in https://www.3dbrew.org/wiki/GPU/Internal_Registers#GPUREG_LIGHTING_CONFIG0 @@ -255,7 +254,7 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light uint input_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SELECT, int(lut_id) << 2, 3); switch (input_id) { case 0u: { - delta = dot(v_normal, normalize(half_vector)); + delta = dot(normal, normalize(half_vector)); break; } case 1u: { @@ -263,11 +262,11 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light break; } case 2u: { - delta = dot(v_normal, normalize(v_view)); + delta = dot(normal, normalize(v_view)); break; } case 3u: { - delta = dot(light_vector, v_normal); + delta = dot(light_vector, normal); break; } case 4u: { @@ -313,6 +312,12 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light } } +vec3 rotateVec3ByQuaternion(vec3 v, vec4 q) { + vec3 u = q.xyz; + float s = q.w; + return 2.0 * dot(u, v) * u + (s * s - dot(u, u)) * v + 2.0 * s * cross(u, v); +} + // Implements the following algorthm: https://mathb.in/26766 void calcLighting(out vec4 primary_color, out vec4 secondary_color) { error_unimpl = false; @@ -336,6 +341,17 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { GPUREG_LIGHTING_LUTINPUT_ABS = readPicaReg(0x01D0u); GPUREG_LIGHTING_LUTINPUT_SELECT = readPicaReg(0x01D1u); + uint bump_mode = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 28, 2); + + // Bump mode is ignored for now because it breaks some games ie. Toad Treasure Tracker + // Could be because the texture is not sampled correctly, may need the clamp/border color configurations + switch (bump_mode) { + default: { + normal = rotateVec3ByQuaternion(vec3(0.0, 0.0, 1.0), v_quaternion); + break; + } + } + vec4 diffuse_sum = vec4(0.0, 0.0, 0.0, 1.0); vec4 specular_sum = vec4(0.0, 0.0, 0.0, 1.0); @@ -377,7 +393,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { light_vector = normalize(light_vector); half_vector = light_vector + normalize(v_view); - float NdotL = dot(v_normal, light_vector); // N dot Li + float NdotL = dot(normal, light_vector); // N dot Li // Two sided diffuse if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) diff --git a/src/host_shaders/opengl_vertex_shader.vert b/src/host_shaders/opengl_vertex_shader.vert index a25d7a6d..057f9a88 100644 --- a/src/host_shaders/opengl_vertex_shader.vert +++ b/src/host_shaders/opengl_vertex_shader.vert @@ -9,9 +9,7 @@ layout(location = 5) in float a_texcoord0_w; layout(location = 6) in vec3 a_view; layout(location = 7) in vec2 a_texcoord2; -out vec3 v_normal; -out vec3 v_tangent; -out vec3 v_bitangent; +out vec4 v_quaternion; out vec4 v_colour; out vec3 v_texcoord0; out vec2 v_texcoord1; @@ -35,12 +33,6 @@ vec4 abgr8888ToVec4(uint abgr) { return scale * vec4(float(abgr & 0xffu), float((abgr >> 8) & 0xffu), float((abgr >> 16) & 0xffu), float(abgr >> 24)); } -vec3 rotateVec3ByQuaternion(vec3 v, vec4 q) { - vec3 u = q.xyz; - float s = q.w; - return 2.0 * dot(u, v) * u + (s * s - dot(u, u)) * v + 2.0 * s * cross(u, v); -} - // Convert an arbitrary-width floating point literal to an f32 float decodeFP(uint hex, uint E, uint M) { uint width = M + E + 1u; @@ -73,10 +65,6 @@ void main() { v_texcoord2 = vec2(a_texcoord2.x, 1.0 - a_texcoord2.y); v_view = a_view; - v_normal = normalize(rotateVec3ByQuaternion(vec3(0.0, 0.0, 1.0), a_quaternion)); - v_tangent = normalize(rotateVec3ByQuaternion(vec3(1.0, 0.0, 0.0), a_quaternion)); - v_bitangent = normalize(rotateVec3ByQuaternion(vec3(0.0, 1.0, 0.0), a_quaternion)); - for (int i = 0; i < 6; i++) { v_textureEnvColor[i] = abgr8888ToVec4(u_textureEnvColor[i]); } @@ -95,4 +83,6 @@ void main() { // There's also another, always-on clipping plane based on vertex z gl_ClipDistance[0] = -a_coords.z; gl_ClipDistance[1] = dot(clipData, a_coords); + + v_quaternion = a_quaternion; } From ed00ddc8058aaeccd00b5cdb3f640759e13e9111 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 18 Jul 2024 00:55:57 +0300 Subject: [PATCH 087/251] Improve lighting register definitions --- include/PICA/pica_frag_config.hpp | 160 ++++++++++++++++++++++++++- include/PICA/regs.hpp | 12 +- include/PICA/shader_gen.hpp | 3 +- src/core/PICA/shader_gen_glsl.cpp | 2 +- src/core/renderer_gl/renderer_gl.cpp | 4 +- 5 files changed, 175 insertions(+), 6 deletions(-) diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index 9e13b3b5..cfb22e5c 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -29,20 +29,178 @@ namespace PICA { std::array tevConfigs; }; + struct Light { + union { + u16 raw; + BitField<0, 3, u16> num; + BitField<3, 1, u16> directional; + BitField<4, 1, u16> twoSidedDiffuse; + BitField<5, 1, u16> distanceAttenuationEnable; + BitField<6, 1, u16> spotAttenuationEnable; + BitField<7, 1, u16> geometricFactor0; + BitField<8, 1, u16> geometricFactor1; + BitField<9, 1, u16> shadowEnable; + }; + }; + + struct LightingLUTConfig { + union { + u32 raw; + BitField<0, 1, u32> enable; + BitField<1, 1, u32> absInput; + BitField<2, 3, u32> type; + }; + float scale; + }; + + struct LightingConfig { + union { + u32 raw{}; + BitField<0, 1, u32> enable; + BitField<1, 4, u32> lightNum; + BitField<5, 2, u32> bumpMode; + BitField<7, 2, u32> bumpSelector; + BitField<9, 1, u32> bumpRenorm; + BitField<10, 1, u32> clampHighlights; + BitField<11, 4, u32> config; + BitField<15, 1, u32> enablePrimaryAlpha; + BitField<16, 1, u32> enableSecondaryAlpha; + BitField<17, 1, u32> enableShadow; + BitField<18, 1, u32> shadowPrimary; + BitField<19, 1, u32> shadowSecondary; + BitField<20, 1, u32> shadowInvert; + BitField<21, 1, u32> shadowAlpha; + BitField<22, 2, u32> shadowSelector; + }; + + LightingLUTConfig d0{}; + LightingLUTConfig d1{}; + LightingLUTConfig sp{}; + LightingLUTConfig fr{}; + LightingLUTConfig rr{}; + LightingLUTConfig rg{}; + LightingLUTConfig rb{}; + + std::array lights{}; + + LightingConfig(const std::array& regs) { + // Ignore lighting registers if it's disabled + if ((regs[InternalRegs::LightingEnable] & 1) == 0) { + return; + } + + const u32 config0 = regs[InternalRegs::LightConfig0]; + const u32 config1 = regs[InternalRegs::LightConfig1]; + const u32 totalLightCount = Helpers::getBits<0, 3>(regs[InternalRegs::LightNumber]) + 1; + + enable = 1; + lightNum = totalLightCount; + + enableShadow = Helpers::getBit<0>(config0); + if (enableShadow) [[unlikely]] { + shadowPrimary = Helpers::getBit<16>(config0); + shadowSecondary = Helpers::getBit<17>(config0); + shadowInvert = Helpers::getBit<18>(config0); + shadowAlpha = Helpers::getBit<19>(config0); + shadowSelector = Helpers::getBits<24, 2>(config0); + } + + enablePrimaryAlpha = Helpers::getBit<2>(config0); + enableSecondaryAlpha = Helpers::getBit<3>(config0); + config = Helpers::getBits<4, 4>(config0); + + bumpSelector = Helpers::getBits<22, 2>(config0); + clampHighlights = Helpers::getBit<27>(config0); + bumpMode = Helpers::getBits<28, 2>(config0); + bumpRenorm = Helpers::getBit<30>(config0) ^ 1; // 0 = enable so flip it with xor + + for (int i = 0; i < totalLightCount; i++) { + auto& light = lights[i]; + const u32 lightConfig = 0x149 + 0x10 * i; + + light.num = (regs[InternalRegs::LightPermutation] >> (i * 4)) & 0x7; + light.directional = Helpers::getBit<0>(lightConfig); + light.twoSidedDiffuse = Helpers::getBit<1>(lightConfig); + light.geometricFactor0 = Helpers::getBit<2>(lightConfig); + light.geometricFactor1 = Helpers::getBit<3>(lightConfig); + + light.shadowEnable = ((config1 >> i) & 1) ^ 1; // This also does 0 = enabled + light.spotAttenuationEnable = ((config1 >> (8 + i)) & 1) ^ 1; // Same here + light.distanceAttenuationEnable = ((config1 >> (24 + i)) & 1) ^ 1; // Of course same here + } + + d0.enable = Helpers::getBit<16>(config1) == 0; + d1.enable = Helpers::getBit<17>(config1) == 0; + fr.enable = Helpers::getBit<19>(config1) == 0; + rb.enable = Helpers::getBit<20>(config1) == 0; + rg.enable = Helpers::getBit<21>(config1) == 0; + rr.enable = Helpers::getBit<22>(config1) == 0; + sp.enable = 1; + + const u32 lutAbs = regs[InternalRegs::LightLUTAbs]; + const u32 lutSelect = regs[InternalRegs::LightLUTSelect]; + const u32 lutScale = regs[InternalRegs::LightLUTScale]; + static constexpr float scales[] = {1.0f, 2.0f, 4.0f, 8.0f, 0.25f, 0.5f}; + + if (d0.enable) { + d0.absInput = Helpers::getBit<1>(lutAbs) == 0; + d0.type = Helpers::getBits<0, 3>(lutSelect); + d0.scale = scales[Helpers::getBits<0, 3>(lutScale)]; + } + + if (d1.enable) { + d1.absInput = Helpers::getBit<5>(lutAbs) == 0; + d1.type = Helpers::getBits<4, 3>(lutSelect); + d1.scale = scales[Helpers::getBits<4, 3>(lutScale)]; + } + + sp.absInput = Helpers::getBit<9>(lutAbs) == 0; + sp.type = Helpers::getBits<8, 3>(lutSelect); + sp.scale = scales[Helpers::getBits<8, 3>(lutScale)]; + + if (fr.enable) { + fr.absInput = Helpers::getBit<13>(lutAbs) == 0; + fr.type = Helpers::getBits<12, 3>(lutSelect); + fr.scale = scales[Helpers::getBits<12, 3>(lutScale)]; + } + + if (rb.enable) { + rb.absInput = Helpers::getBit<17>(lutAbs) == 0; + rb.type = Helpers::getBits<16, 3>(lutSelect); + rb.scale = scales[Helpers::getBits<16, 3>(lutScale)]; + } + + if (rg.enable) { + rg.absInput = Helpers::getBit<21>(lutAbs) == 0; + rg.type = Helpers::getBits<20, 3>(lutSelect); + rg.scale = scales[Helpers::getBits<20, 3>(lutScale)]; + } + + if (rr.enable) { + rr.absInput = Helpers::getBit<25>(lutAbs) == 0; + rr.type = Helpers::getBits<24, 3>(lutSelect); + rr.scale = scales[Helpers::getBits<24, 3>(lutScale)]; + } + } + }; + // Config used for identifying unique fragment pipeline configurations struct FragmentConfig { OutputConfig outConfig; TextureConfig texConfig; + LightingConfig lighting; bool operator==(const FragmentConfig& config) const { // Hash function and equality operator required by std::unordered_map return std::memcmp(this, &config, sizeof(FragmentConfig)) == 0; } + + FragmentConfig(const std::array& regs) : lighting(regs) {} }; static_assert( std::has_unique_object_representations() && std::has_unique_object_representations() && - std::has_unique_object_representations() + std::has_unique_object_representations() ); } // namespace PICA diff --git a/include/PICA/regs.hpp b/include/PICA/regs.hpp index 74f8c7d5..312ac78b 100644 --- a/include/PICA/regs.hpp +++ b/include/PICA/regs.hpp @@ -67,7 +67,17 @@ namespace PICA { ColourBufferLoc = 0x11D, FramebufferSize = 0x11E, - //LightingRegs + + // Lighting registers + LightingEnable = 0x8F, + LightNumber = 0x1C2, + LightConfig0 = 0x1C3, + LightConfig1 = 0x1C4, + LightPermutation = 0x1D9, + LightLUTAbs = 0x1D0, + LightLUTSelect = 0x1D1, + LightLUTScale = 0x1D2, + LightingLUTIndex = 0x01C5, LightingLUTData0 = 0x01C8, LightingLUTData1 = 0x01C9, diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index e8e8ca20..0a6bca8e 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -2,6 +2,7 @@ #include #include "PICA/gpu.hpp" +#include "PICA/pica_frag_config.hpp" #include "PICA/regs.hpp" #include "helpers.hpp" @@ -30,7 +31,7 @@ namespace PICA::ShaderGen { public: FragmentGenerator(API api, Language language) : api(api), language(language) {} - std::string generate(const PICARegs& regs); + std::string generate(const PICARegs& regs, const PICA::FragmentConfig& config); std::string getVertexShader(const PICARegs& regs); void setTarget(API api, Language language) { diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 0877e5f2..5dbc3b81 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -92,7 +92,7 @@ std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { return ret; } -std::string FragmentGenerator::generate(const PICARegs& regs) { +std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConfig& config) { std::string ret = ""; switch (api) { diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 249d8484..b85e7689 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -780,7 +780,7 @@ std::optional RendererGL::getColourBuffer(u32 addr, PICA::ColorFmt OpenGL::Program& RendererGL::getSpecializedShader() { constexpr uint uboBlockBinding = 2; - PICA::FragmentConfig fsConfig; + PICA::FragmentConfig fsConfig(regs); auto& outConfig = fsConfig.outConfig; auto& texConfig = fsConfig.texConfig; @@ -812,7 +812,7 @@ OpenGL::Program& RendererGL::getSpecializedShader() { if (!program.exists()) { std::string vs = fragShaderGen.getVertexShader(regs); - std::string fs = fragShaderGen.generate(regs); + std::string fs = fragShaderGen.generate(regs, fsConfig); OpenGL::Shader vertShader({vs.c_str(), vs.size()}, OpenGL::Vertex); OpenGL::Shader fragShader({fs.c_str(), fs.size()}, OpenGL::Fragment); From 7e7856fa4440b8584895a3ba4ce77e586d62f400 Mon Sep 17 00:00:00 2001 From: offtkp Date: Thu, 18 Jul 2024 02:51:08 +0300 Subject: [PATCH 088/251] Pack sampler configurations in bitfields instead of bool arrays --- src/host_shaders/opengl_fragment_shader.frag | 49 +++++++++++++------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index 582d6eef..23c5c4cb 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -39,18 +39,37 @@ vec3 normal; // Holds the enabled state of the lighting samples for various PICA configurations // As explained in https://www.3dbrew.org/wiki/GPU/Internal_Registers#GPUREG_LIGHTING_CONFIG0 -const bool samplerEnabled[9 * 7] = bool[9 * 7]( - // D0 D1 SP FR RB RG RR - true, false, true, false, false, false, true, // Configuration 0: D0, SP, RR - false, false, true, true, false, false, true, // Configuration 1: FR, SP, RR - true, true, false, false, false, false, true, // Configuration 2: D0, D1, RR - true, true, false, true, false, false, false, // Configuration 3: D0, D1, FR - true, true, true, false, true, true, true, // Configuration 4: All except for FR - true, false, true, true, true, true, true, // Configuration 5: All except for D1 - true, true, true, true, false, false, true, // Configuration 6: All except for RB and RG - false, false, false, false, false, false, false, // Configuration 7: Unused - true, true, true, true, true, true, true // Configuration 8: All -); +// const bool samplerEnabled[9 * 7] = bool[9 * 7]( +// // D0 D1 SP FR RB RG RR +// true, false, true, false, false, false, true, // Configuration 0: D0, SP, RR +// false, false, true, true, false, false, true, // Configuration 1: FR, SP, RR +// true, true, false, false, false, false, true, // Configuration 2: D0, D1, RR +// true, true, false, true, false, false, false, // Configuration 3: D0, D1, FR +// true, true, true, false, true, true, true, // Configuration 4: All except for FR +// true, false, true, true, true, true, true, // Configuration 5: All except for D1 +// true, true, true, true, false, false, true, // Configuration 6: All except for RB and RG +// false, false, false, false, false, false, false, // Configuration 7: Unused +// true, true, true, true, true, true, true // Configuration 8: All +// ); + +// The above have been condensed to two uints to save space +// You can confirm they are the same by running the following: +// for (int i = 0; i < 9 * 7; i++) { +// unsigned arrayIndex = (i >> 5); +// bool b = (samplerEnabledBitfields[arrayIndex] & (1u << (i & 31))) != 0u; +// if (samplerEnabled[i] == b) { +// printf("%d: happy\n", i); +// } else { +// printf("%d: unhappy\n", i); +// } +// } +const uint samplerEnabledBitfields[2] = uint[2](0x7170e645u, 0x7f013fefu); + +bool isSamplerEnabled(uint environment_id, uint lut_id) { + uint index = 7 * environment_id + lut_id; + uint arrayIndex = (index >> 5); + return (samplerEnabledBitfields[arrayIndex] & (1u << (index & 31))) != 0u; +} // OpenGL ES 1.1 reference pages for TEVs (this is what the PICA200 implements): // https://registry.khronos.org/OpenGL-Refpages/es1.1/xhtml/glTexEnv.xml @@ -198,10 +217,6 @@ float decodeFP(uint hex, uint E, uint M) { return uintBitsToFloat(hex); } -bool isSamplerEnabled(uint environment_id, uint lut_id) { - return samplerEnabled[7 * environment_id + lut_id]; -} - float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light_vector, vec3 half_vector) { uint lut_index; // lut_id is one of these values @@ -485,7 +500,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { secondary_color = clamp(specular_sum, vec4(0.0), vec4(1.0)); if (error_unimpl) { - secondary_color = primary_color = unimpl_color; + // secondary_color = primary_color = unimpl_color; } } From b51e2fd25f4a983c93a8781fb4847f8b26409d2b Mon Sep 17 00:00:00 2001 From: offtkp Date: Thu, 18 Jul 2024 02:53:54 +0300 Subject: [PATCH 089/251] Update gles.patch --- .github/gles.patch | 81 ++++++++++++++++++++++++++-------------------- 1 file changed, 46 insertions(+), 35 deletions(-) diff --git a/.github/gles.patch b/.github/gles.patch index f5270518..99258011 100644 --- a/.github/gles.patch +++ b/.github/gles.patch @@ -21,7 +21,7 @@ index 990e2f80..2e7842ac 100644 void main() { diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag -index 1b8e9751..96238000 100644 +index 23c5c4cb..a9851a8b 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -1,4 +1,5 @@ @@ -29,13 +29,13 @@ index 1b8e9751..96238000 100644 +#version 300 es +precision mediump float; - in vec3 v_tangent; - in vec3 v_normal; -@@ -171,11 +172,17 @@ float lutLookup(uint lut, int index) { + in vec4 v_quaternion; + in vec4 v_colour; +@@ -189,11 +190,17 @@ float lutLookup(uint lut, int index) { return texelFetch(u_tex_lighting_lut, ivec2(index, lut), 0).r; } -+// some gles versions have bitfieldExtract and complain if you redefine it, some don't and compile error, using this instead ++// some gles versions have bitfieldExtractCompat and complain if you redefine it, some don't and compile error, using this instead +uint bitfieldExtractCompat(uint val, int off, int size) { + uint mask = uint((1 << size) - 1); + return uint(val >> off) & mask; @@ -50,7 +50,7 @@ index 1b8e9751..96238000 100644 } // Convert an arbitrary-width floating point literal to an f32 -@@ -243,16 +250,16 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light +@@ -257,16 +264,16 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light // If RR is enabled but not RG or RB, the output of RR is used for the three components; Red, Green and Blue. bool current_sampler_enabled = isSamplerEnabled(environment_id, lut_id); // 7 luts per environment @@ -59,25 +59,25 @@ index 1b8e9751..96238000 100644 return 1.0; } -- uint scale_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SCALE, int(lut_id) * 4, 3); -+ uint scale_id = bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_SCALE, int(lut_id) * 4, 3); +- uint scale_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SCALE, int(lut_id) << 2, 3); ++ uint scale_id = bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_SCALE, int(lut_id) << 2, 3); float scale = float(1u << scale_id); if (scale_id >= 6u) scale /= 256.0; float delta = 1.0; -- uint input_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SELECT, int(lut_id) * 4, 3); -+ uint input_id = bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_SELECT, int(lut_id) * 4, 3); +- uint input_id = bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_SELECT, int(lut_id) << 2, 3); ++ uint input_id = bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_SELECT, int(lut_id) << 2, 3); switch (input_id) { case 0u: { - delta = dot(v_normal, normalize(half_vector)); -@@ -271,14 +278,14 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light + delta = dot(normal, normalize(half_vector)); +@@ -285,14 +292,14 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light break; } case 4u: { - // These are ints so that bitfieldExtract sign extends for us + // These are ints so that bitfieldExtractCompat sign extends for us - int GPUREG_LIGHTi_SPOTDIR_LOW = int(readPicaReg(0x0146u + 0x10u * light_id)); - int GPUREG_LIGHTi_SPOTDIR_HIGH = int(readPicaReg(0x0147u + 0x10u * light_id)); + int GPUREG_LIGHTi_SPOTDIR_LOW = int(readPicaReg(0x0146u + (light_id << 4u))); + int GPUREG_LIGHTi_SPOTDIR_HIGH = int(readPicaReg(0x0147u + (light_id << 4u))); // These are fixed point 1.1.11 values, so we need to convert them to float - float x = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13)) / 2047.0; @@ -89,19 +89,19 @@ index 1b8e9751..96238000 100644 vec3 spotlight_vector = vec3(x, y, z); delta = dot(light_vector, spotlight_vector); // spotlight direction is negated so we don't negate light_vector break; -@@ -296,9 +303,9 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light +@@ -310,9 +317,9 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light } // 0 = enabled -- if (bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_ABS, 1 + 4 * int(lut_id), 1) == 0u) { -+ if (bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_ABS, 1 + 4 * int(lut_id), 1) == 0u) { +- if (bitfieldExtract(GPUREG_LIGHTING_LUTINPUT_ABS, 1 + (int(lut_id) << 2), 1) == 0u) { ++ if (bitfieldExtractCompat(GPUREG_LIGHTING_LUTINPUT_ABS, 1 + (int(lut_id) << 2), 1) == 0u) { // Two sided diffuse - if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) { + if (bitfieldExtractCompat(GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) { delta = max(delta, 0.0); } else { delta = abs(delta); -@@ -319,7 +326,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -339,7 +346,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { unimpl_color = vec4(1.0, 0.0, 1.0, 1.0); uint GPUREG_LIGHTING_ENABLE = readPicaReg(0x008Fu); @@ -110,7 +110,16 @@ index 1b8e9751..96238000 100644 primary_color = secondary_color = vec4(0.0); return; } -@@ -339,15 +346,15 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -356,7 +363,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + GPUREG_LIGHTING_LUTINPUT_ABS = readPicaReg(0x01D0u); + GPUREG_LIGHTING_LUTINPUT_SELECT = readPicaReg(0x01D1u); + +- uint bump_mode = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 28, 2); ++ uint bump_mode = bitfieldExtractCompat(GPUREG_LIGHTING_CONFIG0, 28, 2); + + // Bump mode is ignored for now because it breaks some games ie. Toad Treasure Tracker + // Could be because the texture is not sampled correctly, may need the clamp/border color configurations +@@ -370,15 +377,15 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { vec4 diffuse_sum = vec4(0.0, 0.0, 0.0, 1.0); vec4 specular_sum = vec4(0.0, 0.0, 0.0, 1.0); @@ -124,12 +133,12 @@ index 1b8e9751..96238000 100644 vec3 half_vector; for (uint i = 0u; i < GPUREG_LIGHTING_NUM_LIGHTS; i++) { -- light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i * 3u), 3); -+ light_id = bitfieldExtractCompat(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i * 3u), 3); +- light_id = bitfieldExtract(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i) << 2, 3); ++ light_id = bitfieldExtractCompat(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i) << 2, 3); - uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140u + 0x10u * light_id); - uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141u + 0x10u * light_id); -@@ -359,12 +366,12 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140u + (light_id << 4u)); + uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141u + (light_id << 4u)); +@@ -390,12 +397,12 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { float light_distance; vec3 light_position = vec3( @@ -145,8 +154,8 @@ index 1b8e9751..96238000 100644 light_vector = light_position + v_view; } -@@ -380,14 +387,14 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { - float NdotL = dot(v_normal, light_vector); // N dot Li +@@ -411,14 +418,14 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + float NdotL = dot(normal, light_vector); // N dot Li // Two sided diffuse - if (bitfieldExtract(GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) @@ -163,20 +172,20 @@ index 1b8e9751..96238000 100644 if (use_geo_0 || use_geo_1) { geometric_factor = dot(half_vector, half_vector); geometric_factor = geometric_factor == 0.0 ? 0.0 : min(NdotL / geometric_factor, 1.0); -@@ -399,9 +406,9 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -430,9 +437,9 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { // fragment and the distance attenuation scale and bias to calculate where in the LUT to look up. // See: https://www.3dbrew.org/wiki/GPU/Internal_Registers#GPUREG_LIGHTi_ATTENUATION_SCALE float distance_attenuation = 1.0; - if (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, 24 + int(light_id), 1) == 0u) { -- uint GPUREG_LIGHTi_ATTENUATION_BIAS = bitfieldExtract(readPicaReg(0x014Au), 0, 20); -- uint GPUREG_LIGHTi_ATTENUATION_SCALE = bitfieldExtract(readPicaReg(0x014Bu), 0, 20); +- uint GPUREG_LIGHTi_ATTENUATION_BIAS = bitfieldExtract(readPicaReg(0x014Au + (light_id << 4u)), 0, 20); +- uint GPUREG_LIGHTi_ATTENUATION_SCALE = bitfieldExtract(readPicaReg(0x014Bu + (light_id << 4u)), 0, 20); + if (bitfieldExtractCompat(GPUREG_LIGHTING_CONFIG1, 24 + int(light_id), 1) == 0u) { -+ uint GPUREG_LIGHTi_ATTENUATION_BIAS = bitfieldExtractCompat(readPicaReg(0x014Au), 0, 20); -+ uint GPUREG_LIGHTi_ATTENUATION_SCALE = bitfieldExtractCompat(readPicaReg(0x014Bu), 0, 20); ++ uint GPUREG_LIGHTi_ATTENUATION_BIAS = bitfieldExtractCompat(readPicaReg(0x014Au + (light_id << 4u)), 0, 20); ++ uint GPUREG_LIGHTi_ATTENUATION_SCALE = bitfieldExtractCompat(readPicaReg(0x014Bu + (light_id << 4u)), 0, 20); float distance_attenuation_bias = decodeFP(GPUREG_LIGHTi_ATTENUATION_BIAS, 7u, 12u); float distance_attenuation_scale = decodeFP(GPUREG_LIGHTi_ATTENUATION_SCALE, 7u, 12u); -@@ -446,8 +453,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -477,8 +484,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { specular_sum.rgb += light_factor * clamp_factor * (specular0 + specular1); } @@ -188,7 +197,7 @@ index 1b8e9751..96238000 100644 float fresnel_factor; diff --git a/src/host_shaders/opengl_vertex_shader.vert b/src/host_shaders/opengl_vertex_shader.vert -index a25d7a6d..7cf40398 100644 +index 057f9a88..dc735ced 100644 --- a/src/host_shaders/opengl_vertex_shader.vert +++ b/src/host_shaders/opengl_vertex_shader.vert @@ -1,4 +1,6 @@ @@ -199,7 +208,7 @@ index a25d7a6d..7cf40398 100644 layout(location = 0) in vec4 a_coords; layout(location = 1) in vec4 a_quaternion; -@@ -20,7 +22,7 @@ out vec2 v_texcoord2; +@@ -18,7 +20,7 @@ out vec2 v_texcoord2; flat out vec4 v_textureEnvColor[6]; flat out vec4 v_textureEnvBufferColor; @@ -208,7 +217,7 @@ index a25d7a6d..7cf40398 100644 // TEV uniforms uniform uint u_textureEnvColor[6]; -@@ -93,6 +95,6 @@ void main() { +@@ -81,8 +83,8 @@ void main() { ); // There's also another, always-on clipping plane based on vertex z @@ -216,6 +225,8 @@ index a25d7a6d..7cf40398 100644 - gl_ClipDistance[1] = dot(clipData, a_coords); + // gl_ClipDistance[0] = -a_coords.z; + // gl_ClipDistance[1] = dot(clipData, a_coords); + + v_quaternion = a_quaternion; } diff --git a/third_party/opengl/opengl.hpp b/third_party/opengl/opengl.hpp index 9997e63b..5d9d7804 100644 From ccf9693877a649ccb9c2b891b37b36b54d04ffbb Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 18 Jul 2024 02:57:41 +0300 Subject: [PATCH 090/251] Shadergen: More lighting work --- include/PICA/pica_frag_uniforms.hpp | 23 ++++++++++++++++++- include/PICA/shader_gen.hpp | 1 + src/core/PICA/shader_gen_glsl.cpp | 34 ++++++++++++++++++++++++---- src/core/renderer_gl/renderer_gl.cpp | 4 ++++ 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/include/PICA/pica_frag_uniforms.hpp b/include/PICA/pica_frag_uniforms.hpp index 332acd4e..0122ae93 100644 --- a/include/PICA/pica_frag_uniforms.hpp +++ b/include/PICA/pica_frag_uniforms.hpp @@ -1,12 +1,27 @@ #pragma once #include +#include #include #include "helpers.hpp" namespace PICA { - struct FragmentUniforms { + struct LightUniform { using vec3 = std::array; + + // std140 requires vec3s be aligned to 16 bytes + alignas(16) vec3 specular0; + alignas(16) vec3 specular1; + alignas(16) vec3 diffuse; + alignas(16) vec3 ambient; + alignas(16) vec3 position; + alignas(16) vec3 spotlightDirection; + + float distAttenuationBias; + float distanceAttenuationScale; + }; + + struct FragmentUniforms { using vec4 = std::array; static constexpr usize tevStageCount = 6; @@ -17,5 +32,11 @@ namespace PICA { alignas(16) vec4 constantColors[tevStageCount]; alignas(16) vec4 tevBufferColor; alignas(16) vec4 clipCoords; + + // NOTE: THIS MUST BE LAST so that if lighting is disabled we can potentially omit uploading it + LightUniform lightUniforms[8]; }; + + // Assert that lightUniforms is the last member of the structure + static_assert(offsetof(FragmentUniforms, lightUniforms) + 8 * sizeof(LightUniform) == sizeof(FragmentUniforms)); } // namespace PICA \ No newline at end of file diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index 0a6bca8e..21d85d98 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -26,6 +26,7 @@ namespace PICA::ShaderGen { void getAlphaOperation(std::string& shader, PICA::TexEnvConfig::Operation op); void applyAlphaTest(std::string& shader, const PICARegs& regs); + void compileLights(std::string& shader, const PICA::FragmentConfig& config); u32 textureConfig = 0; diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 5dbc3b81..9c319780 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -3,6 +3,17 @@ using namespace PICA; using namespace PICA::ShaderGen; static constexpr const char* uniformDefinition = R"( + struct LightSource { + vec3 specular0; + vec3 specular1; + vec3 diffuse; + vec3 ambient; + vec3 position; + vec3 spotlightDirection; + float distanceAttenuationBias; + float distanceAttenuationScale; + }; + layout(std140) uniform FragmentUniforms { int alphaReference; float depthScale; @@ -11,6 +22,8 @@ static constexpr const char* uniformDefinition = R"( vec4 constantColors[6]; vec4 tevBufferColor; vec4 clipCoords; + + LightSource lightSources[8]; }; )"; @@ -128,7 +141,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf uniform sampler2D u_tex2; // GLES doesn't support sampler1DArray, as such we'll have to change how we handle lighting later #ifndef USING_GLES - uniform sampler1DArray u_tex_lighting_lut; + uniform sampler2D u_tex_lighting_lut; #endif )"; @@ -140,9 +153,14 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf void main() { vec4 combinerOutput = v_colour; vec4 previousBuffer = vec4(0.0); - vec4 tevNextPreviousBuffer = tevBufferColor; + vec4 tevNextPreviousBuffer = tevBufferColor; + + vec4 primaryColor = vec4(0.0); + vec4 secondaryColor = vec4(0.0); )"; + compileLights(ret, config); + ret += R"( vec3 colorOp1 = vec3(0.0); vec3 colorOp2 = vec3(0.0); @@ -353,8 +371,8 @@ void FragmentGenerator::getSource(std::string& shader, TexEnvConfig::Source sour case TexEnvConfig::Source::PreviousBuffer: shader += "previousBuffer"; break; // Lighting - case TexEnvConfig::Source::PrimaryFragmentColor: - case TexEnvConfig::Source::SecondaryFragmentColor: shader += "vec4(1.0, 1.0, 1.0, 1.0)"; break; + case TexEnvConfig::Source::PrimaryFragmentColor: shader += "primaryColor"; break; + case TexEnvConfig::Source::SecondaryFragmentColor: shader += "secondaryColor"; break; default: Helpers::warn("Unimplemented TEV source: %d", static_cast(source)); @@ -430,3 +448,11 @@ void FragmentGenerator::applyAlphaTest(std::string& shader, const PICARegs& regs shader += ") { discard; }\n"; } + +void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentConfig& config) { + if (!config.lighting.enable) { + return; + } + + +} \ No newline at end of file diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index b85e7689..34ed0d22 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -875,6 +875,10 @@ OpenGL::Program& RendererGL::getSpecializedShader() { vec[3] = float((color >> 24) & 0xFF) / 255.0f; } + // Append lighting uniforms + if (fsConfig.lighting.enable) { + } + gl.bindUBO(programEntry.uboBinding); glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(PICA::FragmentUniforms), &uniforms); From 7e480e35ece212277f44844c26b5f628b156b514 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 18 Jul 2024 03:37:11 +0300 Subject: [PATCH 091/251] Shadergen: Upload light uniforms --- include/PICA/pica_frag_uniforms.hpp | 5 +++- include/PICA/regs.hpp | 13 +++++++++- src/core/PICA/shader_gen_glsl.cpp | 2 ++ src/core/renderer_gl/renderer_gl.cpp | 38 ++++++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 2 deletions(-) diff --git a/include/PICA/pica_frag_uniforms.hpp b/include/PICA/pica_frag_uniforms.hpp index 0122ae93..09722d61 100644 --- a/include/PICA/pica_frag_uniforms.hpp +++ b/include/PICA/pica_frag_uniforms.hpp @@ -17,11 +17,12 @@ namespace PICA { alignas(16) vec3 position; alignas(16) vec3 spotlightDirection; - float distAttenuationBias; + float distanceAttenuationBias; float distanceAttenuationScale; }; struct FragmentUniforms { + using vec3 = std::array; using vec4 = std::array; static constexpr usize tevStageCount = 6; @@ -33,6 +34,8 @@ namespace PICA { alignas(16) vec4 tevBufferColor; alignas(16) vec4 clipCoords; + // Note: We upload this as a u32 and decode on GPU + u32 globalAmbientLight; // NOTE: THIS MUST BE LAST so that if lighting is disabled we can potentially omit uploading it LightUniform lightUniforms[8]; }; diff --git a/include/PICA/regs.hpp b/include/PICA/regs.hpp index 312ac78b..bd1f823e 100644 --- a/include/PICA/regs.hpp +++ b/include/PICA/regs.hpp @@ -67,9 +67,20 @@ namespace PICA { ColourBufferLoc = 0x11D, FramebufferSize = 0x11E, - // Lighting registers LightingEnable = 0x8F, + Light0Specular0 = 0x140, + Light0Specular1 = 0x141, + Light0Diffuse = 0x142, + Light0Ambient = 0x143, + Light0XY = 0x144, + Light0Z = 0x145, + Light0SpotlightXY = 0x146, + Light0SpotlightZ = 0x147, + Light0AttenuationBias = 0x14A, + Light0AttenuationScale = 0x14B, + + LightGlobalAmbient = 0x1C0, LightNumber = 0x1C2, LightConfig0 = 0x1C3, LightConfig1 = 0x1C4, diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 9c319780..98a10bca 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -23,6 +23,8 @@ static constexpr const char* uniformDefinition = R"( vec4 tevBufferColor; vec4 clipCoords; + // Note: We upload this as a u32 and decode on GPU + uint globalAmbientLight; LightSource lightSources[8]; }; )"; diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 34ed0d22..5f599e9c 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -877,6 +877,44 @@ OpenGL::Program& RendererGL::getSpecializedShader() { // Append lighting uniforms if (fsConfig.lighting.enable) { + uniforms.globalAmbientLight = regs[InternalRegs::LightGlobalAmbient]; + for (int i = 0; i < 8; i++) { + auto& light = uniforms.lightUniforms[i]; + const u32 specular0 = regs[InternalRegs::Light0Specular0 + i * 0x10]; + const u32 specular1 = regs[InternalRegs::Light0Specular1 + i * 0x10]; + const u32 diffuse = regs[InternalRegs::Light0Diffuse + i * 0x10]; + const u32 ambient = regs[InternalRegs::Light0Ambient + i * 0x10]; + const u32 lightXY = regs[InternalRegs::Light0XY + i * 0x10]; + const u32 lightZ = regs[InternalRegs::Light0Z + i * 0x10]; + + const u32 spotlightXY = regs[InternalRegs::Light0SpotlightXY + i * 0x10]; + const u32 spotlightZ = regs[InternalRegs::Light0SpotlightZ + i * 0x10]; + const u32 attenuationBias = regs[InternalRegs::Light0AttenuationBias + i * 0x10]; + const u32 attenuationScale = regs[InternalRegs::Light0AttenuationScale + i * 0x10]; + +#define lightColorToVec3(value) \ + { \ + float(Helpers::getBits<20, 8>(value)) / 255.0f, \ + float(Helpers::getBits<10, 8>(value)) / 255.0f, \ + float(Helpers::getBits<0, 8>(value)) / 255.0f, \ + } + light.specular0 = lightColorToVec3(specular0); + light.specular1 = lightColorToVec3(specular1); + light.diffuse = lightColorToVec3(diffuse); + light.ambient = lightColorToVec3(ambient); + light.position[0] = Floats::f16::fromRaw(u16(lightXY)).toFloat32(); + light.position[1] = Floats::f16::fromRaw(u16(lightXY >> 16)).toFloat32(); + light.position[2] = Floats::f16::fromRaw(u16(lightXY)).toFloat32(); + + // Fixed point 1.11.1 to float, without negation + light.spotlightDirection[0] = float(s32(spotlightXY & 0x1FFF) << 19 >> 19) / 2047.0; + light.spotlightDirection[1] = float(s32((spotlightXY >> 16) & 0x1FFF) << 19 >> 19) / 2047.0; + light.spotlightDirection[2] = float(s32(spotlightZ & 0x1FFF) << 19 >> 19) / 2047.0; + + light.distanceAttenuationBias = Floats::f20::fromRaw(attenuationBias & 0xFFFFF).toFloat32(); + light.distanceAttenuationScale = Floats::f20::fromRaw(attenuationScale & 0xFFFFF).toFloat32(); +#undef lightColorToVec3 + } } gl.bindUBO(programEntry.uboBinding); From e1268f57b567b4cf7da5822298978a244056687b Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 18 Jul 2024 04:21:00 +0300 Subject: [PATCH 092/251] Shadergen: Fix attribute declarations --- src/core/PICA/shader_gen_glsl.cpp | 28 +++++++++----------- src/host_shaders/opengl_fragment_shader.frag | 2 +- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 98a10bca..fea9786e 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -59,9 +59,7 @@ std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { layout(location = 6) in vec3 a_view; layout(location = 7) in vec2 a_texcoord2; - out vec3 v_normal; - out vec3 v_tangent; - out vec3 v_bitangent; + out vec4 v_quaternion; out vec4 v_colour; out vec3 v_texcoord0; out vec2 v_texcoord1; @@ -77,12 +75,6 @@ std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { return scale * vec4(float(abgr & 0xffu), float((abgr >> 8) & 0xffu), float((abgr >> 16) & 0xffu), float(abgr >> 24)); } - vec3 rotateVec3ByQuaternion(vec3 v, vec4 q) { - vec3 u = q.xyz; - float s = q.w; - return 2.0 * dot(u, v) * u + (s * s - dot(u, u)) * v + 2.0 * s * cross(u, v); - } - void main() { gl_Position = a_coords; vec4 colourAbs = abs(a_vertexColour); @@ -92,10 +84,7 @@ std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { v_texcoord1 = vec2(a_texcoord1.x, 1.0 - a_texcoord1.y); v_texcoord2 = vec2(a_texcoord2.x, 1.0 - a_texcoord2.y); v_view = a_view; - - v_normal = normalize(rotateVec3ByQuaternion(vec3(0.0, 0.0, 1.0), a_quaternion)); - v_tangent = normalize(rotateVec3ByQuaternion(vec3(1.0, 0.0, 0.0), a_quaternion)); - v_bitangent = normalize(rotateVec3ByQuaternion(vec3(0.0, 1.0, 0.0), a_quaternion)); + v_quaternion = a_quaternion; #ifndef USING_GLES gl_ClipDistance[0] = -a_coords.z; @@ -128,9 +117,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf // Input and output attributes ret += R"( - in vec3 v_tangent; - in vec3 v_normal; - in vec3 v_bitangent; + in vec4 v_quaternion; in vec4 v_colour; in vec3 v_texcoord0; in vec2 v_texcoord1; @@ -148,6 +135,15 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf )"; ret += uniformDefinition; + if (config.lighting.enable) { + ret += R"( + vec3 rotateVec3ByQuaternion(vec3 v, vec4 q) { + vec3 u = q.xyz; + float s = q.w; + return 2.0 * dot(u, v) * u + (s * s - dot(u, u)) * v + 2.0 * s * cross(u, v); + } + )"; + } // Emit main function for fragment shader // When not initialized, source 13 is set to vec4(0.0) and 15 is set to the vertex colour diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index 23c5c4cb..e42d8e57 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -586,4 +586,4 @@ void main() { break; } } -} +} \ No newline at end of file From 6279ce3df28baeba4f65e0f93dec57a33853d179 Mon Sep 17 00:00:00 2001 From: Paris Oplopoios Date: Thu, 18 Jul 2024 04:54:28 +0300 Subject: [PATCH 093/251] Move comments to docs, sign extend stuff for Android (#539) * Move documentation, sign extend spot dir * Update gles.patch * Fix compilation errors * Update gles.patch --- .github/gles.patch | 66 ++++++++-------- docs/3ds/lighting.md | 79 +++++++++++++++++++ src/host_shaders/opengl_fragment_shader.frag | 83 ++++---------------- 3 files changed, 127 insertions(+), 101 deletions(-) create mode 100644 docs/3ds/lighting.md diff --git a/.github/gles.patch b/.github/gles.patch index 99258011..a27b3d00 100644 --- a/.github/gles.patch +++ b/.github/gles.patch @@ -21,7 +21,7 @@ index 990e2f80..2e7842ac 100644 void main() { diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag -index 23c5c4cb..a9851a8b 100644 +index b4ad7ecc..98b1bd80 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -1,4 +1,5 @@ @@ -31,7 +31,7 @@ index 23c5c4cb..a9851a8b 100644 in vec4 v_quaternion; in vec4 v_colour; -@@ -189,11 +190,17 @@ float lutLookup(uint lut, int index) { +@@ -164,11 +165,17 @@ float lutLookup(uint lut, int index) { return texelFetch(u_tex_lighting_lut, ivec2(index, lut), 0).r; } @@ -50,8 +50,8 @@ index 23c5c4cb..a9851a8b 100644 } // Convert an arbitrary-width floating point literal to an f32 -@@ -257,16 +264,16 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light - // If RR is enabled but not RG or RB, the output of RR is used for the three components; Red, Green and Blue. +@@ -208,16 +215,16 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light + bool current_sampler_enabled = isSamplerEnabled(environment_id, lut_id); // 7 luts per environment - if (!current_sampler_enabled || (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, bit_in_config1, 1) != 0u)) { @@ -70,26 +70,23 @@ index 23c5c4cb..a9851a8b 100644 switch (input_id) { case 0u: { delta = dot(normal, normalize(half_vector)); -@@ -285,14 +292,14 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light - break; - } - case 4u: { -- // These are ints so that bitfieldExtract sign extends for us -+ // These are ints so that bitfieldExtractCompat sign extends for us +@@ -239,11 +246,11 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light int GPUREG_LIGHTi_SPOTDIR_LOW = int(readPicaReg(0x0146u + (light_id << 4u))); int GPUREG_LIGHTi_SPOTDIR_HIGH = int(readPicaReg(0x0147u + (light_id << 4u))); - // These are fixed point 1.1.11 values, so we need to convert them to float -- float x = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13)) / 2047.0; -- float y = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13)) / 2047.0; -- float z = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13)) / 2047.0; -+ float x = float(bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13)) / 2047.0; -+ float y = float(bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13)) / 2047.0; -+ float z = float(bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13)) / 2047.0; - vec3 spotlight_vector = vec3(x, y, z); - delta = dot(light_vector, spotlight_vector); // spotlight direction is negated so we don't negate light_vector - break; -@@ -310,9 +317,9 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light +- // Sign extend them. Normally bitfieldExtract would do that but it's missing on some versions ++ // Sign extend them. Normally bitfieldExtractCompat would do that but it's missing on some versions + // of GLSL so we do it manually +- int se_x = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13); +- int se_y = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13); +- int se_z = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13); ++ int se_x = bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13); ++ int se_y = bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13); ++ int se_z = bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13); + + if ((se_x & 0x1000) == 0x1000) se_x |= 0xffffe000; + if ((se_y & 0x1000) == 0x1000) se_y |= 0xffffe000; +@@ -270,9 +277,9 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light } // 0 = enabled @@ -101,16 +98,16 @@ index 23c5c4cb..a9851a8b 100644 delta = max(delta, 0.0); } else { delta = abs(delta); -@@ -339,7 +346,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { - unimpl_color = vec4(1.0, 0.0, 1.0, 1.0); - +@@ -296,7 +303,7 @@ vec3 rotateVec3ByQuaternion(vec3 v, vec4 q) { + // Implements the following algorthm: https://mathb.in/26766 + void calcLighting(out vec4 primary_color, out vec4 secondary_color) { uint GPUREG_LIGHTING_ENABLE = readPicaReg(0x008Fu); - if (bitfieldExtract(GPUREG_LIGHTING_ENABLE, 0, 1) == 0u) { + if (bitfieldExtractCompat(GPUREG_LIGHTING_ENABLE, 0, 1) == 0u) { primary_color = secondary_color = vec4(0.0); return; } -@@ -356,7 +363,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -313,7 +320,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { GPUREG_LIGHTING_LUTINPUT_ABS = readPicaReg(0x01D0u); GPUREG_LIGHTING_LUTINPUT_SELECT = readPicaReg(0x01D1u); @@ -118,8 +115,8 @@ index 23c5c4cb..a9851a8b 100644 + uint bump_mode = bitfieldExtractCompat(GPUREG_LIGHTING_CONFIG0, 28, 2); // Bump mode is ignored for now because it breaks some games ie. Toad Treasure Tracker - // Could be because the texture is not sampled correctly, may need the clamp/border color configurations -@@ -370,15 +377,15 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { + switch (bump_mode) { +@@ -326,15 +333,15 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { vec4 diffuse_sum = vec4(0.0, 0.0, 0.0, 1.0); vec4 specular_sum = vec4(0.0, 0.0, 0.0, 1.0); @@ -138,7 +135,7 @@ index 23c5c4cb..a9851a8b 100644 uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140u + (light_id << 4u)); uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141u + (light_id << 4u)); -@@ -390,12 +397,12 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -346,12 +353,12 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { float light_distance; vec3 light_position = vec3( @@ -154,7 +151,7 @@ index 23c5c4cb..a9851a8b 100644 light_vector = light_position + v_view; } -@@ -411,14 +418,14 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -367,23 +374,23 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { float NdotL = dot(normal, light_vector); // N dot Li // Two sided diffuse @@ -172,9 +169,8 @@ index 23c5c4cb..a9851a8b 100644 if (use_geo_0 || use_geo_1) { geometric_factor = dot(half_vector, half_vector); geometric_factor = geometric_factor == 0.0 ? 0.0 : min(NdotL / geometric_factor, 1.0); -@@ -430,9 +437,9 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { - // fragment and the distance attenuation scale and bias to calculate where in the LUT to look up. - // See: https://www.3dbrew.org/wiki/GPU/Internal_Registers#GPUREG_LIGHTi_ATTENUATION_SCALE + } + float distance_attenuation = 1.0; - if (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, 24 + int(light_id), 1) == 0u) { - uint GPUREG_LIGHTi_ATTENUATION_BIAS = bitfieldExtract(readPicaReg(0x014Au + (light_id << 4u)), 0, 20); @@ -185,7 +181,7 @@ index 23c5c4cb..a9851a8b 100644 float distance_attenuation_bias = decodeFP(GPUREG_LIGHTi_ATTENUATION_BIAS, 7u, 12u); float distance_attenuation_scale = decodeFP(GPUREG_LIGHTi_ATTENUATION_SCALE, 7u, 12u); -@@ -477,8 +484,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -428,8 +435,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { specular_sum.rgb += light_factor * clamp_factor * (specular0 + specular1); } @@ -229,10 +225,10 @@ index 057f9a88..dc735ced 100644 v_quaternion = a_quaternion; } diff --git a/third_party/opengl/opengl.hpp b/third_party/opengl/opengl.hpp -index 9997e63b..5d9d7804 100644 +index 828fb784..a1861b77 100644 --- a/third_party/opengl/opengl.hpp +++ b/third_party/opengl/opengl.hpp -@@ -561,22 +561,22 @@ namespace OpenGL { +@@ -568,22 +568,22 @@ namespace OpenGL { static void disableScissor() { glDisable(GL_SCISSOR_TEST); } static void enableBlend() { glEnable(GL_BLEND); } static void disableBlend() { glDisable(GL_BLEND); } diff --git a/docs/3ds/lighting.md b/docs/3ds/lighting.md new file mode 100644 index 00000000..9f4ff2f2 --- /dev/null +++ b/docs/3ds/lighting.md @@ -0,0 +1,79 @@ +## Info on the lighting implementation + +### Missing shadow attenuation +Shadow attenuation samples a texture unit, and that likely needs render to texture for most games so that they can construct +their shadow map. As such the colors are not multiplied by the shadow attenuation value, so there's no shadows. + +### Missing bump mapping +Bump mapping also samples a texture unit, most likely doesn't need render to texture however may need better texture sampling +implementation (such as GPUREG_TEXUNITi_BORDER_COLOR, GPUREG_TEXUNITi_BORDER_PARAM). Bump mapping would work for some things, +namely the 3ds-examples bump mapping demo, but would break others such as Toad Treasure Tracker with a naive `texture` implementation. + +Also the CP configuration is missing, because it needs a tangent map implementation. It is currently marked with error_unimpl. + +### samplerEnabledBitfields +Holds the enabled state of the lighting samples for various PICA configurations +As explained in https://www.3dbrew.org/wiki/GPU/Internal_Registers#GPUREG_LIGHTING_CONFIG0 + +```c +const bool samplerEnabled[9 * 7] = bool[9 * 7]( + // D0 D1 SP FR RB RG RR + true, false, true, false, false, false, true, // Configuration 0: D0, SP, RR + false, false, true, true, false, false, true, // Configuration 1: FR, SP, RR + true, true, false, false, false, false, true, // Configuration 2: D0, D1, RR + true, true, false, true, false, false, false, // Configuration 3: D0, D1, FR + true, true, true, false, true, true, true, // Configuration 4: All except for FR + true, false, true, true, true, true, true, // Configuration 5: All except for D1 + true, true, true, true, false, false, true, // Configuration 6: All except for RB and RG + false, false, false, false, false, false, false, // Configuration 7: Unused + true, true, true, true, true, true, true // Configuration 8: All +); +``` + +The above has been condensed to two uints for performance reasons. +You can confirm they are the same by running the following: +```c +const uint samplerEnabledBitfields[2] = { 0x7170e645u, 0x7f013fefu }; +for (int i = 0; i < 9 * 7; i++) { + unsigned arrayIndex = (i >> 5); + bool b = (samplerEnabledBitfields[arrayIndex] & (1u << (i & 31))) != 0u; + if (samplerEnabled[i] == b) { + printf("%d: happy\n", i); + } else { + printf("%d: unhappy\n", i); + } +} +``` + +### lightLutLookup +lut_id is one of these values +0 D0 +1 D1 +2 SP +3 FR +4 RB +5 RG +6 RR + +lut_index on the other hand represents the actual index of the LUT in the texture +u_tex_lighting_lut has 24 LUTs and they are used like so: +0 D0 +1 D1 +2 is missing because SP uses LUTs 8-15 +3 FR +4 RB +5 RG +6 RR +8-15 SP0-7 +16-23 DA0-7, but this is not handled in this function as the lookup is a bit different + +The light environment configuration controls which LUTs are available for use +If a LUT is not available in the selected configuration, its value will always read a constant 1.0 regardless of the enable state in GPUREG_LIGHTING_CONFIG1 +If RR is enabled but not RG or RB, the output of RR is used for the three components; Red, Green and Blue. + +### Distance attenuation +Distance attenuation is computed differently from the other factors, for example +it doesn't store its scale in GPUREG_LIGHTING_LUTINPUT_SCALE and it doesn't use +GPUREG_LIGHTING_LUTINPUT_SELECT. Instead, it uses the distance from the light to the +fragment and the distance attenuation scale and bias to calculate where in the LUT to look up. +See: https://www.3dbrew.org/wiki/GPU/Internal_Registers#GPUREG_LIGHTi_ATTENUATION_SCALE \ No newline at end of file diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index e42d8e57..6f30ebf0 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -37,38 +37,13 @@ vec4 tevNextPreviousBuffer; bool tevUnimplementedSourceFlag = false; vec3 normal; -// Holds the enabled state of the lighting samples for various PICA configurations -// As explained in https://www.3dbrew.org/wiki/GPU/Internal_Registers#GPUREG_LIGHTING_CONFIG0 -// const bool samplerEnabled[9 * 7] = bool[9 * 7]( -// // D0 D1 SP FR RB RG RR -// true, false, true, false, false, false, true, // Configuration 0: D0, SP, RR -// false, false, true, true, false, false, true, // Configuration 1: FR, SP, RR -// true, true, false, false, false, false, true, // Configuration 2: D0, D1, RR -// true, true, false, true, false, false, false, // Configuration 3: D0, D1, FR -// true, true, true, false, true, true, true, // Configuration 4: All except for FR -// true, false, true, true, true, true, true, // Configuration 5: All except for D1 -// true, true, true, true, false, false, true, // Configuration 6: All except for RB and RG -// false, false, false, false, false, false, false, // Configuration 7: Unused -// true, true, true, true, true, true, true // Configuration 8: All -// ); - -// The above have been condensed to two uints to save space -// You can confirm they are the same by running the following: -// for (int i = 0; i < 9 * 7; i++) { -// unsigned arrayIndex = (i >> 5); -// bool b = (samplerEnabledBitfields[arrayIndex] & (1u << (i & 31))) != 0u; -// if (samplerEnabled[i] == b) { -// printf("%d: happy\n", i); -// } else { -// printf("%d: unhappy\n", i); -// } -// } +// See docs/lighting.md const uint samplerEnabledBitfields[2] = uint[2](0x7170e645u, 0x7f013fefu); bool isSamplerEnabled(uint environment_id, uint lut_id) { uint index = 7 * environment_id + lut_id; uint arrayIndex = (index >> 5); - return (samplerEnabledBitfields[arrayIndex] & (1u << (index & 31))) != 0u; + return (samplerEnabledBitfields[arrayIndex] & (1u << (index & 31u))) != 0u; } // OpenGL ES 1.1 reference pages for TEVs (this is what the PICA200 implements): @@ -182,8 +157,8 @@ uint GPUREG_LIGHTING_CONFIG1; uint GPUREG_LIGHTING_LUTINPUT_SELECT; uint GPUREG_LIGHTING_LUTINPUT_SCALE; uint GPUREG_LIGHTING_LUTINPUT_ABS; -bool error_unimpl; -vec4 unimpl_color; +bool error_unimpl = false; +vec4 unimpl_color = vec4(1.0, 0.0, 1.0, 1.0); float lutLookup(uint lut, int index) { return texelFetch(u_tex_lighting_lut, ivec2(index, lut), 0).r; @@ -219,27 +194,6 @@ float decodeFP(uint hex, uint E, uint M) { float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light_vector, vec3 half_vector) { uint lut_index; - // lut_id is one of these values - // 0 D0 - // 1 D1 - // 2 SP - // 3 FR - // 4 RB - // 5 RG - // 6 RR - - // lut_index on the other hand represents the actual index of the LUT in the texture - // u_tex_lighting_lut has 24 LUTs and they are used like so: - // 0 D0 - // 1 D1 - // 2 is missing because SP uses LUTs 8-15 - // 3 FR - // 4 RB - // 5 RG - // 6 RR - // 8-15 SP0-7 - // 16-23 DA0-7, but this is not handled in this function as the lookup is a bit different - int bit_in_config1; if (lut_id == SP_LUT) { // These are the spotlight attenuation LUTs @@ -252,9 +206,6 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light error_unimpl = true; } - // The light environment configuration controls which LUTs are available for use - // If a LUT is not available in the selected configuration, its value will always read a constant 1.0 regardless of the enable state in GPUREG_LIGHTING_CONFIG1 - // If RR is enabled but not RG or RB, the output of RR is used for the three components; Red, Green and Blue. bool current_sampler_enabled = isSamplerEnabled(environment_id, lut_id); // 7 luts per environment if (!current_sampler_enabled || (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, bit_in_config1, 1) != 0u)) { @@ -285,14 +236,23 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light break; } case 4u: { - // These are ints so that bitfieldExtract sign extends for us int GPUREG_LIGHTi_SPOTDIR_LOW = int(readPicaReg(0x0146u + (light_id << 4u))); int GPUREG_LIGHTi_SPOTDIR_HIGH = int(readPicaReg(0x0147u + (light_id << 4u))); + // Sign extend them. Normally bitfieldExtract would do that but it's missing on some versions + // of GLSL so we do it manually + int se_x = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13); + int se_y = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13); + int se_z = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13); + + if ((se_x & 0x1000) == 0x1000) se_x |= 0xffffe000; + if ((se_y & 0x1000) == 0x1000) se_y |= 0xffffe000; + if ((se_z & 0x1000) == 0x1000) se_z |= 0xffffe000; + // These are fixed point 1.1.11 values, so we need to convert them to float - float x = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13)) / 2047.0; - float y = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13)) / 2047.0; - float z = float(bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13)) / 2047.0; + float x = float(se_x) / 2047.0; + float y = float(se_y) / 2047.0; + float z = float(se_z) / 2047.0; vec3 spotlight_vector = vec3(x, y, z); delta = dot(light_vector, spotlight_vector); // spotlight direction is negated so we don't negate light_vector break; @@ -335,9 +295,6 @@ vec3 rotateVec3ByQuaternion(vec3 v, vec4 q) { // Implements the following algorthm: https://mathb.in/26766 void calcLighting(out vec4 primary_color, out vec4 secondary_color) { - error_unimpl = false; - unimpl_color = vec4(1.0, 0.0, 1.0, 1.0); - uint GPUREG_LIGHTING_ENABLE = readPicaReg(0x008Fu); if (bitfieldExtract(GPUREG_LIGHTING_ENABLE, 0, 1) == 0u) { primary_color = secondary_color = vec4(0.0); @@ -359,7 +316,6 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { uint bump_mode = bitfieldExtract(GPUREG_LIGHTING_CONFIG0, 28, 2); // Bump mode is ignored for now because it breaks some games ie. Toad Treasure Tracker - // Could be because the texture is not sampled correctly, may need the clamp/border color configurations switch (bump_mode) { default: { normal = rotateVec3ByQuaternion(vec3(0.0, 0.0, 1.0), v_quaternion); @@ -424,11 +380,6 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { geometric_factor = geometric_factor == 0.0 ? 0.0 : min(NdotL / geometric_factor, 1.0); } - // Distance attenuation is computed differently from the other factors, for example - // it doesn't store its scale in GPUREG_LIGHTING_LUTINPUT_SCALE and it doesn't use - // GPUREG_LIGHTING_LUTINPUT_SELECT. Instead, it uses the distance from the light to the - // fragment and the distance attenuation scale and bias to calculate where in the LUT to look up. - // See: https://www.3dbrew.org/wiki/GPU/Internal_Registers#GPUREG_LIGHTi_ATTENUATION_SCALE float distance_attenuation = 1.0; if (bitfieldExtract(GPUREG_LIGHTING_CONFIG1, 24 + int(light_id), 1) == 0u) { uint GPUREG_LIGHTi_ATTENUATION_BIAS = bitfieldExtract(readPicaReg(0x014Au + (light_id << 4u)), 0, 20); From 00037d8a5e0a99c20405e2d54628dd071628599d Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 18 Jul 2024 19:27:12 +0300 Subject: [PATCH 094/251] Shadergen: Start implementing lighting --- include/PICA/shader_gen.hpp | 1 + src/core/PICA/shader_gen_glsl.cpp | 95 +++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index 21d85d98..c74e6953 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -27,6 +27,7 @@ namespace PICA::ShaderGen { void applyAlphaTest(std::string& shader, const PICARegs& regs); void compileLights(std::string& shader, const PICA::FragmentConfig& config); + void compileLUTLookup(std::string& shader, u32 lightIndex, u32 lutIndex, bool abs); u32 textureConfig = 0; diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index fea9786e..96c44ec2 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -135,6 +135,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf )"; ret += uniformDefinition; + if (config.lighting.enable) { ret += R"( vec3 rotateVec3ByQuaternion(vec3 v, vec4 q) { @@ -142,6 +143,10 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf float s = q.w; return 2.0 * dot(u, v) * u + (s * s - dot(u, u)) * v + 2.0 * s * cross(u, v); } + + float lutLookup(uint lut, int index) { + return texelFetch(u_tex_lighting_lut, ivec2(index, lut), 0).r; + } )"; } @@ -452,5 +457,95 @@ void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentC return; } + // Currently ignore bump mode + shader += "vec3 normal = rotateVec3ByQuaternion(vec3(0.0, 0.0, 1.0), v_quaternion);\n"; + shader += R"( + vec4 diffuse_sum = vec4(0.0, 0.0, 0.0, 1.0); + vec4 specular_sum = vec4(0.0, 0.0, 0.0, 1.0); + vec3 light_position, light_vector, half_vector, specular0, specular1, light_factor; + float light_distance, NdotL, geometric_factor, distance_attenuation, distance_att_delta; + float spotlight_attenuation, specular0_dist, specular1_dist, reflected_color; + )"; + + for (int i = 0; i < config.lighting.lightNum; i++) { + const auto& lightConfig = config.lighting.lights[i]; + shader += "light_position = lightSources[" + std::to_string(i) + "].position;\n"; + + if (lightConfig.directional) { // Directional lighting + shader += "light_vector = light_position + v_view;\n"; + } else { // Positional lighting + shader += "light_vector = light_position;\n"; + } + + shader += R"( + light_distance = length(light_vector); + light_vector = normalize(light_vector); + half_vector = light_vector + normalize(v_view); + + distance_attenuation = 1.0; + NdotL = dot(normal, light_vector); + )"; + + shader += lightConfig.twoSidedDiffuse ? "NdotL = abs(NdotL);\n" : "NdotL = max(NdotL, 0.0);\n"; + + if (lightConfig.geometricFactor0 || lightConfig.geometricFactor1) { + shader += R"( + geometric_factor = dot(half_vector, half_vector); + geometric_factor = (geometric_factor == 0.0) ? 0.0 : min(NdotL / geometric_factor, 1.0); + )"; + } + + if (lightConfig.distanceAttenuationEnable) { + shader += "distance_att_delta = clamp(light_distance * lightSources[" + std::to_string(i) + "].distanceAttenuationScale + lightSources[" + + std::to_string(i) + "].distanceAttenuationBias, 0.0, 1.0);\n"; + + shader += + "distance_attenuation = lutLookup(" + std::to_string(16 + i) + ", int(clamp(floor(distance_att_delta * 255.0), 0.0, 255.0)));\n"; + } + + // TODO: LightLutLookup stuff + shader += "spotlight_attenuation = 0.0; // Placeholder\n"; + shader += "specular0_dist = 0.0; // Placeholder\n"; + shader += "specular1_dist = 0.0; // Placeholder\n"; + shader += "reflected_color = vec3(0.0); // Placeholder\n"; + + shader += "specular0 = lightSources[" + std::to_string(i) + "].specular0;\n"; + shader += "specular0 = specular0 * specular0_dist"; + if (lightConfig.geometricFactor0) { + shader += " * geometric_factor;\n"; + } else { + shader += ";\n"; + } + + shader += "specular1 = lightSources[" + std::to_string(i) + "].specular1;\n"; + shader += "specular1 = specular1 * specular1_dist * reflected_color"; + if (lightConfig.geometricFactor1) { + shader += " * geometric_factor;\n"; + } else { + shader += ";\n"; + } + + shader += "light_factor = distance_attenuation * spotlight_attenuation;\n"; + + if (config.lighting.clampHighlights) { + shader += "specular_sum.rgb += light_factor * (NdotL == 0.0 ? 0.0 : 1.0) * (specular0 + specular1);\n"; + } else { + shader += "specular_sum.rgb += light_factor * (specular0 + specular1);\n"; + } + + shader += "diffuse_sum.rgb += vec3(0.0); // Placeholder\n"; + } + + // TODO: Rest of the post-per-light stuff + shader += R"( + vec4 global_ambient = vec4(regToColor(GPUREG_LIGHTING_AMBIENT), 1.0); + + primaryColor = clamp(global_ambient + diffuse_sum, vec4(0.0), vec4(1.0)); + secondaryColor = clamp(specular_sum, vec4(0.0), vec4(1.0)); + )"; +} + +void FragmentGenerator::compileLUTLookup(std::string& shader, u32 lightIndex, u32 lutIndex, bool abs) { + // TODO } \ No newline at end of file From b4ae32960c121b0125721388267d2e657aa61828 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 18 Jul 2024 20:10:20 +0300 Subject: [PATCH 095/251] Moar lighting --- include/PICA/regs.hpp | 3 +- include/PICA/shader_gen.hpp | 3 +- src/core/PICA/shader_gen_glsl.cpp | 47 ++++++++++++++++++++++++++++++- 3 files changed, 50 insertions(+), 3 deletions(-) diff --git a/include/PICA/regs.hpp b/include/PICA/regs.hpp index bd1f823e..2482c25b 100644 --- a/include/PICA/regs.hpp +++ b/include/PICA/regs.hpp @@ -252,7 +252,8 @@ namespace PICA { enum : u32 { LUT_D0 = 0, LUT_D1, - LUT_FR, + // LUT 2 is not used, the emulator internally uses it for referring to the current source's spotlight in shaders + LUT_FR = 0x3, LUT_RB, LUT_RG, LUT_RR, diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index c74e6953..1d4d07c5 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -27,7 +27,8 @@ namespace PICA::ShaderGen { void applyAlphaTest(std::string& shader, const PICARegs& regs); void compileLights(std::string& shader, const PICA::FragmentConfig& config); - void compileLUTLookup(std::string& shader, u32 lightIndex, u32 lutIndex, bool abs); + void compileLUTLookup(std::string& shader, const PICA::FragmentConfig& config, const PICARegs& regs, u32 lightIndex, u32 lutID, bool abs); + bool isSamplerEnabled(u32 environmentID, u32 lutID); u32 textureConfig = 0; diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 96c44ec2..cc01fad1 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -29,6 +29,11 @@ static constexpr const char* uniformDefinition = R"( }; )"; +// There's actually 8 different LUTs (SP0-SP7), one for each light with different indices (8-15) +// We use an unused LUT value for "this light source's spotlight" instead and figure out which light source to use in compileLutLookup +// This is particularly intuitive in several places, such as checking if a LUT is enabled +static constexpr int spotlightLutIndex = 2; + std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { std::string ret = ""; @@ -546,6 +551,46 @@ void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentC )"; } -void FragmentGenerator::compileLUTLookup(std::string& shader, u32 lightIndex, u32 lutIndex, bool abs) { +bool FragmentGenerator::isSamplerEnabled(u32 environmentID, u32 lutID) { + static constexpr bool samplerEnabled[9 * 7] = { + // D0 D1 SP FR RB RG RR + true, false, true, false, false, false, true, // Configuration 0: D0, SP, RR + false, false, true, true, false, false, true, // Configuration 1: FR, SP, RR + true, true, false, false, false, false, true, // Configuration 2: D0, D1, RR + true, true, false, true, false, false, false, // Configuration 3: D0, D1, FR + true, true, true, false, true, true, true, // Configuration 4: All except for FR + true, false, true, true, true, true, true, // Configuration 5: All except for D1 + true, true, true, true, false, false, true, // Configuration 6: All except for RB and RG + false, false, false, false, false, false, false, // Configuration 7: Unused + true, true, true, true, true, true, true, // Configuration 8: All + }; + + return samplerEnabled[environmentID * 7 + lutID]; +} + +void FragmentGenerator::compileLUTLookup( + std::string& shader, const PICA::FragmentConfig& config, const PICARegs& regs, u32 lightIndex, u32 lutID, bool abs +) { + uint lutIndex = 0; + int bitInConfig1 = 0; + + if (lutID == spotlightLutIndex) { + // These are the spotlight attenuation LUTs + bitInConfig1 = 8 + (lightIndex & 0x7); + lutIndex = 8u + lightIndex; + } else if (lutID <= 6) { + bitInConfig1 = 16 + lutID; + lutIndex = lutID; + } else { + Helpers::warn("Shadergen: Unimplemented LUT value"); + } + + const bool samplerEnabled = isSamplerEnabled(config.lighting.config, lutID); + const u32 config1 = regs[InternalRegs::LightConfig1]; + + if (!samplerEnabled || ((config1 >> bitInConfig1) != 0)) { + // 1.0 + } + // TODO } \ No newline at end of file From 2f50038db9967821ec9a2cf22f0b44ac293b26ae Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 18 Jul 2024 22:56:05 +0300 Subject: [PATCH 096/251] Shadergen: Lighting almost 100% working --- include/PICA/pica_frag_config.hpp | 2 +- include/PICA/shader_gen.hpp | 4 +- src/core/PICA/shader_gen_glsl.cpp | 146 ++++++++++++++++++++------- src/core/renderer_gl/renderer_gl.cpp | 2 +- 4 files changed, 111 insertions(+), 43 deletions(-) diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index cfb22e5c..cdb68854 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -140,7 +140,7 @@ namespace PICA { const u32 lutAbs = regs[InternalRegs::LightLUTAbs]; const u32 lutSelect = regs[InternalRegs::LightLUTSelect]; const u32 lutScale = regs[InternalRegs::LightLUTScale]; - static constexpr float scales[] = {1.0f, 2.0f, 4.0f, 8.0f, 0.25f, 0.5f}; + static constexpr float scales[] = {1.0f, 2.0f, 4.0f, 8.0f, 0.0f, 0.0f, 0.25f, 0.5f}; if (d0.enable) { d0.absInput = Helpers::getBit<1>(lutAbs) == 0; diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index 1d4d07c5..372e0550 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -26,8 +26,8 @@ namespace PICA::ShaderGen { void getAlphaOperation(std::string& shader, PICA::TexEnvConfig::Operation op); void applyAlphaTest(std::string& shader, const PICARegs& regs); - void compileLights(std::string& shader, const PICA::FragmentConfig& config); - void compileLUTLookup(std::string& shader, const PICA::FragmentConfig& config, const PICARegs& regs, u32 lightIndex, u32 lutID, bool abs); + void compileLights(std::string& shader, const PICA::FragmentConfig& config, const PICARegs& regs); + void compileLUTLookup(std::string& shader, const PICA::FragmentConfig& config, const PICARegs& regs, u32 lightIndex, u32 lutID); bool isSamplerEnabled(u32 environmentID, u32 lutID); u32 textureConfig = 0; diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index cc01fad1..8d955d50 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -133,10 +133,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf uniform sampler2D u_tex0; uniform sampler2D u_tex1; uniform sampler2D u_tex2; - // GLES doesn't support sampler1DArray, as such we'll have to change how we handle lighting later -#ifndef USING_GLES uniform sampler2D u_tex_lighting_lut; -#endif )"; ret += uniformDefinition; @@ -152,6 +149,10 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf float lutLookup(uint lut, int index) { return texelFetch(u_tex_lighting_lut, ivec2(index, lut), 0).r; } + + vec3 regToColor(uint reg) { + return (1.0 / 255.0) * vec3(float((reg >> 20) & 0xFF), float((reg >> 10) & 0xFF), float(reg & 0xFF)); + } )"; } @@ -167,7 +168,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf vec4 secondaryColor = vec4(0.0); )"; - compileLights(ret, config); + compileLights(ret, config, regs); ret += R"( vec3 colorOp1 = vec3(0.0); @@ -457,7 +458,7 @@ void FragmentGenerator::applyAlphaTest(std::string& shader, const PICARegs& regs shader += ") { discard; }\n"; } -void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentConfig& config) { +void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentConfig& config, const PICARegs& regs) { if (!config.lighting.enable) { return; } @@ -467,15 +468,21 @@ void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentC shader += R"( vec4 diffuse_sum = vec4(0.0, 0.0, 0.0, 1.0); vec4 specular_sum = vec4(0.0, 0.0, 0.0, 1.0); - vec3 light_position, light_vector, half_vector, specular0, specular1, light_factor; + vec3 light_position, light_vector, half_vector, specular0, specular1, reflected_color; - float light_distance, NdotL, geometric_factor, distance_attenuation, distance_att_delta; - float spotlight_attenuation, specular0_dist, specular1_dist, reflected_color; + float light_distance, NdotL, light_factor, geometric_factor, distance_attenuation, distance_att_delta; + float spotlight_attenuation, specular0_dist, specular1_dist; + float lut_lookup_result, lut_lookup_delta; + int lut_lookup_index; )"; + uint lightID = 0;; + for (int i = 0; i < config.lighting.lightNum; i++) { - const auto& lightConfig = config.lighting.lights[i]; - shader += "light_position = lightSources[" + std::to_string(i) + "].position;\n"; + lightID = config.lighting.lights[i].num; + + const auto& lightConfig = config.lighting.lights[lightID]; + shader += "light_position = lightSources[" + std::to_string(lightID) + "].position;\n"; if (lightConfig.directional) { // Directional lighting shader += "light_vector = light_position + v_view;\n"; @@ -502,49 +509,76 @@ void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentC } if (lightConfig.distanceAttenuationEnable) { - shader += "distance_att_delta = clamp(light_distance * lightSources[" + std::to_string(i) + "].distanceAttenuationScale + lightSources[" + - std::to_string(i) + "].distanceAttenuationBias, 0.0, 1.0);\n"; + shader += "distance_att_delta = clamp(light_distance * lightSources[" + std::to_string(lightID) + + "].distanceAttenuationScale + lightSources[" + std::to_string(lightID) + "].distanceAttenuationBias, 0.0, 1.0);\n"; - shader += - "distance_attenuation = lutLookup(" + std::to_string(16 + i) + ", int(clamp(floor(distance_att_delta * 255.0), 0.0, 255.0)));\n"; + shader += "distance_attenuation = lutLookup(" + std::to_string(16 + lightID) + + ", int(clamp(floor(distance_att_delta * 256.0), 0.0, 255.0)));\n"; } - // TODO: LightLutLookup stuff - shader += "spotlight_attenuation = 0.0; // Placeholder\n"; - shader += "specular0_dist = 0.0; // Placeholder\n"; - shader += "specular1_dist = 0.0; // Placeholder\n"; - shader += "reflected_color = vec3(0.0); // Placeholder\n"; + compileLUTLookup(shader, config, regs, lightID, spotlightLutIndex); + shader += "spotlight_attenuation = lut_lookup_result;\n"; - shader += "specular0 = lightSources[" + std::to_string(i) + "].specular0;\n"; - shader += "specular0 = specular0 * specular0_dist"; + compileLUTLookup(shader, config, regs, lightID, PICA::Lights::LUT_D0); + shader += "specular0_dist = lut_lookup_result;\n"; + + compileLUTLookup(shader, config, regs, lightID, PICA::Lights::LUT_D1); + shader += "specular1_dist = lut_lookup_result;\n"; + + compileLUTLookup(shader, config, regs, lightID, PICA::Lights::LUT_RR); + shader += "reflected_color.r = lut_lookup_result;\n"; + + if (isSamplerEnabled(config.lighting.config, PICA::Lights::LUT_RG)) { + compileLUTLookup(shader, config, regs, lightID, PICA::Lights::LUT_RG); + shader += "reflected_color.g = lut_lookup_result;\n"; + } else { + shader += "reflected_color.g = reflected_color.r;\n"; + } + + if (isSamplerEnabled(config.lighting.config, PICA::Lights::LUT_RB)) { + compileLUTLookup(shader, config, regs, lightID, PICA::Lights::LUT_RB); + shader += "reflected_color.b = lut_lookup_result;\n"; + } else { + shader += "reflected_color.b = reflected_color.r;\n"; + } + + shader += "specular0 = lightSources[" + std::to_string(lightID) + "].specular0 * specular0_dist;\n"; if (lightConfig.geometricFactor0) { - shader += " * geometric_factor;\n"; - } else { - shader += ";\n"; + shader += "specular0 *= geometric_factor;\n"; } - shader += "specular1 = lightSources[" + std::to_string(i) + "].specular1;\n"; - shader += "specular1 = specular1 * specular1_dist * reflected_color"; + shader += "specular1 = lightSources[" + std::to_string(lightID) + "].specular1 * specular1_dist * reflected_color;\n"; if (lightConfig.geometricFactor1) { - shader += " * geometric_factor;\n"; - } else { - shader += ";\n"; + shader += "specular1 *= geometric_factor;\n"; } shader += "light_factor = distance_attenuation * spotlight_attenuation;\n"; - + if (config.lighting.clampHighlights) { shader += "specular_sum.rgb += light_factor * (NdotL == 0.0 ? 0.0 : 1.0) * (specular0 + specular1);\n"; } else { shader += "specular_sum.rgb += light_factor * (specular0 + specular1);\n"; } - shader += "diffuse_sum.rgb += vec3(0.0); // Placeholder\n"; + shader += "diffuse_sum.rgb += light_factor * lightSources[" + std::to_string(lightID) + "].ambient + lightSources[" + + std::to_string(lightID) + "].diffuse * NdotL;\n"; + } + + if (config.lighting.enablePrimaryAlpha || config.lighting.enableSecondaryAlpha) { + compileLUTLookup(shader, config, regs, lightID, PICA::Lights::LUT_FR); + shader += "float fresnel_factor = lut_lookup_result;\n"; + } + + if (config.lighting.enablePrimaryAlpha) { + shader += "diffuse_sum.a = fresnel_factor;\n"; + } + + if (config.lighting.enableSecondaryAlpha) { + shader += "specular_sum.a = fresnel_factor;\n"; } - // TODO: Rest of the post-per-light stuff shader += R"( - vec4 global_ambient = vec4(regToColor(GPUREG_LIGHTING_AMBIENT), 1.0); + vec4 global_ambient = vec4(regToColor(globalAmbientLight), 1.0); primaryColor = clamp(global_ambient + diffuse_sum, vec4(0.0), vec4(1.0)); secondaryColor = clamp(specular_sum, vec4(0.0), vec4(1.0)); @@ -568,9 +602,7 @@ bool FragmentGenerator::isSamplerEnabled(u32 environmentID, u32 lutID) { return samplerEnabled[environmentID * 7 + lutID]; } -void FragmentGenerator::compileLUTLookup( - std::string& shader, const PICA::FragmentConfig& config, const PICARegs& regs, u32 lightIndex, u32 lutID, bool abs -) { +void FragmentGenerator::compileLUTLookup(std::string& shader, const PICA::FragmentConfig& config, const PICARegs& regs, u32 lightIndex, u32 lutID) { uint lutIndex = 0; int bitInConfig1 = 0; @@ -588,9 +620,45 @@ void FragmentGenerator::compileLUTLookup( const bool samplerEnabled = isSamplerEnabled(config.lighting.config, lutID); const u32 config1 = regs[InternalRegs::LightConfig1]; - if (!samplerEnabled || ((config1 >> bitInConfig1) != 0)) { - // 1.0 + if (!samplerEnabled || ((config1 >> bitInConfig1) & 1)) { + shader += "lut_lookup_result = 1.0;\n"; + return; } - // TODO + static constexpr float scales[] = {1.0f, 2.0f, 4.0f, 8.0f, 0.0f, 0.0f, 0.25f, 0.5f}; + const u32 lutAbs = regs[InternalRegs::LightLUTAbs]; + const u32 lutSelect = regs[InternalRegs::LightLUTSelect]; + const u32 lutScale = regs[InternalRegs::LightLUTScale]; + + // The way these bitfields are encoded is so cursed + float scale = scales[(lutScale >> (4 * lutIndex)) & 0x7]; + uint inputID = (lutSelect >> (4 * lutIndex)) & 0x7; + bool absEnabled = ((lutAbs >> (4 * lutIndex + 1)) & 0x1) == 0; // 0 = enabled... + + switch (inputID) { + case 0: shader += "lut_lookup_delta = dot(normal, normalize(half_vector));\n"; break; + case 1: shader += "lut_lookup_delta = dot(normalize(v_view), normalize(half_vector));\n"; break; + case 2: shader += "lut_lookup_delta = dot(normal, normalize(v_view));\n"; break; + case 3: shader += "lut_lookup_delta = dot(normal, light_vector);\n"; break; + + case 4: // Spotlight + default: + Helpers::warn("Shadergen: Unimplemented LUT select"); + shader += "lut_lookup_delta = 1.0;\n"; + break; + } + + if (absEnabled) { + bool twoSidedDiffuse = config.lighting.lights[lightIndex].twoSidedDiffuse; + shader += twoSidedDiffuse ? "lut_lookup_delta = abs(lut_lookup_delta);\n" : "lut_lookup_delta = max(lut_lookup_delta, 0.0);\n"; + shader += "lut_lookup_result = lutLookup(" + std::to_string(lutIndex) + ", int(clamp(floor(lut_lookup_delta * 256.0), 0.0, 255.0)));\n"; + if (scale != 1.0) { + shader += "lut_lookup_result *= " + std::to_string(scale) + ";\n"; + } + } else { + // Range is [-1, 1] so we need to map it to [0, 1] + shader += "lut_lookup_index = int(clamp(floor(lut_lookup_delta * 128.0), -128.f, 127.f));\n"; + shader += "if (lut_lookup_index < 0) lut_lookup_index += 256;\n"; + shader += "lut_lookup_result = lutLookup(" + std::to_string(lutIndex) + ", lut_lookup_index) *" + std::to_string(scale) + ";\n"; + } } \ No newline at end of file diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 38575e40..bcf33b57 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -903,7 +903,7 @@ OpenGL::Program& RendererGL::getSpecializedShader() { light.ambient = lightColorToVec3(ambient); light.position[0] = Floats::f16::fromRaw(u16(lightXY)).toFloat32(); light.position[1] = Floats::f16::fromRaw(u16(lightXY >> 16)).toFloat32(); - light.position[2] = Floats::f16::fromRaw(u16(lightXY)).toFloat32(); + light.position[2] = Floats::f16::fromRaw(u16(lightZ)).toFloat32(); // Fixed point 1.11.1 to float, without negation light.spotlightDirection[0] = float(s32(spotlightXY & 0x1FFF) << 19 >> 19) / 2047.0; From bd38f9a8abcd1db3124a752ce49c1ee6fb299fde Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:34:23 +0300 Subject: [PATCH 097/251] Shadergen: Add spotlight --- src/core/PICA/shader_gen_glsl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 8d955d50..b4525634 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -640,8 +640,8 @@ void FragmentGenerator::compileLUTLookup(std::string& shader, const PICA::Fragme case 1: shader += "lut_lookup_delta = dot(normalize(v_view), normalize(half_vector));\n"; break; case 2: shader += "lut_lookup_delta = dot(normal, normalize(v_view));\n"; break; case 3: shader += "lut_lookup_delta = dot(normal, light_vector);\n"; break; + case 4: shader += "lut_lookup_delta = dot(normal, lightSources[" + std ::to_string(lightIndex) + "].spotlightDirection);\n"; break; - case 4: // Spotlight default: Helpers::warn("Shadergen: Unimplemented LUT select"); shader += "lut_lookup_delta = 1.0;\n"; From 53c76ae0d425579754fdbe5439708bb531acff74 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 18 Jul 2024 23:57:43 +0300 Subject: [PATCH 098/251] Shadergen: Fix spotlight --- src/core/PICA/shader_gen_glsl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index b4525634..136647d7 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -640,7 +640,7 @@ void FragmentGenerator::compileLUTLookup(std::string& shader, const PICA::Fragme case 1: shader += "lut_lookup_delta = dot(normalize(v_view), normalize(half_vector));\n"; break; case 2: shader += "lut_lookup_delta = dot(normal, normalize(v_view));\n"; break; case 3: shader += "lut_lookup_delta = dot(normal, light_vector);\n"; break; - case 4: shader += "lut_lookup_delta = dot(normal, lightSources[" + std ::to_string(lightIndex) + "].spotlightDirection);\n"; break; + case 4: shader += "lut_lookup_delta = dot(light_vector, lightSources[" + std ::to_string(lightIndex) + "].spotlightDirection);\n"; break; default: Helpers::warn("Shadergen: Unimplemented LUT select"); From e36b6c77a7110df3652277e36bafa6fa96774413 Mon Sep 17 00:00:00 2001 From: offtkp Date: Fri, 19 Jul 2024 00:07:29 +0300 Subject: [PATCH 099/251] Fix lugi and toad treasure tracker in ubershader Co-authored-by: wheremyfoodat <4909372+wheremyfoodat@users.noreply.github.com> --- src/host_shaders/opengl_fragment_shader.frag | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index 6f30ebf0..6a2baa96 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -118,7 +118,7 @@ vec4 tevCalculateCombiner(int tev_id) { case 6u: result.rgb = vec3(4.0 * dot(source0.rgb - 0.5, source1.rgb - 0.5)); break; // Dot3 RGB case 7u: result = vec4(4.0 * dot(source0.rgb - 0.5, source1.rgb - 0.5)); break; // Dot3 RGBA case 8u: result.rgb = min(source0.rgb * source1.rgb + source2.rgb, 1.0); break; // Multiply then add - case 9u: result.rgb = min((source0.rgb + source1.rgb) * source2.rgb, 1.0); break; // Add then multiply + case 9u: result.rgb = min(source0.rgb + source1.rgb, 1.0) * source2.rgb; break; // Add then multiply default: break; } @@ -133,7 +133,7 @@ vec4 tevCalculateCombiner(int tev_id) { case 4u: result.a = mix(source1.a, source0.a, source2.a); break; // Interpolate case 5u: result.a = max(0.0, source0.a - source1.a); break; // Subtract case 8u: result.a = min(1.0, source0.a * source1.a + source2.a); break; // Multiply then add - case 9u: result.a = min(1.0, (source0.a + source1.a) * source2.a); break; // Add then multiply + case 9u: result.a = min(source0.a + source1.a, 1.0) * source2.a; break; // Add then multiply default: break; } } @@ -277,7 +277,7 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light } else { delta = abs(delta); } - int index = int(clamp(floor(delta * 256.0), 0.f, 255.f)); + int index = int(clamp(floor(delta * 255.0), 0.f, 255.f)); return lutLookup(lut_index, index) * scale; } else { // Range is [-1, 1] so we need to map it to [0, 1] From 90abf8a3769d9e1bebc85cd12d39c20851a9af1c Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 19 Jul 2024 01:29:48 +0300 Subject: [PATCH 100/251] Fix signedness mess-ups in shaders --- src/core/PICA/shader_gen_glsl.cpp | 6 +++--- src/host_shaders/opengl_fragment_shader.frag | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 136647d7..61a2c57c 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -147,11 +147,11 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf } float lutLookup(uint lut, int index) { - return texelFetch(u_tex_lighting_lut, ivec2(index, lut), 0).r; + return texelFetch(u_tex_lighting_lut, ivec2(index, int(lut)), 0).r; } vec3 regToColor(uint reg) { - return (1.0 / 255.0) * vec3(float((reg >> 20) & 0xFF), float((reg >> 10) & 0xFF), float(reg & 0xFF)); + return (1.0 / 255.0) * vec3(float((reg >> 20u) & 0xFFu), float((reg >> 10u) & 0xFFu), float(reg & 0xFFu)); } )"; } @@ -476,7 +476,7 @@ void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentC int lut_lookup_index; )"; - uint lightID = 0;; + uint lightID = 0; for (int i = 0; i < config.lighting.lightNum; i++) { lightID = config.lighting.lights[i].num; diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index 6a2baa96..9f369e39 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -161,7 +161,7 @@ bool error_unimpl = false; vec4 unimpl_color = vec4(1.0, 0.0, 1.0, 1.0); float lutLookup(uint lut, int index) { - return texelFetch(u_tex_lighting_lut, ivec2(index, lut), 0).r; + return texelFetch(u_tex_lighting_lut, ivec2(index, int(lut)), 0).r; } vec3 regToColor(uint reg) { From 25098082c78e899cc2e387547358fea8f7ca1f28 Mon Sep 17 00:00:00 2001 From: offtkp Date: Fri, 19 Jul 2024 02:45:09 +0300 Subject: [PATCH 101/251] Use lutID instead of lutIndex --- src/core/PICA/shader_gen_glsl.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 61a2c57c..21b55338 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -631,9 +631,9 @@ void FragmentGenerator::compileLUTLookup(std::string& shader, const PICA::Fragme const u32 lutScale = regs[InternalRegs::LightLUTScale]; // The way these bitfields are encoded is so cursed - float scale = scales[(lutScale >> (4 * lutIndex)) & 0x7]; - uint inputID = (lutSelect >> (4 * lutIndex)) & 0x7; - bool absEnabled = ((lutAbs >> (4 * lutIndex + 1)) & 0x1) == 0; // 0 = enabled... + float scale = scales[(lutScale >> (4 * lutID)) & 0x7]; + uint inputID = (lutSelect >> (4 * lutID)) & 0x7; + bool absEnabled = ((lutAbs >> (4 * lutID + 1)) & 0x1) == 0; // 0 = enabled... switch (inputID) { case 0: shader += "lut_lookup_delta = dot(normal, normalize(half_vector));\n"; break; From ac55c3e3249603e4a77dc2bd7dc97f372dad6f78 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 19 Jul 2024 03:01:12 +0300 Subject: [PATCH 102/251] Shadergen: Fix small register decoding oopsie --- include/PICA/pica_frag_config.hpp | 2 +- include/PICA/regs.hpp | 1 + src/core/PICA/shader_gen_glsl.cpp | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index cdb68854..5338f719 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -116,7 +116,7 @@ namespace PICA { for (int i = 0; i < totalLightCount; i++) { auto& light = lights[i]; - const u32 lightConfig = 0x149 + 0x10 * i; + const u32 lightConfig = regs[InternalRegs::Light0Config + 0x10 * i]; light.num = (regs[InternalRegs::LightPermutation] >> (i * 4)) & 0x7; light.directional = Helpers::getBit<0>(lightConfig); diff --git a/include/PICA/regs.hpp b/include/PICA/regs.hpp index 2482c25b..4518e16a 100644 --- a/include/PICA/regs.hpp +++ b/include/PICA/regs.hpp @@ -77,6 +77,7 @@ namespace PICA { Light0Z = 0x145, Light0SpotlightXY = 0x146, Light0SpotlightZ = 0x147, + Light0Config = 0x149, Light0AttenuationBias = 0x14A, Light0AttenuationScale = 0x14B, diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 21b55338..a892e514 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -485,9 +485,9 @@ void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentC shader += "light_position = lightSources[" + std::to_string(lightID) + "].position;\n"; if (lightConfig.directional) { // Directional lighting - shader += "light_vector = light_position + v_view;\n"; - } else { // Positional lighting shader += "light_vector = light_position;\n"; + } else { // Positional lighting + shader += "light_vector = light_position + v_view;\n"; } shader += R"( From 5c1e2912a3c269e01eebefd23274001d1a08341a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 19 Jul 2024 14:35:01 +0300 Subject: [PATCH 103/251] Shadergen: Minimize shader compilation time by caching the default VS --- include/PICA/shader_gen.hpp | 2 +- include/renderer_gl/renderer_gl.hpp | 3 ++ src/core/PICA/shader_gen_glsl.cpp | 2 +- src/core/renderer_gl/renderer_gl.cpp | 8 +-- third_party/opengl/opengl.hpp | 79 +++++++++++++++++----------- 5 files changed, 57 insertions(+), 37 deletions(-) diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index 372e0550..8cdaadfc 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -35,7 +35,7 @@ namespace PICA::ShaderGen { public: FragmentGenerator(API api, Language language) : api(api), language(language) {} std::string generate(const PICARegs& regs, const PICA::FragmentConfig& config); - std::string getVertexShader(const PICARegs& regs); + std::string getDefaultVertexShader(); void setTarget(API api, Language language) { this->api = api; diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index 46d344b2..abde96bf 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -66,6 +66,9 @@ class RendererGL final : public Renderer { OpenGL::Texture lightLUTTexture; OpenGL::Framebuffer screenFramebuffer; OpenGL::Texture blankTexture; + // The "default" vertex shader to use when using specialized shaders but not PICA vertex shader -> GLSL recompilation + // We can compile this once and then link it with all other generated fragment shaders + OpenGL::Shader defaultShadergenVs; // Cached recompiled fragment shader struct CachedProgram { diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index a892e514..c7195b25 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -34,7 +34,7 @@ static constexpr const char* uniformDefinition = R"( // This is particularly intuitive in several places, such as checking if a LUT is enabled static constexpr int spotlightLutIndex = 2; -std::string FragmentGenerator::getVertexShader(const PICARegs& regs) { +std::string FragmentGenerator::getDefaultVertexShader() { std::string ret = ""; switch (api) { diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index bcf33b57..7fca385d 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -162,6 +162,10 @@ void RendererGL::initGraphicsContextInternal() { OpenGL::setViewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]); reset(); + + // Initialize the default vertex shader used with shadergen + std::string defaultShadergenVSSource = fragShaderGen.getDefaultVertexShader(); + defaultShadergenVs.create({defaultShadergenVSSource.c_str(), defaultShadergenVSSource.size()}, OpenGL::Vertex); } // The OpenGL renderer doesn't need to do anything with the GL context (For Qt frontend) or the SDL window (For SDL frontend) @@ -810,12 +814,10 @@ OpenGL::Program& RendererGL::getSpecializedShader() { OpenGL::Program& program = programEntry.program; if (!program.exists()) { - std::string vs = fragShaderGen.getVertexShader(regs); std::string fs = fragShaderGen.generate(regs, fsConfig); - OpenGL::Shader vertShader({vs.c_str(), vs.size()}, OpenGL::Vertex); OpenGL::Shader fragShader({fs.c_str(), fs.size()}, OpenGL::Fragment); - program.create({vertShader, fragShader}); + program.create({defaultShadergenVs, fragShader}); gl.useProgram(program); // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 diff --git a/third_party/opengl/opengl.hpp b/third_party/opengl/opengl.hpp index 828fb784..4a08650a 100644 --- a/third_party/opengl/opengl.hpp +++ b/third_party/opengl/opengl.hpp @@ -355,46 +355,57 @@ namespace OpenGL { } }; - enum ShaderType { - Fragment = GL_FRAGMENT_SHADER, - Vertex = GL_VERTEX_SHADER, - Geometry = GL_GEOMETRY_SHADER, - Compute = GL_COMPUTE_SHADER, - TessControl = GL_TESS_CONTROL_SHADER, - TessEvaluation = GL_TESS_EVALUATION_SHADER - }; + enum ShaderType { + Fragment = GL_FRAGMENT_SHADER, + Vertex = GL_VERTEX_SHADER, + Geometry = GL_GEOMETRY_SHADER, + Compute = GL_COMPUTE_SHADER, + TessControl = GL_TESS_CONTROL_SHADER, + TessEvaluation = GL_TESS_EVALUATION_SHADER + }; - struct Shader { - GLuint m_handle = 0; + struct Shader { + GLuint m_handle = 0; - Shader() {} - Shader(const std::string_view source, ShaderType type) { create(source, static_cast(type)); } + Shader() {} + Shader(const std::string_view source, ShaderType type) { create(source, static_cast(type)); } - // Returns whether compilation failed or not - bool create(const std::string_view source, GLenum type) { - m_handle = glCreateShader(type); - const GLchar* const sources[1] = { source.data() }; + // Returns whether compilation failed or not + bool create(const std::string_view source, GLenum type) { + m_handle = glCreateShader(type); + const GLchar* const sources[1] = {source.data()}; - glShaderSource(m_handle, 1, sources, nullptr); - glCompileShader(m_handle); + glShaderSource(m_handle, 1, sources, nullptr); + glCompileShader(m_handle); - GLint success; - glGetShaderiv(m_handle, GL_COMPILE_STATUS, &success); - if (success == GL_FALSE) { - char buf[4096]; - glGetShaderInfoLog(m_handle, 4096, nullptr, buf); - fprintf(stderr, "Failed to compile shader\nError: %s\n", buf); - glDeleteShader(m_handle); + GLint success; + glGetShaderiv(m_handle, GL_COMPILE_STATUS, &success); + if (success == GL_FALSE) { + char buf[4096]; + glGetShaderInfoLog(m_handle, 4096, nullptr, buf); + fprintf(stderr, "Failed to compile shader\nError: %s\n", buf); + glDeleteShader(m_handle); - m_handle = 0; - } + m_handle = 0; + } - return m_handle != 0; - } + return m_handle != 0; + } - GLuint handle() const { return m_handle; } - bool exists() const { return m_handle != 0; } - }; + GLuint handle() const { return m_handle; } + bool exists() const { return m_handle != 0; } + + void free() { + if (exists()) { + glDeleteShader(m_handle); + m_handle = 0; + } + } + +#ifdef OPENGL_DESTRUCTORS + ~Shader() { free(); } +#endif + }; struct Program { GLuint m_handle = 0; @@ -431,6 +442,10 @@ namespace OpenGL { m_handle = 0; } } + +#ifdef OPENGL_DESTRUCTORS + ~Program() { free(); } +#endif }; static void dispatchCompute(GLuint groupsX = 1, GLuint groupsY = 1, GLuint groupsZ = 1) { From e4550b3e4f14c487678a9d3a05d1e05bd7f23826 Mon Sep 17 00:00:00 2001 From: offtkp Date: Fri, 19 Jul 2024 15:55:02 +0300 Subject: [PATCH 104/251] Fix pokedex3d on specialized shaders --- src/core/PICA/shader_gen_glsl.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index c7195b25..0a9c1a5a 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -560,8 +560,8 @@ void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentC shader += "specular_sum.rgb += light_factor * (specular0 + specular1);\n"; } - shader += "diffuse_sum.rgb += light_factor * lightSources[" + std::to_string(lightID) + "].ambient + lightSources[" + - std::to_string(lightID) + "].diffuse * NdotL;\n"; + shader += "diffuse_sum.rgb += light_factor * (lightSources[" + std::to_string(lightID) + "].ambient + lightSources[" + + std::to_string(lightID) + "].diffuse * NdotL);\n"; } if (config.lighting.enablePrimaryAlpha || config.lighting.enableSecondaryAlpha) { From 9415cee59a05f4531a2b89867b02266baff3d344 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:02:42 +0300 Subject: [PATCH 105/251] Enable shadergen by default for now --- include/config.hpp | 3 ++- src/core/PICA/shader_gen_glsl.cpp | 2 +- src/libretro_core.cpp | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/include/config.hpp b/include/config.hpp index 8aa695aa..ed2c270f 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -13,7 +13,8 @@ struct EmulatorConfig { static constexpr bool shaderJitDefault = false; #endif - static constexpr bool ubershaderDefault = true; + // For now, use specialized shaders by default + static constexpr bool ubershaderDefault = false; bool shaderJitEnabled = shaderJitDefault; bool discordRpcEnabled = false; diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 0a9c1a5a..95b042f1 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -165,7 +165,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf vec4 tevNextPreviousBuffer = tevBufferColor; vec4 primaryColor = vec4(0.0); - vec4 secondaryColor = vec4(0.0); + vec4 secondaryColor = vec4(0.0); )"; compileLights(ret, config, regs); diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index a6a1ff00..b48e937a 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -147,7 +147,7 @@ static void configInit() { static const retro_variable values[] = { {"panda3ds_use_shader_jit", "Enable shader JIT; enabled|disabled"}, {"panda3ds_accurate_shader_mul", "Enable accurate shader multiplication; disabled|enabled"}, - {"panda3ds_use_ubershader", "Use ubershaders (No stutter, maybe slower); enabled|disabled"}, + {"panda3ds_use_ubershader", "Use ubershaders (No stutter, maybe slower); disabled|enabled"}, {"panda3ds_use_vsync", "Enable VSync; enabled|disabled"}, {"panda3ds_dsp_emulation", "DSP emulation; Null|HLE|LLE"}, {"panda3ds_use_audio", "Enable audio; disabled|enabled"}, From 20335b7d2d875b12402a59d7f50961098290f7ec Mon Sep 17 00:00:00 2001 From: offtkp Date: Fri, 19 Jul 2024 18:05:43 +0300 Subject: [PATCH 106/251] Update gles.patch --- .github/gles.patch | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/gles.patch b/.github/gles.patch index a27b3d00..270e336e 100644 --- a/.github/gles.patch +++ b/.github/gles.patch @@ -21,7 +21,7 @@ index 990e2f80..2e7842ac 100644 void main() { diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag -index b4ad7ecc..98b1bd80 100644 +index 9f369e39..b4bb19d3 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -1,4 +1,5 @@ @@ -32,7 +32,7 @@ index b4ad7ecc..98b1bd80 100644 in vec4 v_quaternion; in vec4 v_colour; @@ -164,11 +165,17 @@ float lutLookup(uint lut, int index) { - return texelFetch(u_tex_lighting_lut, ivec2(index, lut), 0).r; + return texelFetch(u_tex_lighting_lut, ivec2(index, int(lut)), 0).r; } +// some gles versions have bitfieldExtractCompat and complain if you redefine it, some don't and compile error, using this instead @@ -225,10 +225,10 @@ index 057f9a88..dc735ced 100644 v_quaternion = a_quaternion; } diff --git a/third_party/opengl/opengl.hpp b/third_party/opengl/opengl.hpp -index 828fb784..a1861b77 100644 +index 4a08650a..21af37e3 100644 --- a/third_party/opengl/opengl.hpp +++ b/third_party/opengl/opengl.hpp -@@ -568,22 +568,22 @@ namespace OpenGL { +@@ -583,22 +583,22 @@ namespace OpenGL { static void disableScissor() { glDisable(GL_SCISSOR_TEST); } static void enableBlend() { glEnable(GL_BLEND); } static void disableBlend() { glDisable(GL_BLEND); } From eb7e02fbc2ce9a285b3d523ef2c76c1e893e525a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:20:21 +0300 Subject: [PATCH 107/251] Shadergen: Remove redundant whitespace generation --- src/core/PICA/shader_gen_glsl.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 95b042f1..e19c459e 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -162,7 +162,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf void main() { vec4 combinerOutput = v_colour; vec4 previousBuffer = vec4(0.0); - vec4 tevNextPreviousBuffer = tevBufferColor; + vec4 tevNextPreviousBuffer = tevBufferColor; vec4 primaryColor = vec4(0.0); vec4 secondaryColor = vec4(0.0); @@ -494,7 +494,7 @@ void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentC light_distance = length(light_vector); light_vector = normalize(light_vector); half_vector = light_vector + normalize(v_view); - + distance_attenuation = 1.0; NdotL = dot(normal, light_vector); )"; From 270f4b00a91b55087012ac598cda0b985261f7d8 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 20 Jul 2024 01:01:15 +0300 Subject: [PATCH 108/251] AES: Fix fixed crypto key mode and CTR for versions 0 and 2 --- src/core/loader/ncch.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 47d5a4c2..98574289 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -88,8 +88,8 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn encryptionInfoTmp.normalKey = *primaryKey; encryptionInfoTmp.initialCounter.fill(0); - for (std::size_t i = 1; i <= sizeof(std::uint64_t) - 1; i++) { - encryptionInfoTmp.initialCounter[i] = header[0x108 + sizeof(std::uint64_t) - 1 - i]; + for (usize i = 0; i < 8; i++) { + encryptionInfoTmp.initialCounter[i] = header[0x108 + 7 - i]; } encryptionInfoTmp.initialCounter[8] = 1; exheaderInfo.encryptionInfo = encryptionInfoTmp; @@ -305,6 +305,7 @@ std::pair NCCH::getPrimaryKey(Crypto::AESEngine &aesEngine if (encrypted) { if (fixedCryptoKey) { + result.fill(0); return {true, result}; } @@ -326,6 +327,7 @@ std::pair NCCH::getSecondaryKey(Crypto::AESEngine &aesEngi if (encrypted) { if (fixedCryptoKey) { + result.fill(0); return {true, result}; } From af552edd9d8456338446a5cc959aeedd2a028c4f Mon Sep 17 00:00:00 2001 From: Paris Oplopoios Date: Sat, 20 Jul 2024 02:37:49 +0300 Subject: [PATCH 109/251] Remove dependency of PICA regs in fragment config (#541) Remove dependency of PICA regs in fragment config Nyom Nyom part 2 Nyom 3: The final nyom Nyom 4: The nyomening Nyom 5: The final Nyom for real --- include/PICA/pica_frag_config.hpp | 54 +++++++++--- include/PICA/regs.hpp | 5 ++ include/PICA/shader_gen.hpp | 19 ++-- src/core/PICA/shader_gen_glsl.cpp | 124 ++++++++++++--------------- src/core/renderer_gl/renderer_gl.cpp | 27 +----- 5 files changed, 109 insertions(+), 120 deletions(-) diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index 5338f719..89dd3420 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -73,13 +73,7 @@ namespace PICA { BitField<22, 2, u32> shadowSelector; }; - LightingLUTConfig d0{}; - LightingLUTConfig d1{}; - LightingLUTConfig sp{}; - LightingLUTConfig fr{}; - LightingLUTConfig rr{}; - LightingLUTConfig rg{}; - LightingLUTConfig rb{}; + std::array luts{}; std::array lights{}; @@ -116,19 +110,27 @@ namespace PICA { for (int i = 0; i < totalLightCount; i++) { auto& light = lights[i]; - const u32 lightConfig = regs[InternalRegs::Light0Config + 0x10 * i]; - light.num = (regs[InternalRegs::LightPermutation] >> (i * 4)) & 0x7; + + const u32 lightConfig = regs[InternalRegs::Light0Config + 0x10 * light.num]; light.directional = Helpers::getBit<0>(lightConfig); light.twoSidedDiffuse = Helpers::getBit<1>(lightConfig); light.geometricFactor0 = Helpers::getBit<2>(lightConfig); light.geometricFactor1 = Helpers::getBit<3>(lightConfig); - light.shadowEnable = ((config1 >> i) & 1) ^ 1; // This also does 0 = enabled - light.spotAttenuationEnable = ((config1 >> (8 + i)) & 1) ^ 1; // Same here - light.distanceAttenuationEnable = ((config1 >> (24 + i)) & 1) ^ 1; // Of course same here + light.shadowEnable = ((config1 >> light.num) & 1) ^ 1; // This also does 0 = enabled + light.spotAttenuationEnable = ((config1 >> (8 + light.num)) & 1) ^ 1; // Same here + light.distanceAttenuationEnable = ((config1 >> (24 + light.num)) & 1) ^ 1; // Of course same here } + LightingLUTConfig& d0 = luts[Lights::LUT_D0]; + LightingLUTConfig& d1 = luts[Lights::LUT_D1]; + LightingLUTConfig& sp = luts[spotlightLutIndex]; + LightingLUTConfig& fr = luts[Lights::LUT_FR]; + LightingLUTConfig& rb = luts[Lights::LUT_RB]; + LightingLUTConfig& rg = luts[Lights::LUT_RG]; + LightingLUTConfig& rr = luts[Lights::LUT_RR]; + d0.enable = Helpers::getBit<16>(config1) == 0; d1.enable = Helpers::getBit<17>(config1) == 0; fr.enable = Helpers::getBit<19>(config1) == 0; @@ -144,7 +146,7 @@ namespace PICA { if (d0.enable) { d0.absInput = Helpers::getBit<1>(lutAbs) == 0; - d0.type = Helpers::getBits<0, 3>(lutSelect); + d0.type = Helpers::getBits<0, 3>(lutSelect); d0.scale = scales[Helpers::getBits<0, 3>(lutScale)]; } @@ -195,7 +197,31 @@ namespace PICA { return std::memcmp(this, &config, sizeof(FragmentConfig)) == 0; } - FragmentConfig(const std::array& regs) : lighting(regs) {} + FragmentConfig(const std::array& regs) : lighting(regs) { + auto alphaTestConfig = regs[InternalRegs::AlphaTestConfig]; + auto alphaTestFunction = Helpers::getBits<4, 3>(alphaTestConfig); + + outConfig.alphaTestFunction = + (alphaTestConfig & 1) ? static_cast(alphaTestFunction) : PICA::CompareFunction::Always; + outConfig.depthMapEnable = regs[InternalRegs::DepthmapEnable] & 1; + + texConfig.texUnitConfig = regs[InternalRegs::TexUnitCfg]; + texConfig.texEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; + + // Set up TEV stages. Annoyingly we can't just memcpy as the TEV registers are arranged like + // {Source, Operand, Combiner, Color, Scale} and we want to skip the color register since it's uploaded via UBO +#define setupTevStage(stage) \ + std::memcpy(&texConfig.tevConfigs[stage * 4], ®s[InternalRegs::TexEnv##stage##Source], 3 * sizeof(u32)); \ + texConfig.tevConfigs[stage * 4 + 3] = regs[InternalRegs::TexEnv##stage##Source + 4]; + + setupTevStage(0); + setupTevStage(1); + setupTevStage(2); + setupTevStage(3); + setupTevStage(4); + setupTevStage(5); +#undef setupTevStage + } }; static_assert( diff --git a/include/PICA/regs.hpp b/include/PICA/regs.hpp index 4518e16a..c4d6a5fb 100644 --- a/include/PICA/regs.hpp +++ b/include/PICA/regs.hpp @@ -278,6 +278,11 @@ namespace PICA { }; } + // There's actually 8 different LUTs (SP0-SP7), one for each light with different indices (8-15) + // We use an unused LUT value for "this light source's spotlight" instead and figure out which light source to use in compileLutLookup + // This is particularly intuitive in several places, such as checking if a LUT is enabled + static constexpr int spotlightLutIndex = 2; + enum class TextureFmt : u32 { RGBA8 = 0x0, RGB8 = 0x1, diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index 8cdaadfc..6cf810a0 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -14,27 +14,24 @@ namespace PICA::ShaderGen { enum class Language { GLSL }; class FragmentGenerator { - using PICARegs = std::array; API api; Language language; - void compileTEV(std::string& shader, int stage, const PICARegs& regs); - void getSource(std::string& shader, PICA::TexEnvConfig::Source source, int index); - void getColorOperand(std::string& shader, PICA::TexEnvConfig::Source source, PICA::TexEnvConfig::ColorOperand color, int index); - void getAlphaOperand(std::string& shader, PICA::TexEnvConfig::Source source, PICA::TexEnvConfig::AlphaOperand alpha, int index); + void compileTEV(std::string& shader, int stage, const PICA::FragmentConfig& config); + void getSource(std::string& shader, PICA::TexEnvConfig::Source source, int index, const PICA::FragmentConfig& config); + void getColorOperand(std::string& shader, PICA::TexEnvConfig::Source source, PICA::TexEnvConfig::ColorOperand color, int index, const PICA::FragmentConfig& config); + void getAlphaOperand(std::string& shader, PICA::TexEnvConfig::Source source, PICA::TexEnvConfig::AlphaOperand alpha, int index, const PICA::FragmentConfig& config); void getColorOperation(std::string& shader, PICA::TexEnvConfig::Operation op); void getAlphaOperation(std::string& shader, PICA::TexEnvConfig::Operation op); - void applyAlphaTest(std::string& shader, const PICARegs& regs); - void compileLights(std::string& shader, const PICA::FragmentConfig& config, const PICARegs& regs); - void compileLUTLookup(std::string& shader, const PICA::FragmentConfig& config, const PICARegs& regs, u32 lightIndex, u32 lutID); + void applyAlphaTest(std::string& shader, const PICA::FragmentConfig& config); + void compileLights(std::string& shader, const PICA::FragmentConfig& config); + void compileLUTLookup(std::string& shader, const PICA::FragmentConfig& config, u32 lightIndex, u32 lutID); bool isSamplerEnabled(u32 environmentID, u32 lutID); - u32 textureConfig = 0; - public: FragmentGenerator(API api, Language language) : api(api), language(language) {} - std::string generate(const PICARegs& regs, const PICA::FragmentConfig& config); + std::string generate(const PICA::FragmentConfig& config); std::string getDefaultVertexShader(); void setTarget(API api, Language language) { diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index e19c459e..47df58b8 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -1,3 +1,5 @@ +#include "PICA/pica_frag_config.hpp" +#include "PICA/regs.hpp" #include "PICA/shader_gen.hpp" using namespace PICA; using namespace PICA::ShaderGen; @@ -29,11 +31,6 @@ static constexpr const char* uniformDefinition = R"( }; )"; -// There's actually 8 different LUTs (SP0-SP7), one for each light with different indices (8-15) -// We use an unused LUT value for "this light source's spotlight" instead and figure out which light source to use in compileLutLookup -// This is particularly intuitive in several places, such as checking if a LUT is enabled -static constexpr int spotlightLutIndex = 2; - std::string FragmentGenerator::getDefaultVertexShader() { std::string ret = ""; @@ -101,7 +98,7 @@ std::string FragmentGenerator::getDefaultVertexShader() { return ret; } -std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConfig& config) { +std::string FragmentGenerator::generate(const FragmentConfig& config) { std::string ret = ""; switch (api) { @@ -168,7 +165,7 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf vec4 secondaryColor = vec4(0.0); )"; - compileLights(ret, config, regs); + compileLights(ret, config); ret += R"( vec3 colorOp1 = vec3(0.0); @@ -187,44 +184,39 @@ std::string FragmentGenerator::generate(const PICARegs& regs, const FragmentConf float depth = z_over_w * depthScale + depthOffset; )"; - if ((regs[InternalRegs::DepthmapEnable] & 1) == 0) { + if (!config.outConfig.depthMapEnable) { ret += "depth /= gl_FragCoord.w;\n"; } ret += "gl_FragDepth = depth;\n"; - textureConfig = regs[InternalRegs::TexUnitCfg]; for (int i = 0; i < 6; i++) { - compileTEV(ret, i, regs); + compileTEV(ret, i, config); } - applyAlphaTest(ret, regs); + applyAlphaTest(ret, config); ret += "fragColor = combinerOutput;\n}"; // End of main function return ret; } -void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICARegs& regs) { - // Base address for each TEV stage's configuration - static constexpr std::array ioBases = { - InternalRegs::TexEnv0Source, InternalRegs::TexEnv1Source, InternalRegs::TexEnv2Source, - InternalRegs::TexEnv3Source, InternalRegs::TexEnv4Source, InternalRegs::TexEnv5Source, - }; +void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICA::FragmentConfig& config) { + const u32* tevValues = config.texConfig.tevConfigs.data() + stage * 4; - const u32 ioBase = ioBases[stage]; - TexEnvConfig tev(regs[ioBase], regs[ioBase + 1], regs[ioBase + 2], regs[ioBase + 3], regs[ioBase + 4]); + // Pass a 0 to constColor here, as it doesn't matter for compilation + TexEnvConfig tev(tevValues[0], tevValues[1], tevValues[2], 0, tevValues[3]); if (!tev.isPassthroughStage()) { // Get color operands shader += "colorOp1 = "; - getColorOperand(shader, tev.colorSource1, tev.colorOperand1, stage); + getColorOperand(shader, tev.colorSource1, tev.colorOperand1, stage, config); shader += ";\ncolorOp2 = "; - getColorOperand(shader, tev.colorSource2, tev.colorOperand2, stage); + getColorOperand(shader, tev.colorSource2, tev.colorOperand2, stage, config); shader += ";\ncolorOp3 = "; - getColorOperand(shader, tev.colorSource3, tev.colorOperand3, stage); + getColorOperand(shader, tev.colorSource3, tev.colorOperand3, stage, config); shader += ";\nvec3 outputColor" + std::to_string(stage) + " = clamp("; getColorOperation(shader, tev.colorOp); @@ -236,13 +228,13 @@ void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICAReg } else { // Get alpha operands shader += "alphaOp1 = "; - getAlphaOperand(shader, tev.alphaSource1, tev.alphaOperand1, stage); + getAlphaOperand(shader, tev.alphaSource1, tev.alphaOperand1, stage, config); shader += ";\nalphaOp2 = "; - getAlphaOperand(shader, tev.alphaSource2, tev.alphaOperand2, stage); + getAlphaOperand(shader, tev.alphaSource2, tev.alphaOperand2, stage, config); shader += ";\nalphaOp3 = "; - getAlphaOperand(shader, tev.alphaSource3, tev.alphaOperand3, stage); + getAlphaOperand(shader, tev.alphaSource3, tev.alphaOperand3, stage, config); shader += ";\nfloat outputAlpha" + std::to_string(stage) + " = clamp("; getAlphaOperation(shader, tev.alphaOp); @@ -258,7 +250,7 @@ void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICAReg shader += "previousBuffer = tevNextPreviousBuffer;\n\n"; // Update the "next previous buffer" if necessary - const u32 textureEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; + const u32 textureEnvUpdateBuffer = config.texConfig.texEnvUpdateBuffer; if (stage < 4) { // Check whether to update rgb if ((textureEnvUpdateBuffer & (0x100 << stage))) { @@ -272,7 +264,7 @@ void FragmentGenerator::compileTEV(std::string& shader, int stage, const PICAReg } } -void FragmentGenerator::getColorOperand(std::string& shader, TexEnvConfig::Source source, TexEnvConfig::ColorOperand color, int index) { +void FragmentGenerator::getColorOperand(std::string& shader, TexEnvConfig::Source source, TexEnvConfig::ColorOperand color, int index, const PICA::FragmentConfig& config) { using OperandType = TexEnvConfig::ColorOperand; // For inverting operands, add the 1.0 - x subtraction @@ -284,31 +276,31 @@ void FragmentGenerator::getColorOperand(std::string& shader, TexEnvConfig::Sourc switch (color) { case OperandType::SourceColor: case OperandType::OneMinusSourceColor: - getSource(shader, source, index); + getSource(shader, source, index, config); shader += ".rgb"; break; case OperandType::SourceRed: case OperandType::OneMinusSourceRed: - getSource(shader, source, index); + getSource(shader, source, index, config); shader += ".rrr"; break; case OperandType::SourceGreen: case OperandType::OneMinusSourceGreen: - getSource(shader, source, index); + getSource(shader, source, index, config); shader += ".ggg"; break; case OperandType::SourceBlue: case OperandType::OneMinusSourceBlue: - getSource(shader, source, index); + getSource(shader, source, index, config); shader += ".bbb"; break; case OperandType::SourceAlpha: case OperandType::OneMinusSourceAlpha: - getSource(shader, source, index); + getSource(shader, source, index, config); shader += ".aaa"; break; @@ -319,7 +311,7 @@ void FragmentGenerator::getColorOperand(std::string& shader, TexEnvConfig::Sourc } } -void FragmentGenerator::getAlphaOperand(std::string& shader, TexEnvConfig::Source source, TexEnvConfig::AlphaOperand color, int index) { +void FragmentGenerator::getAlphaOperand(std::string& shader, TexEnvConfig::Source source, TexEnvConfig::AlphaOperand color, int index, const PICA::FragmentConfig& config) { using OperandType = TexEnvConfig::AlphaOperand; // For inverting operands, add the 1.0 - x subtraction @@ -331,25 +323,25 @@ void FragmentGenerator::getAlphaOperand(std::string& shader, TexEnvConfig::Sourc switch (color) { case OperandType::SourceRed: case OperandType::OneMinusSourceRed: - getSource(shader, source, index); + getSource(shader, source, index, config); shader += ".r"; break; case OperandType::SourceGreen: case OperandType::OneMinusSourceGreen: - getSource(shader, source, index); + getSource(shader, source, index, config); shader += ".g"; break; case OperandType::SourceBlue: case OperandType::OneMinusSourceBlue: - getSource(shader, source, index); + getSource(shader, source, index, config); shader += ".b"; break; case OperandType::SourceAlpha: case OperandType::OneMinusSourceAlpha: - getSource(shader, source, index); + getSource(shader, source, index, config); shader += ".a"; break; @@ -360,14 +352,14 @@ void FragmentGenerator::getAlphaOperand(std::string& shader, TexEnvConfig::Sourc } } -void FragmentGenerator::getSource(std::string& shader, TexEnvConfig::Source source, int index) { +void FragmentGenerator::getSource(std::string& shader, TexEnvConfig::Source source, int index, const PICA::FragmentConfig& config) { switch (source) { case TexEnvConfig::Source::PrimaryColor: shader += "v_colour"; break; case TexEnvConfig::Source::Texture0: shader += "texture(u_tex0, v_texcoord0.xy)"; break; case TexEnvConfig::Source::Texture1: shader += "texture(u_tex1, v_texcoord1)"; break; case TexEnvConfig::Source::Texture2: { // If bit 13 in texture config is set then we use the texcoords for texture 1, otherwise for texture 2 - if (Helpers::getBit<13>(textureConfig)) { + if (Helpers::getBit<13>(config.texConfig.texUnitConfig)) { shader += "texture(u_tex2, v_texcoord1)"; } else { shader += "texture(u_tex2, v_texcoord2)"; @@ -428,12 +420,11 @@ void FragmentGenerator::getAlphaOperation(std::string& shader, TexEnvConfig::Ope } } -void FragmentGenerator::applyAlphaTest(std::string& shader, const PICARegs& regs) { - const u32 alphaConfig = regs[InternalRegs::AlphaTestConfig]; - const auto function = static_cast(Helpers::getBits<4, 3>(alphaConfig)); +void FragmentGenerator::applyAlphaTest(std::string& shader, const PICA::FragmentConfig& config) { + const CompareFunction function = config.outConfig.alphaTestFunction; // Alpha test disabled - if (Helpers::getBit<0>(alphaConfig) == 0 || function == CompareFunction::Always) { + if (function == CompareFunction::Always) { return; } @@ -458,7 +449,7 @@ void FragmentGenerator::applyAlphaTest(std::string& shader, const PICARegs& regs shader += ") { discard; }\n"; } -void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentConfig& config, const PICARegs& regs) { +void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentConfig& config) { if (!config.lighting.enable) { return; } @@ -481,7 +472,7 @@ void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentC for (int i = 0; i < config.lighting.lightNum; i++) { lightID = config.lighting.lights[i].num; - const auto& lightConfig = config.lighting.lights[lightID]; + const auto& lightConfig = config.lighting.lights[i]; shader += "light_position = lightSources[" + std::to_string(lightID) + "].position;\n"; if (lightConfig.directional) { // Directional lighting @@ -516,27 +507,27 @@ void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentC ", int(clamp(floor(distance_att_delta * 256.0), 0.0, 255.0)));\n"; } - compileLUTLookup(shader, config, regs, lightID, spotlightLutIndex); + compileLUTLookup(shader, config, i, spotlightLutIndex); shader += "spotlight_attenuation = lut_lookup_result;\n"; - compileLUTLookup(shader, config, regs, lightID, PICA::Lights::LUT_D0); + compileLUTLookup(shader, config, i, PICA::Lights::LUT_D0); shader += "specular0_dist = lut_lookup_result;\n"; - compileLUTLookup(shader, config, regs, lightID, PICA::Lights::LUT_D1); + compileLUTLookup(shader, config, i, PICA::Lights::LUT_D1); shader += "specular1_dist = lut_lookup_result;\n"; - compileLUTLookup(shader, config, regs, lightID, PICA::Lights::LUT_RR); + compileLUTLookup(shader, config, i, PICA::Lights::LUT_RR); shader += "reflected_color.r = lut_lookup_result;\n"; if (isSamplerEnabled(config.lighting.config, PICA::Lights::LUT_RG)) { - compileLUTLookup(shader, config, regs, lightID, PICA::Lights::LUT_RG); + compileLUTLookup(shader, config, i, PICA::Lights::LUT_RG); shader += "reflected_color.g = lut_lookup_result;\n"; } else { shader += "reflected_color.g = reflected_color.r;\n"; } if (isSamplerEnabled(config.lighting.config, PICA::Lights::LUT_RB)) { - compileLUTLookup(shader, config, regs, lightID, PICA::Lights::LUT_RB); + compileLUTLookup(shader, config, i, PICA::Lights::LUT_RB); shader += "reflected_color.b = lut_lookup_result;\n"; } else { shader += "reflected_color.b = reflected_color.r;\n"; @@ -565,7 +556,7 @@ void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentC } if (config.lighting.enablePrimaryAlpha || config.lighting.enableSecondaryAlpha) { - compileLUTLookup(shader, config, regs, lightID, PICA::Lights::LUT_FR); + compileLUTLookup(shader, config, config.lighting.lightNum - 1, PICA::Lights::LUT_FR); shader += "float fresnel_factor = lut_lookup_result;\n"; } @@ -602,45 +593,40 @@ bool FragmentGenerator::isSamplerEnabled(u32 environmentID, u32 lutID) { return samplerEnabled[environmentID * 7 + lutID]; } -void FragmentGenerator::compileLUTLookup(std::string& shader, const PICA::FragmentConfig& config, const PICARegs& regs, u32 lightIndex, u32 lutID) { +void FragmentGenerator::compileLUTLookup(std::string& shader, const PICA::FragmentConfig& config, u32 lightIndex, u32 lutID) { + const LightingLUTConfig& lut = config.lighting.luts[lutID]; + uint lightID = config.lighting.lights[lightIndex].num; uint lutIndex = 0; - int bitInConfig1 = 0; + bool lutEnabled = false; if (lutID == spotlightLutIndex) { // These are the spotlight attenuation LUTs - bitInConfig1 = 8 + (lightIndex & 0x7); - lutIndex = 8u + lightIndex; + lutIndex = 8u + lightID; + lutEnabled = config.lighting.lights[lightIndex].spotAttenuationEnable; } else if (lutID <= 6) { - bitInConfig1 = 16 + lutID; lutIndex = lutID; + lutEnabled = lut.enable; } else { Helpers::warn("Shadergen: Unimplemented LUT value"); } const bool samplerEnabled = isSamplerEnabled(config.lighting.config, lutID); - const u32 config1 = regs[InternalRegs::LightConfig1]; - if (!samplerEnabled || ((config1 >> bitInConfig1) & 1)) { + if (!samplerEnabled || !lutEnabled) { shader += "lut_lookup_result = 1.0;\n"; return; } - static constexpr float scales[] = {1.0f, 2.0f, 4.0f, 8.0f, 0.0f, 0.0f, 0.25f, 0.5f}; - const u32 lutAbs = regs[InternalRegs::LightLUTAbs]; - const u32 lutSelect = regs[InternalRegs::LightLUTSelect]; - const u32 lutScale = regs[InternalRegs::LightLUTScale]; - - // The way these bitfields are encoded is so cursed - float scale = scales[(lutScale >> (4 * lutID)) & 0x7]; - uint inputID = (lutSelect >> (4 * lutID)) & 0x7; - bool absEnabled = ((lutAbs >> (4 * lutID + 1)) & 0x1) == 0; // 0 = enabled... + float scale = lut.scale; + uint inputID = lut.type; + bool absEnabled = lut.absInput; switch (inputID) { case 0: shader += "lut_lookup_delta = dot(normal, normalize(half_vector));\n"; break; case 1: shader += "lut_lookup_delta = dot(normalize(v_view), normalize(half_vector));\n"; break; case 2: shader += "lut_lookup_delta = dot(normal, normalize(v_view));\n"; break; case 3: shader += "lut_lookup_delta = dot(normal, light_vector);\n"; break; - case 4: shader += "lut_lookup_delta = dot(light_vector, lightSources[" + std ::to_string(lightIndex) + "].spotlightDirection);\n"; break; + case 4: shader += "lut_lookup_delta = dot(light_vector, lightSources[" + std ::to_string(lightID) + "].spotlightDirection);\n"; break; default: Helpers::warn("Shadergen: Unimplemented LUT select"); diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 7fca385d..2d39f65f 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -784,37 +784,12 @@ OpenGL::Program& RendererGL::getSpecializedShader() { constexpr uint uboBlockBinding = 2; PICA::FragmentConfig fsConfig(regs); - auto& outConfig = fsConfig.outConfig; - auto& texConfig = fsConfig.texConfig; - - auto alphaTestConfig = regs[InternalRegs::AlphaTestConfig]; - auto alphaTestFunction = Helpers::getBits<4, 3>(alphaTestConfig); - - outConfig.alphaTestFunction = (alphaTestConfig & 1) ? static_cast(alphaTestFunction) : PICA::CompareFunction::Always; - outConfig.depthMapEnable = regs[InternalRegs::DepthmapEnable] & 1; - - texConfig.texUnitConfig = regs[InternalRegs::TexUnitCfg]; - texConfig.texEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; - - // Set up TEV stages. Annoyingly we can't just memcpy as the TEV registers are arranged like - // {Source, Operand, Combiner, Color, Scale} and we want to skip the color register since it's uploaded via UBO -#define setupTevStage(stage) \ - std::memcpy(&texConfig.tevConfigs[stage * 4], ®s[InternalRegs::TexEnv##stage##Source], 3 * sizeof(u32)); \ - texConfig.tevConfigs[stage * 4 + 3] = regs[InternalRegs::TexEnv##stage##Source + 5]; - - setupTevStage(0); - setupTevStage(1); - setupTevStage(2); - setupTevStage(3); - setupTevStage(4); - setupTevStage(5); -#undef setupTevStage CachedProgram& programEntry = shaderCache[fsConfig]; OpenGL::Program& program = programEntry.program; if (!program.exists()) { - std::string fs = fragShaderGen.generate(regs, fsConfig); + std::string fs = fragShaderGen.generate(fsConfig); OpenGL::Shader fragShader({fs.c_str(), fs.size()}, OpenGL::Fragment); program.create({defaultShadergenVs, fragShader}); From 69c79a7f6c8eafe5f042b37de8d750129d7f6869 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 20 Jul 2024 03:40:50 +0300 Subject: [PATCH 110/251] Ubershader: Add lighting shadergen override --- include/config.hpp | 5 +++++ include/renderer.hpp | 5 +++++ include/renderer_gl/renderer_gl.hpp | 4 ++-- src/config.cpp | 5 +++++ src/core/PICA/gpu.cpp | 4 ++++ src/core/renderer_gl/renderer_gl.cpp | 13 +++++++++++++ 6 files changed, 34 insertions(+), 2 deletions(-) diff --git a/include/config.hpp b/include/config.hpp index ed2c270f..a3fc77e4 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -20,6 +20,11 @@ struct EmulatorConfig { bool discordRpcEnabled = false; bool useUbershaders = ubershaderDefault; bool accurateShaderMul = false; + + // Toggles whether to force shadergen when there's more than N lights active and we're using the ubershader, for better performance + bool forceShadergenForLights = true; + int lightShadergenThreshold = 1; + RendererType rendererType = RendererType::OpenGL; Audio::DSPCore::Type dspType = Audio::DSPCore::Type::Null; diff --git a/include/renderer.hpp b/include/renderer.hpp index e64d49e3..569a730b 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -20,6 +20,7 @@ enum class RendererType : s8 { Software = 3, }; +struct EmulatorConfig; class GPU; struct SDL_Window; @@ -46,6 +47,8 @@ class Renderer { u32 outputWindowWidth = 400; u32 outputWindowHeight = 240 * 2; + EmulatorConfig* emulatorConfig = nullptr; + public: Renderer(GPU& gpu, const std::array& internalRegs, const std::array& externalRegs); virtual ~Renderer(); @@ -101,4 +104,6 @@ class Renderer { outputWindowWidth = width; outputWindowHeight = height; } + + void setConfig(EmulatorConfig* config) { emulatorConfig = config; } }; diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index abde96bf..bfa9922b 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -30,7 +30,7 @@ class RendererGL final : public Renderer { OpenGL::VertexArray vao; OpenGL::VertexBuffer vbo; - bool usingUbershader = true; + bool enableUbershader = true; // Data struct { @@ -110,7 +110,7 @@ class RendererGL final : public Renderer { virtual std::string getUbershader() override; virtual void setUbershader(const std::string& shader) override; - virtual void setUbershaderSetting(bool value) override { usingUbershader = value; } + virtual void setUbershaderSetting(bool value) override { enableUbershader = value; } std::optional getColourBuffer(u32 addr, PICA::ColorFmt format, u32 width, u32 height, bool createIfnotFound = true); diff --git a/src/config.cpp b/src/config.cpp index cc34d148..dae5a0ab 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -64,6 +64,9 @@ void EmulatorConfig::load() { vsyncEnabled = toml::find_or(gpu, "EnableVSync", true); useUbershaders = toml::find_or(gpu, "UseUbershaders", ubershaderDefault); accurateShaderMul = toml::find_or(gpu, "AccurateShaderMultiplication", false); + + forceShadergenForLights = toml::find_or(gpu, "ForceShadergenForLighting", true); + lightShadergenThreshold = toml::find_or(gpu, "ShadergenLightThreshold", 1); } } @@ -130,6 +133,8 @@ void EmulatorConfig::save() { data["GPU"]["EnableVSync"] = vsyncEnabled; data["GPU"]["AccurateShaderMultiplication"] = accurateShaderMul; data["GPU"]["UseUbershaders"] = useUbershaders; + data["GPU"]["ForceShadergenForLighting"] = forceShadergenForLights; + data["GPU"]["ShadergenLightThreshold"] = lightShadergenThreshold; data["Audio"]["DSPEmulation"] = std::string(Audio::DSPCore::typeToString(dspType)); data["Audio"]["EnableAudio"] = audioEnabled; diff --git a/src/core/PICA/gpu.cpp b/src/core/PICA/gpu.cpp index a54fe6eb..ace49fea 100644 --- a/src/core/PICA/gpu.cpp +++ b/src/core/PICA/gpu.cpp @@ -58,6 +58,10 @@ GPU::GPU(Memory& mem, EmulatorConfig& config) : mem(mem), config(config) { break; } } + + if (renderer != nullptr) { + renderer->setConfig(&config); + } } void GPU::reset() { diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 2d39f65f..22750f7d 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -4,6 +4,7 @@ #include +#include "config.hpp" #include "PICA/float_types.hpp" #include "PICA/pica_frag_uniforms.hpp" #include "PICA/gpu.hpp" @@ -383,6 +384,18 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v OpenGL::Triangle, }; + bool usingUbershader = enableUbershader; + if (usingUbershader) { + const bool lightsEnabled = (regs[InternalRegs::LightingEnable] & 1) != 0; + const uint lightCount = (regs[InternalRegs::LightNumber] & 0x7) + 1; + + // Emulating lights in the ubershader is incredibly slow, so we've got an option to render draws using moret han N lights via shadergen + // This way we generate fewer shaders overall than with full shadergen, but don't tank performance + if (emulatorConfig->forceShadergenForLights && lightsEnabled && lightCount >= emulatorConfig->lightShadergenThreshold) { + usingUbershader = false; + } + } + if (usingUbershader) { gl.useProgram(triangleProgram); } else { From 8091e44206615bb0d63000c3d27e8811f0b71f8f Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 20 Jul 2024 03:48:48 +0300 Subject: [PATCH 111/251] Add shadergen lighting override options to LR core --- src/libretro_core.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index b48e937a..fc3e53b3 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -155,6 +155,8 @@ static void configInit() { {"panda3ds_write_protect_virtual_sd", "Write protect virtual SD card; disabled|enabled"}, {"panda3ds_battery_level", "Battery percentage; 5|10|20|30|50|70|90|100"}, {"panda3ds_use_charger", "Charger plugged; enabled|disabled"}, + {"panda3ds_ubershader_lighting_override", "Force shadergen when rendering lights; enabled|disabled"}, + {"panda3ds_ubershader_lighting_override_threshold", "Light threshold for forcing shadergen; 1|2|3|4|5|6|7|8"}, {nullptr, nullptr}, }; @@ -175,6 +177,8 @@ static void configUpdate() { config.sdWriteProtected = FetchVariableBool("panda3ds_write_protect_virtual_sd", false); config.accurateShaderMul = FetchVariableBool("panda3ds_accurate_shader_mul", false); config.useUbershaders = FetchVariableBool("panda3ds_use_ubershader", true); + config.forceShadergenForLights = FetchVariableBool("panda3ds_ubershader_lighting_override", true); + config.lightShadergenThreshold = std::clamp(std::stoi(FetchVariable("panda3ds_ubershader_lighting_override_threshold", "1")), 1, 8); config.discordRpcEnabled = false; config.save(); From 4214d9bce4944ea7d004e38e271b2bf67b86228d Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 20 Jul 2024 17:45:14 +0300 Subject: [PATCH 112/251] Adjust ubershader defaults --- include/config.hpp | 7 ++++++- src/libretro_core.cpp | 3 ++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/include/config.hpp b/include/config.hpp index a3fc77e4..25f352e8 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -13,8 +13,13 @@ struct EmulatorConfig { static constexpr bool shaderJitDefault = false; #endif - // For now, use specialized shaders by default + // For now, use specialized shaders by default on MacOS as M1 drivers are buggy when using the ubershader, and on Android since mobile GPUs are + // horrible On other platforms we default to ubershader + shadergen fallback for lights +#if defined(__ANDROID__) || defined(__APPLE__) static constexpr bool ubershaderDefault = false; +#else + static constexpr bool ubershaderDefault = true; +#endif bool shaderJitEnabled = shaderJitDefault; bool discordRpcEnabled = false; diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index fc3e53b3..02bf3cd1 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -147,7 +147,8 @@ static void configInit() { static const retro_variable values[] = { {"panda3ds_use_shader_jit", "Enable shader JIT; enabled|disabled"}, {"panda3ds_accurate_shader_mul", "Enable accurate shader multiplication; disabled|enabled"}, - {"panda3ds_use_ubershader", "Use ubershaders (No stutter, maybe slower); disabled|enabled"}, + {"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_dsp_emulation", "DSP emulation; Null|HLE|LLE"}, {"panda3ds_use_audio", "Enable audio; disabled|enabled"}, From 5c40fb0cbf683a586d92e43da5002a6c5b3e580a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 20 Jul 2024 18:37:35 +0300 Subject: [PATCH 113/251] Fix oopsie --- include/config.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/config.hpp b/include/config.hpp index 25f352e8..52be1af7 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -14,7 +14,7 @@ struct EmulatorConfig { #endif // For now, use specialized shaders by default on MacOS as M1 drivers are buggy when using the ubershader, and on Android since mobile GPUs are - // horrible On other platforms we default to ubershader + shadergen fallback for lights + // horrible. On other platforms we default to ubershader + shadergen fallback for lights #if defined(__ANDROID__) || defined(__APPLE__) static constexpr bool ubershaderDefault = false; #else @@ -51,4 +51,4 @@ struct EmulatorConfig { EmulatorConfig(const std::filesystem::path& path); void load(); void save(); -}; \ No newline at end of file +}; From f219432c6ac3926a2eb2accea6ea2797940c3af8 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 20 Jul 2024 23:18:52 +0300 Subject: [PATCH 114/251] Renderer GL: Don't leak shader/UBO handles --- include/renderer_gl/renderer_gl.hpp | 1 + src/core/renderer_gl/renderer_gl.cpp | 23 ++++++++++++++--------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index bfa9922b..d00445ac 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -117,6 +117,7 @@ class RendererGL final : public Renderer { // Note: The caller is responsible for deleting the currently bound FBO before calling this void setFBO(uint handle) { screenFramebuffer.m_handle = handle; } void resetStateManager() { gl.reset(); } + void clearShaderCache(); void initUbershader(OpenGL::Program& program); #ifdef PANDA3DS_FRONTEND_QT diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 22750f7d..36827027 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -24,10 +24,7 @@ void RendererGL::reset() { colourBufferCache.reset(); textureCache.reset(); - for (auto& shader : shaderCache) { - shader.second.program.free(); - } - shaderCache.clear(); + clearShaderCache(); // Init the colour/depth buffer settings to some random defaults on reset colourBufferLoc = 0; @@ -808,6 +805,8 @@ OpenGL::Program& RendererGL::getSpecializedShader() { program.create({defaultShadergenVs, fragShader}); gl.useProgram(program); + fragShader.free(); + // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 glUniform1i(OpenGL::uniformLocation(program, "u_tex0"), 0); glUniform1i(OpenGL::uniformLocation(program, "u_tex1"), 1); @@ -937,16 +936,22 @@ void RendererGL::screenshot(const std::string& name) { stbi_write_png(name.c_str(), width, height, 4, flippedPixels.data(), 0); } +void RendererGL::clearShaderCache() { + for (auto& shader : shaderCache) { + CachedProgram& cachedProgram = shader.second; + cachedProgram.program.free(); + glDeleteBuffers(1, &cachedProgram.uboBinding); + } + + shaderCache.clear(); +} + void RendererGL::deinitGraphicsContext() { // Invalidate all surface caches since they'll no longer be valid textureCache.reset(); depthBufferCache.reset(); colourBufferCache.reset(); - - for (auto& shader : shaderCache) { - shader.second.program.free(); - } - shaderCache.clear(); + clearShaderCache(); // All other GL objects should be invalidated automatically and be recreated by the next call to initGraphicsContext // TODO: Make it so that depth and colour buffers get written back to 3DS memory From 8611e98b92f8c60ae634d5bf890e8e2dc523f402 Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Sat, 20 Jul 2024 23:21:00 +0300 Subject: [PATCH 115/251] Libretro: Add support for touch input --- src/libretro_core.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index 02bf3cd1..3e0436b8 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -15,6 +15,8 @@ static retro_input_state_t inputStateCallback; static retro_hw_render_callback hw_render; static std::filesystem::path savePath; +static bool screenTouched; + std::unique_ptr emulator; RendererGL* renderer; @@ -314,7 +316,8 @@ void retro_run() { hid.setCirclepadX((xLeft / +32767) * 0x9C); hid.setCirclepadY((yLeft / -32767) * 0x9C); - bool touch = inputStateCallback(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT); + bool touchScreen = false; + const int posX = inputStateCallback(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_X); const int posY = inputStateCallback(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_Y); @@ -324,16 +327,23 @@ void retro_run() { const int offsetX = 40; const int offsetY = emulator->height / 2; - const bool inScreenX = newX >= offsetX && newX < emulator->width - offsetX; + const bool inScreenX = newX >= offsetX && newX <= emulator->width - offsetX; const bool inScreenY = newY >= offsetY && newY <= emulator->height; - if (touch && inScreenX && inScreenY) { + if (inScreenX && inScreenY) { + touchScreen |= inputStateCallback(0, RETRO_DEVICE_MOUSE, 0, RETRO_DEVICE_ID_MOUSE_LEFT); + touchScreen |= inputStateCallback(0, RETRO_DEVICE_POINTER, 0, RETRO_DEVICE_ID_POINTER_PRESSED); + } + + if (touchScreen) { u16 x = static_cast(newX - offsetX); u16 y = static_cast(newY - offsetY); hid.setTouchScreenPress(x, y); - } else { + screenTouched = true; + } else if (screenTouched) { hid.releaseTouchScreen(); + screenTouched = false; } hid.updateInputs(emulator->getTicks()); From 8b26e1e3fcf9d7b42d8917b3a75acb121210709b Mon Sep 17 00:00:00 2001 From: offtkp Date: Sun, 21 Jul 2024 15:42:12 +0300 Subject: [PATCH 116/251] Fix shadowed variable in ubershader --- src/host_shaders/opengl_fragment_shader.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index 9f369e39..48b55a4c 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -307,8 +307,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { primary_color = vec4(vec3(0.0), 1.0); secondary_color = vec4(vec3(0.0), 1.0); - uint GPUREG_LIGHTING_LUTINPUT_SCALE = readPicaReg(0x01D2u); uint GPUREG_LIGHTING_CONFIG0 = readPicaReg(0x01C3u); + GPUREG_LIGHTING_LUTINPUT_SCALE = readPicaReg(0x01D2u); GPUREG_LIGHTING_CONFIG1 = readPicaReg(0x01C4u); GPUREG_LIGHTING_LUTINPUT_ABS = readPicaReg(0x01D0u); GPUREG_LIGHTING_LUTINPUT_SELECT = readPicaReg(0x01D1u); From 2a6cd3c5ea29cd39ad1719e64fe65ea96a94ccbc Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 21 Jul 2024 16:02:22 +0300 Subject: [PATCH 117/251] Separate graphics API/Language types from the fragment recompiler --- CMakeLists.txt | 2 +- include/PICA/shader_gen.hpp | 7 +------ include/PICA/shader_gen_types.hpp | 9 +++++++++ 3 files changed, 11 insertions(+), 7 deletions(-) create mode 100644 include/PICA/shader_gen_types.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c52ccd51..fdfe8a4a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -249,7 +249,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/audio/dsp_core.hpp include/audio/null_core.hpp include/audio/teakra_core.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp include/PICA/pica_frag_config.hpp - include/PICA/pica_frag_uniforms.hpp + include/PICA/pica_frag_uniforms.hpp include/PICA/shader_gen_types.hpp ) cmrc_add_resource_library( diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index 6cf810a0..085d990a 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -4,15 +4,10 @@ #include "PICA/gpu.hpp" #include "PICA/pica_frag_config.hpp" #include "PICA/regs.hpp" +#include "PICA/shader_gen_types.hpp" #include "helpers.hpp" namespace PICA::ShaderGen { - // Graphics API this shader is targetting - enum class API { GL, GLES, Vulkan }; - - // Shading language to use (Only GLSL for the time being) - enum class Language { GLSL }; - class FragmentGenerator { API api; Language language; diff --git a/include/PICA/shader_gen_types.hpp b/include/PICA/shader_gen_types.hpp new file mode 100644 index 00000000..1877227f --- /dev/null +++ b/include/PICA/shader_gen_types.hpp @@ -0,0 +1,9 @@ +#pragma once + +namespace PICA::ShaderGen { + // Graphics API this shader is targetting + enum class API { GL, GLES, Vulkan }; + + // Shading language to use (Only GLSL for the time being) + enum class Language { GLSL }; +} // namespace PICA::ShaderGen \ No newline at end of file From be1c801fc24467cfefdc9e8f371418bca8269adb Mon Sep 17 00:00:00 2001 From: offtkp Date: Sun, 21 Jul 2024 16:37:37 +0300 Subject: [PATCH 118/251] Fix hashing for FragmentConfig --- include/PICA/pica_frag_config.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index 89dd3420..ee18eee0 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -12,11 +12,11 @@ namespace PICA { struct OutputConfig { union { - u32 raw; + u32 raw{}; // Merge the enable + compare function into 1 field to avoid duplicate shaders // enable == off means a CompareFunction of Always BitField<0, 3, CompareFunction> alphaTestFunction; - BitField<4, 1, u32> depthMapEnable; + BitField<3, 1, u32> depthMapEnable; }; }; From b333bf8a0c66e80a7b0f622ad123eeb8b6f27428 Mon Sep 17 00:00:00 2001 From: offtkp Date: Sun, 21 Jul 2024 17:28:40 +0300 Subject: [PATCH 119/251] Use u32 for scale instead of float in FragmentConfig --- include/PICA/pica_frag_config.hpp | 17 ++++++++--------- src/core/PICA/shader_gen_glsl.cpp | 13 +++++++++---- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index ee18eee0..f4142ef1 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -49,8 +49,8 @@ namespace PICA { BitField<0, 1, u32> enable; BitField<1, 1, u32> absInput; BitField<2, 3, u32> type; + BitField<5, 3, u32> scale; }; - float scale; }; struct LightingConfig { @@ -142,46 +142,45 @@ namespace PICA { const u32 lutAbs = regs[InternalRegs::LightLUTAbs]; const u32 lutSelect = regs[InternalRegs::LightLUTSelect]; const u32 lutScale = regs[InternalRegs::LightLUTScale]; - static constexpr float scales[] = {1.0f, 2.0f, 4.0f, 8.0f, 0.0f, 0.0f, 0.25f, 0.5f}; if (d0.enable) { d0.absInput = Helpers::getBit<1>(lutAbs) == 0; d0.type = Helpers::getBits<0, 3>(lutSelect); - d0.scale = scales[Helpers::getBits<0, 3>(lutScale)]; + d0.scale = Helpers::getBits<0, 3>(lutScale); } if (d1.enable) { d1.absInput = Helpers::getBit<5>(lutAbs) == 0; d1.type = Helpers::getBits<4, 3>(lutSelect); - d1.scale = scales[Helpers::getBits<4, 3>(lutScale)]; + d1.scale = Helpers::getBits<4, 3>(lutScale); } sp.absInput = Helpers::getBit<9>(lutAbs) == 0; sp.type = Helpers::getBits<8, 3>(lutSelect); - sp.scale = scales[Helpers::getBits<8, 3>(lutScale)]; + sp.scale = Helpers::getBits<8, 3>(lutScale); if (fr.enable) { fr.absInput = Helpers::getBit<13>(lutAbs) == 0; fr.type = Helpers::getBits<12, 3>(lutSelect); - fr.scale = scales[Helpers::getBits<12, 3>(lutScale)]; + fr.scale = Helpers::getBits<12, 3>(lutScale); } if (rb.enable) { rb.absInput = Helpers::getBit<17>(lutAbs) == 0; rb.type = Helpers::getBits<16, 3>(lutSelect); - rb.scale = scales[Helpers::getBits<16, 3>(lutScale)]; + rb.scale = Helpers::getBits<16, 3>(lutScale); } if (rg.enable) { rg.absInput = Helpers::getBit<21>(lutAbs) == 0; rg.type = Helpers::getBits<20, 3>(lutSelect); - rg.scale = scales[Helpers::getBits<20, 3>(lutScale)]; + rg.scale = Helpers::getBits<20, 3>(lutScale); } if (rr.enable) { rr.absInput = Helpers::getBit<25>(lutAbs) == 0; rr.type = Helpers::getBits<24, 3>(lutSelect); - rr.scale = scales[Helpers::getBits<24, 3>(lutScale)]; + rr.scale = Helpers::getBits<24, 3>(lutScale); } } }; diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 47df58b8..3d688bd2 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -617,7 +617,7 @@ void FragmentGenerator::compileLUTLookup(std::string& shader, const PICA::Fragme return; } - float scale = lut.scale; + uint scale = lut.scale; uint inputID = lut.type; bool absEnabled = lut.absInput; @@ -634,17 +634,22 @@ void FragmentGenerator::compileLUTLookup(std::string& shader, const PICA::Fragme break; } + static constexpr float scales[] = {1.0f, 2.0f, 4.0f, 8.0f, 0.0f, 0.0f, 0.25f, 0.5f}; + if (absEnabled) { bool twoSidedDiffuse = config.lighting.lights[lightIndex].twoSidedDiffuse; shader += twoSidedDiffuse ? "lut_lookup_delta = abs(lut_lookup_delta);\n" : "lut_lookup_delta = max(lut_lookup_delta, 0.0);\n"; shader += "lut_lookup_result = lutLookup(" + std::to_string(lutIndex) + ", int(clamp(floor(lut_lookup_delta * 256.0), 0.0, 255.0)));\n"; - if (scale != 1.0) { - shader += "lut_lookup_result *= " + std::to_string(scale) + ";\n"; + if (scale != 0) { + shader += "lut_lookup_result *= " + std::to_string(scales[scale]) + ";\n"; } } else { // Range is [-1, 1] so we need to map it to [0, 1] shader += "lut_lookup_index = int(clamp(floor(lut_lookup_delta * 128.0), -128.f, 127.f));\n"; shader += "if (lut_lookup_index < 0) lut_lookup_index += 256;\n"; - shader += "lut_lookup_result = lutLookup(" + std::to_string(lutIndex) + ", lut_lookup_index) *" + std::to_string(scale) + ";\n"; + shader += "lut_lookup_result = lutLookup(" + std::to_string(lutIndex) + ", lut_lookup_index);\n"; + if (scale != 0) { + shader += "lut_lookup_result *= " + std::to_string(scales[scale]) + ";\n"; + } } } \ No newline at end of file From 4176a1925623252200804ff3ffe00f4dc8d3da09 Mon Sep 17 00:00:00 2001 From: offtkp Date: Sun, 21 Jul 2024 03:16:15 +0300 Subject: [PATCH 120/251] Fog in ubershader --- docs/3ds/lighting.md | 2 +- include/PICA/gpu.hpp | 3 + include/PICA/regs.hpp | 12 ++++ include/renderer_gl/renderer_gl.hpp | 3 +- src/core/PICA/gpu.cpp | 3 + src/core/PICA/regs.cpp | 15 +++++ src/core/PICA/shader_gen_glsl.cpp | 4 +- src/core/renderer_gl/renderer_gl.cpp | 58 +++++++++++++++----- src/host_shaders/opengl_fragment_shader.frag | 30 +++++++++- 9 files changed, 110 insertions(+), 20 deletions(-) diff --git a/docs/3ds/lighting.md b/docs/3ds/lighting.md index 9f4ff2f2..8b6b9885 100644 --- a/docs/3ds/lighting.md +++ b/docs/3ds/lighting.md @@ -56,7 +56,7 @@ lut_id is one of these values 6 RR lut_index on the other hand represents the actual index of the LUT in the texture -u_tex_lighting_lut has 24 LUTs and they are used like so: +u_tex_luts has 24 LUTs for lighting and they are used like so: 0 D0 1 D1 2 is missing because SP uses LUTs 8-15 diff --git a/include/PICA/gpu.hpp b/include/PICA/gpu.hpp index 61020f76..1e37729b 100644 --- a/include/PICA/gpu.hpp +++ b/include/PICA/gpu.hpp @@ -92,6 +92,9 @@ class GPU { // Set to false by the renderer when the lighting_lut is uploaded ot the GPU bool lightingLUTDirty = false; + std::array fogLUT; + bool fogLUTDirty = false; + GPU(Memory& mem, EmulatorConfig& config); void display() { renderer->display(); } void screenshot(const std::string& name) { renderer->screenshot(name); } diff --git a/include/PICA/regs.hpp b/include/PICA/regs.hpp index c4d6a5fb..c66c90ca 100644 --- a/include/PICA/regs.hpp +++ b/include/PICA/regs.hpp @@ -51,6 +51,18 @@ namespace PICA { #undef defineTexEnv // clang-format on + // Fog registers + FogColor = 0xE1, + FogLUTIndex = 0xE6, + FogLUTData0 = 0xE8, + FogLUTData1 = 0xE9, + FogLUTData2 = 0xEA, + FogLUTData3 = 0xEB, + FogLUTData4 = 0xEC, + FogLUTData5 = 0xED, + FogLUTData6 = 0xEE, + FogLUTData7 = 0xEF, + // Framebuffer registers ColourOperation = 0x100, BlendFunc = 0x101, diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index d00445ac..f5a964a3 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -63,7 +63,7 @@ class RendererGL final : public Renderer { OpenGL::VertexBuffer dummyVBO; OpenGL::Texture screenTexture; - OpenGL::Texture lightLUTTexture; + OpenGL::Texture LUTTexture; OpenGL::Framebuffer screenFramebuffer; OpenGL::Texture blankTexture; // The "default" vertex shader to use when using specialized shaders but not PICA vertex shader -> GLSL recompilation @@ -90,6 +90,7 @@ class RendererGL final : public Renderer { void setupUbershaderTexEnv(); void bindTexturesToSlots(); void updateLightingLUT(); + void updateFogLUT(); void initGraphicsContextInternal(); public: diff --git a/src/core/PICA/gpu.cpp b/src/core/PICA/gpu.cpp index ace49fea..fe336edc 100644 --- a/src/core/PICA/gpu.cpp +++ b/src/core/PICA/gpu.cpp @@ -74,6 +74,9 @@ void GPU::reset() { lightingLUT.fill(0); lightingLUTDirty = true; + fogLUT.fill(0); + fogLUTDirty = true; + totalAttribCount = 0; fixedAttribMask = 0; fixedAttribIndex = 0; diff --git a/src/core/PICA/regs.cpp b/src/core/PICA/regs.cpp index baaa2256..45e624ec 100644 --- a/src/core/PICA/regs.cpp +++ b/src/core/PICA/regs.cpp @@ -135,6 +135,21 @@ void GPU::writeInternalReg(u32 index, u32 value, u32 mask) { break; } + case FogLUTData0: + case FogLUTData1: + case FogLUTData2: + case FogLUTData3: + case FogLUTData4: + case FogLUTData5: + case FogLUTData6: + case FogLUTData7: { + const uint32_t index = regs[FogLUTIndex] & 127; + fogLUT[index] = value; + fogLUTDirty = true; + regs[FogLUTIndex] = (index + 1) & 127; + break; + } + case LightingLUTData0: case LightingLUTData1: case LightingLUTData2: diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 3d688bd2..01210587 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -130,7 +130,7 @@ std::string FragmentGenerator::generate(const FragmentConfig& config) { uniform sampler2D u_tex0; uniform sampler2D u_tex1; uniform sampler2D u_tex2; - uniform sampler2D u_tex_lighting_lut; + uniform sampler2D u_tex_luts; )"; ret += uniformDefinition; @@ -144,7 +144,7 @@ std::string FragmentGenerator::generate(const FragmentConfig& config) { } float lutLookup(uint lut, int index) { - return texelFetch(u_tex_lighting_lut, ivec2(index, int(lut)), 0).r; + return texelFetch(u_tex_luts, ivec2(index, int(lut)), 0).r; } vec3 regToColor(uint reg) { diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 36827027..b6c90374 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -115,10 +115,11 @@ void RendererGL::initGraphicsContextInternal() { const u32 screenTextureWidth = 400; // Top screen is 400 pixels wide, bottom is 320 const u32 screenTextureHeight = 2 * 240; // Both screens are 240 pixels tall - lightLUTTexture.create(256, Lights::LUT_Count, GL_R32F); - lightLUTTexture.bind(); - lightLUTTexture.setMinFilter(OpenGL::Linear); - lightLUTTexture.setMagFilter(OpenGL::Linear); + // 24 rows for light, 1 for fog + LUTTexture.create(256, Lights::LUT_Count + 1, GL_RG32F); + LUTTexture.bind(); + LUTTexture.setMinFilter(OpenGL::Linear); + LUTTexture.setMagFilter(OpenGL::Linear); auto prevTexture = OpenGL::getTex2D(); @@ -353,22 +354,49 @@ void RendererGL::bindTexturesToSlots() { } glActiveTexture(GL_TEXTURE0 + 3); - lightLUTTexture.bind(); + LUTTexture.bind(); glActiveTexture(GL_TEXTURE0); } void RendererGL::updateLightingLUT() { gpu.lightingLUTDirty = false; - std::array lightingLut; + std::array lightingLut; - for (int i = 0; i < gpu.lightingLUT.size(); i++) { - uint64_t value = gpu.lightingLUT[i] & 0xFFF; + for (int i = 0; i < lightingLut.size(); i += 2) { + uint64_t value = gpu.lightingLUT[i >> 1] & 0xFFF; lightingLut[i] = (float)(value << 4) / 65535.0f; } glActiveTexture(GL_TEXTURE0 + 3); - lightLUTTexture.bind(); - glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 256, Lights::LUT_Count, GL_RED, GL_FLOAT, lightingLut.data()); + LUTTexture.bind(); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, 256, Lights::LUT_Count, GL_RG, GL_FLOAT, lightingLut.data()); + glActiveTexture(GL_TEXTURE0); +} + +void RendererGL::updateFogLUT() { + gpu.fogLUTDirty = false; + + // Fog LUT elements are of this type: + // 0-12 fixed1.1.11, Difference from next element + // 13-23 fixed0.0.11, Value + // We will store them as a 128x1 RG texture with R being the value and G being the difference + std::array fogLut; + + for (int i = 0; i < fogLut.size(); i += 2) { + const uint32_t value = gpu.fogLUT[i >> 1]; + int32_t diff = value & 0x1fff; + diff = (diff << 19) >> 19; // Sign extend the 13-bit value to 32 bits + const float fogDifference = float(diff) / 2048.0f; + const float fogValue = float((value >> 13) & 0x7ff) / 2048.0f; + + fogLut[i] = fogValue; + fogLut[i + 1] = fogDifference; + } + + glActiveTexture(GL_TEXTURE0 + 3); + LUTTexture.bind(); + // The fog LUT exists at the end of the lighting LUT + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, Lights::LUT_Count, 128, 1, GL_RG, GL_FLOAT, fogLut.data()); glActiveTexture(GL_TEXTURE0); } @@ -453,6 +481,10 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v bindTexturesToSlots(); + if (gpu.fogLUTDirty) { + updateFogLUT(); + } + if (gpu.lightingLUTDirty) { updateLightingLUT(); } @@ -811,7 +843,7 @@ OpenGL::Program& RendererGL::getSpecializedShader() { glUniform1i(OpenGL::uniformLocation(program, "u_tex0"), 0); glUniform1i(OpenGL::uniformLocation(program, "u_tex1"), 1); glUniform1i(OpenGL::uniformLocation(program, "u_tex2"), 2); - glUniform1i(OpenGL::uniformLocation(program, "u_tex_lighting_lut"), 3); + glUniform1i(OpenGL::uniformLocation(program, "u_tex_luts"), 3); // Allocate memory for the program UBO glGenBuffers(1, &programEntry.uboBinding); @@ -994,9 +1026,9 @@ void RendererGL::initUbershader(OpenGL::Program& program) { ubershaderData.depthmapEnableLoc = OpenGL::uniformLocation(program, "u_depthmapEnable"); ubershaderData.picaRegLoc = OpenGL::uniformLocation(program, "u_picaRegs"); - // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 + // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, light maps go in TU 3, and the fog map goes in TU 4 glUniform1i(OpenGL::uniformLocation(program, "u_tex0"), 0); glUniform1i(OpenGL::uniformLocation(program, "u_tex1"), 1); glUniform1i(OpenGL::uniformLocation(program, "u_tex2"), 2); - glUniform1i(OpenGL::uniformLocation(program, "u_tex_lighting_lut"), 3); + glUniform1i(OpenGL::uniformLocation(program, "u_tex_luts"), 3); } diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index 48b55a4c..b9f9fe4c 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -25,7 +25,7 @@ uniform bool u_depthmapEnable; uniform sampler2D u_tex0; uniform sampler2D u_tex1; uniform sampler2D u_tex2; -uniform sampler2D u_tex_lighting_lut; +uniform sampler2D u_tex_luts; uniform uint u_picaRegs[0x200 - 0x48]; @@ -152,6 +152,8 @@ vec4 tevCalculateCombiner(int tev_id) { #define RG_LUT 5u #define RR_LUT 6u +#define FOG_INDEX 24 + uint GPUREG_LIGHTi_CONFIG; uint GPUREG_LIGHTING_CONFIG1; uint GPUREG_LIGHTING_LUTINPUT_SELECT; @@ -161,7 +163,7 @@ bool error_unimpl = false; vec4 unimpl_color = vec4(1.0, 0.0, 1.0, 1.0); float lutLookup(uint lut, int index) { - return texelFetch(u_tex_lighting_lut, ivec2(index, int(lut)), 0).r; + return texelFetch(u_tex_luts, ivec2(index, int(lut)), 0).r; } vec3 regToColor(uint reg) { @@ -494,7 +496,7 @@ void main() { if (tevUnimplementedSourceFlag) { // fragColour = vec4(1.0, 0.0, 1.0, 1.0); } - // fragColour.rg = texture(u_tex_lighting_lut,vec2(gl_FragCoord.x/200.,float(int(gl_FragCoord.y/2)%24))).rr; + // fragColour.rg = texture(u_tex_luts,vec2(gl_FragCoord.x/200.,float(int(gl_FragCoord.y/2)%24))).rr; // Get original depth value by converting from [near, far] = [0, 1] to [-1, 1] // We do this by converting to [0, 2] first and subtracting 1 to go to [-1, 1] @@ -507,6 +509,28 @@ void main() { // Write final fragment depth gl_FragDepth = depth; + bool enable_fog = (textureEnvUpdateBuffer & 7u) == 5u; + + if (enable_fog) { + bool flip_depth = (textureEnvUpdateBuffer & (1u << 16)) != 0u; + float fog_index = flip_depth ? 1.0 - depth : depth; + fog_index *= 128.0; + float clamped_index = clamp(floor(fog_index), 0.0, 127.0); + float delta = fog_index - clamped_index; + vec2 value = texelFetch(u_tex_luts, ivec2(int(clamped_index), FOG_INDEX), 0).rg; + float fog_factor = clamp(value.r + value.g * delta, 0.0, 1.0); + + uint GPUREG_FOG_COLOR = readPicaReg(0x00E1u); + + // Annoyingly color is not encoded in the same way as light color + float r = (GPUREG_FOG_COLOR & 0xFFu) / 255.0; + float g = ((GPUREG_FOG_COLOR >> 8) & 0xFFu) / 255.0; + float b = ((GPUREG_FOG_COLOR >> 16) & 0xFFu) / 255.0; + vec3 fog_color = vec3(r, g, b); + + fragColour.rgb = mix(fog_color, fragColour.rgb, fog_factor); + } + // Perform alpha test uint alphaControl = readPicaReg(0x104u); if ((alphaControl & 1u) != 0u) { // Check if alpha test is on From b90c15919b0334a34c80b7f52d29f060699a54a3 Mon Sep 17 00:00:00 2001 From: offtkp Date: Sun, 21 Jul 2024 16:32:45 +0300 Subject: [PATCH 121/251] Shadergen fog --- include/PICA/pica_frag_config.hpp | 21 ++++++++++++++++++++- include/PICA/regs.hpp | 6 ++++++ include/PICA/shader_gen.hpp | 2 ++ src/core/PICA/shader_gen_glsl.cpp | 25 +++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 1 deletion(-) diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index f4142ef1..32fa7aa6 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -29,6 +29,18 @@ namespace PICA { std::array tevConfigs; }; + struct FogConfig { + union { + u32 raw{}; + + BitField<0, 3, FogMode> mode; + BitField<3, 1, u32> flipDepth; + BitField<8, 8, u32> fogColorR; + BitField<16, 8, u32> fogColorG; + BitField<24, 8, u32> fogColorB; + }; + }; + struct Light { union { u16 raw; @@ -189,6 +201,7 @@ namespace PICA { struct FragmentConfig { OutputConfig outConfig; TextureConfig texConfig; + FogConfig fogConfig; LightingConfig lighting; bool operator==(const FragmentConfig& config) const { @@ -220,12 +233,18 @@ namespace PICA { setupTevStage(4); setupTevStage(5); #undef setupTevStage + + fogConfig.mode = (FogMode)Helpers::getBits<0, 3>(regs[InternalRegs::TexEnvUpdateBuffer]); + fogConfig.flipDepth = Helpers::getBit<16>(regs[InternalRegs::TexEnvUpdateBuffer]); + fogConfig.fogColorR = Helpers::getBits<0, 8>(regs[InternalRegs::FogColor]); + fogConfig.fogColorG = Helpers::getBits<8, 8>(regs[InternalRegs::FogColor]); + fogConfig.fogColorB = Helpers::getBits<16, 8>(regs[InternalRegs::FogColor]); } }; static_assert( std::has_unique_object_representations() && std::has_unique_object_representations() && - std::has_unique_object_representations() + std::has_unique_object_representations() && std::has_unique_object_representations() ); } // namespace PICA diff --git a/include/PICA/regs.hpp b/include/PICA/regs.hpp index c66c90ca..636e8f7c 100644 --- a/include/PICA/regs.hpp +++ b/include/PICA/regs.hpp @@ -396,6 +396,12 @@ namespace PICA { GreaterOrEqual = 7, }; + enum class FogMode : u32 { + Disabled = 0, + Fog = 5, + Gas = 7, + }; + struct TexEnvConfig { enum class Source : u8 { PrimaryColor = 0x0, diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index 085d990a..215e5adb 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -24,6 +24,8 @@ namespace PICA::ShaderGen { void compileLUTLookup(std::string& shader, const PICA::FragmentConfig& config, u32 lightIndex, u32 lutID); bool isSamplerEnabled(u32 environmentID, u32 lutID); + void compileFog(std::string& shader, const PICA::FragmentConfig& config); + public: FragmentGenerator(API api, Language language) : api(api), language(language) {} std::string generate(const PICA::FragmentConfig& config); diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 01210587..9802be90 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -194,6 +194,8 @@ std::string FragmentGenerator::generate(const FragmentConfig& config) { compileTEV(ret, i, config); } + compileFog(ret, config); + applyAlphaTest(ret, config); ret += "fragColor = combinerOutput;\n}"; // End of main function @@ -652,4 +654,27 @@ void FragmentGenerator::compileLUTLookup(std::string& shader, const PICA::Fragme shader += "lut_lookup_result *= " + std::to_string(scales[scale]) + ";\n"; } } +} + +void FragmentGenerator::compileFog(std::string& shader, const PICA::FragmentConfig& config) { + if (config.fogConfig.mode != FogMode::Fog) { + return; + } + + float r = config.fogConfig.fogColorR / 255.0f; + float g = config.fogConfig.fogColorG / 255.0f; + float b = config.fogConfig.fogColorB / 255.0f; + + if (config.fogConfig.flipDepth) { + shader += "float fog_index = (1.0 - depth) * 128.0;\n"; + } else { + shader += "float fog_index = depth * 128.0;\n"; + } + + shader += "float clamped_index = clamp(floor(fog_index), 0.0, 127.0);"; + shader += "float delta = fog_index - clamped_index;"; + shader += "vec3 fog_color = vec3(" + std::to_string(r) + ", " + std::to_string(g) + ", " + std::to_string(b) + ");"; + shader += "vec2 value = texelFetch(u_tex_luts, ivec2(int(clamped_index), 24), 0).rg;"; // fog LUT is past the light LUTs + shader += "float fog_factor = clamp(value.r + value.g * delta, 0.0, 1.0);"; + shader += "combinerOutput.rgb = mix(fog_color, combinerOutput.rgb, fog_factor);"; } \ No newline at end of file From 82df95cf88a066be296acd3e21f45e511c474f24 Mon Sep 17 00:00:00 2001 From: offtkp Date: Sun, 21 Jul 2024 17:40:43 +0300 Subject: [PATCH 122/251] Update gles.patch --- .github/gles.patch | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/gles.patch b/.github/gles.patch index 270e336e..5a922fcf 100644 --- a/.github/gles.patch +++ b/.github/gles.patch @@ -21,7 +21,7 @@ index 990e2f80..2e7842ac 100644 void main() { diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag -index 9f369e39..b4bb19d3 100644 +index b9f9fe4c..f1cf286f 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -1,4 +1,5 @@ @@ -31,8 +31,8 @@ index 9f369e39..b4bb19d3 100644 in vec4 v_quaternion; in vec4 v_colour; -@@ -164,11 +165,17 @@ float lutLookup(uint lut, int index) { - return texelFetch(u_tex_lighting_lut, ivec2(index, int(lut)), 0).r; +@@ -166,11 +167,17 @@ float lutLookup(uint lut, int index) { + return texelFetch(u_tex_luts, ivec2(index, int(lut)), 0).r; } +// some gles versions have bitfieldExtractCompat and complain if you redefine it, some don't and compile error, using this instead @@ -50,7 +50,7 @@ index 9f369e39..b4bb19d3 100644 } // Convert an arbitrary-width floating point literal to an f32 -@@ -208,16 +215,16 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light +@@ -210,16 +217,16 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light bool current_sampler_enabled = isSamplerEnabled(environment_id, lut_id); // 7 luts per environment @@ -70,7 +70,7 @@ index 9f369e39..b4bb19d3 100644 switch (input_id) { case 0u: { delta = dot(normal, normalize(half_vector)); -@@ -239,11 +246,11 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light +@@ -241,11 +248,11 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light int GPUREG_LIGHTi_SPOTDIR_LOW = int(readPicaReg(0x0146u + (light_id << 4u))); int GPUREG_LIGHTi_SPOTDIR_HIGH = int(readPicaReg(0x0147u + (light_id << 4u))); @@ -86,7 +86,7 @@ index 9f369e39..b4bb19d3 100644 if ((se_x & 0x1000) == 0x1000) se_x |= 0xffffe000; if ((se_y & 0x1000) == 0x1000) se_y |= 0xffffe000; -@@ -270,9 +277,9 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light +@@ -272,9 +279,9 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light } // 0 = enabled @@ -98,7 +98,7 @@ index 9f369e39..b4bb19d3 100644 delta = max(delta, 0.0); } else { delta = abs(delta); -@@ -296,7 +303,7 @@ vec3 rotateVec3ByQuaternion(vec3 v, vec4 q) { +@@ -298,7 +305,7 @@ vec3 rotateVec3ByQuaternion(vec3 v, vec4 q) { // Implements the following algorthm: https://mathb.in/26766 void calcLighting(out vec4 primary_color, out vec4 secondary_color) { uint GPUREG_LIGHTING_ENABLE = readPicaReg(0x008Fu); @@ -107,7 +107,7 @@ index 9f369e39..b4bb19d3 100644 primary_color = secondary_color = vec4(0.0); return; } -@@ -313,7 +320,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -315,7 +322,7 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { GPUREG_LIGHTING_LUTINPUT_ABS = readPicaReg(0x01D0u); GPUREG_LIGHTING_LUTINPUT_SELECT = readPicaReg(0x01D1u); @@ -116,7 +116,7 @@ index 9f369e39..b4bb19d3 100644 // Bump mode is ignored for now because it breaks some games ie. Toad Treasure Tracker switch (bump_mode) { -@@ -326,15 +333,15 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -328,15 +335,15 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { vec4 diffuse_sum = vec4(0.0, 0.0, 0.0, 1.0); vec4 specular_sum = vec4(0.0, 0.0, 0.0, 1.0); @@ -135,7 +135,7 @@ index 9f369e39..b4bb19d3 100644 uint GPUREG_LIGHTi_SPECULAR0 = readPicaReg(0x0140u + (light_id << 4u)); uint GPUREG_LIGHTi_SPECULAR1 = readPicaReg(0x0141u + (light_id << 4u)); -@@ -346,12 +353,12 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -348,12 +355,12 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { float light_distance; vec3 light_position = vec3( @@ -151,7 +151,7 @@ index 9f369e39..b4bb19d3 100644 light_vector = light_position + v_view; } -@@ -367,23 +374,23 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -369,23 +376,23 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { float NdotL = dot(normal, light_vector); // N dot Li // Two sided diffuse @@ -181,7 +181,7 @@ index 9f369e39..b4bb19d3 100644 float distance_attenuation_bias = decodeFP(GPUREG_LIGHTi_ATTENUATION_BIAS, 7u, 12u); float distance_attenuation_scale = decodeFP(GPUREG_LIGHTi_ATTENUATION_SCALE, 7u, 12u); -@@ -428,8 +435,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { +@@ -430,8 +437,8 @@ void calcLighting(out vec4 primary_color, out vec4 secondary_color) { specular_sum.rgb += light_factor * clamp_factor * (specular0 + specular1); } From 8fc61cdb7b9f73e56470265a7ed8e6a477e6f04a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 21 Jul 2024 17:52:06 +0300 Subject: [PATCH 123/251] Add shader decompiler files --- CMakeLists.txt | 3 ++- include/PICA/shader_decompiler.hpp | 9 +++++++++ src/core/PICA/shader_decompiler.cpp | 1 + 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 include/PICA/shader_decompiler.hpp create mode 100644 src/core/PICA/shader_decompiler.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index fdfe8a4a..9d7be502 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -199,6 +199,7 @@ set(PICA_SOURCE_FILES src/core/PICA/gpu.cpp src/core/PICA/regs.cpp src/core/PICA src/core/PICA/shader_interpreter.cpp src/core/PICA/dynapica/shader_rec.cpp src/core/PICA/dynapica/shader_rec_emitter_x64.cpp src/core/PICA/pica_hash.cpp src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp src/core/PICA/shader_gen_glsl.cpp + src/core/PICA/shader_decompiler.cpp ) set(LOADER_SOURCE_FILES src/core/loader/elf.cpp src/core/loader/ncsd.cpp src/core/loader/ncch.cpp src/core/loader/3dsx.cpp src/core/loader/lz77.cpp) @@ -249,7 +250,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/audio/dsp_core.hpp include/audio/null_core.hpp include/audio/teakra_core.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp include/PICA/pica_frag_config.hpp - include/PICA/pica_frag_uniforms.hpp include/PICA/shader_gen_types.hpp + include/PICA/pica_frag_uniforms.hpp include/PICA/shader_gen_types.hpp include/PICA/shader_decompiler.hpp ) cmrc_add_resource_library( diff --git a/include/PICA/shader_decompiler.hpp b/include/PICA/shader_decompiler.hpp new file mode 100644 index 00000000..18c950e1 --- /dev/null +++ b/include/PICA/shader_decompiler.hpp @@ -0,0 +1,9 @@ +#pragma once +#include + +#include "PICA/shader.hpp" +#include "PICA/shader_gen_types.hpp" + +namespace PICA::ShaderGen { + std::string decompileShader(PICAShader& shaderUnit); +} \ No newline at end of file diff --git a/src/core/PICA/shader_decompiler.cpp b/src/core/PICA/shader_decompiler.cpp new file mode 100644 index 00000000..b4f8f155 --- /dev/null +++ b/src/core/PICA/shader_decompiler.cpp @@ -0,0 +1 @@ +#include "PICA/shader_decompiler.hpp" \ No newline at end of file From b8712b37c3df9f270942fde07e2cf3a3d8df3855 Mon Sep 17 00:00:00 2001 From: offtkp Date: Sun, 21 Jul 2024 18:25:51 +0300 Subject: [PATCH 124/251] A few kissable changes --- include/PICA/gpu.hpp | 2 +- include/PICA/pica_frag_config.hpp | 11 +++++++---- src/core/PICA/regs.cpp | 4 ++-- src/core/renderer_gl/renderer_gl.cpp | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/include/PICA/gpu.hpp b/include/PICA/gpu.hpp index 1e37729b..c4c8db5c 100644 --- a/include/PICA/gpu.hpp +++ b/include/PICA/gpu.hpp @@ -92,8 +92,8 @@ class GPU { // Set to false by the renderer when the lighting_lut is uploaded ot the GPU bool lightingLUTDirty = false; - std::array fogLUT; bool fogLUTDirty = false; + std::array fogLUT; GPU(Memory& mem, EmulatorConfig& config); void display() { renderer->display(); } diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index 32fa7aa6..337fd211 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -235,10 +235,13 @@ namespace PICA { #undef setupTevStage fogConfig.mode = (FogMode)Helpers::getBits<0, 3>(regs[InternalRegs::TexEnvUpdateBuffer]); - fogConfig.flipDepth = Helpers::getBit<16>(regs[InternalRegs::TexEnvUpdateBuffer]); - fogConfig.fogColorR = Helpers::getBits<0, 8>(regs[InternalRegs::FogColor]); - fogConfig.fogColorG = Helpers::getBits<8, 8>(regs[InternalRegs::FogColor]); - fogConfig.fogColorB = Helpers::getBits<16, 8>(regs[InternalRegs::FogColor]); + + if (fogConfig.mode == FogMode::Fog) { + fogConfig.flipDepth = Helpers::getBit<16>(regs[InternalRegs::TexEnvUpdateBuffer]); + fogConfig.fogColorR = Helpers::getBits<0, 8>(regs[InternalRegs::FogColor]); + fogConfig.fogColorG = Helpers::getBits<8, 8>(regs[InternalRegs::FogColor]); + fogConfig.fogColorB = Helpers::getBits<16, 8>(regs[InternalRegs::FogColor]); + } } }; diff --git a/src/core/PICA/regs.cpp b/src/core/PICA/regs.cpp index 45e624ec..99519272 100644 --- a/src/core/PICA/regs.cpp +++ b/src/core/PICA/regs.cpp @@ -143,10 +143,10 @@ void GPU::writeInternalReg(u32 index, u32 value, u32 mask) { case FogLUTData5: case FogLUTData6: case FogLUTData7: { - const uint32_t index = regs[FogLUTIndex] & 127; + const uint32_t index = regs[FogLUTIndex] & 0x7F; fogLUT[index] = value; fogLUTDirty = true; - regs[FogLUTIndex] = (index + 1) & 127; + regs[FogLUTIndex] = (index + 1) & 0x7F; break; } diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index b6c90374..5e1462b9 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -1026,7 +1026,7 @@ void RendererGL::initUbershader(OpenGL::Program& program) { ubershaderData.depthmapEnableLoc = OpenGL::uniformLocation(program, "u_depthmapEnable"); ubershaderData.picaRegLoc = OpenGL::uniformLocation(program, "u_picaRegs"); - // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, light maps go in TU 3, and the fog map goes in TU 4 + // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2 and the LUTs go in TU 3 glUniform1i(OpenGL::uniformLocation(program, "u_tex0"), 0); glUniform1i(OpenGL::uniformLocation(program, "u_tex1"), 1); glUniform1i(OpenGL::uniformLocation(program, "u_tex2"), 2); From 906abe0fb322e3692520d741e7a721c42a0157ff Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 21 Jul 2024 18:29:39 +0300 Subject: [PATCH 125/251] Add -Wno-interference-size flag for GNUC --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fdfe8a4a..1264ce89 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ if(APPLE) endif() if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format-nonliteral -Wno-format-security") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format-nonliteral -Wno-format-security -Wno-interference-size") endif() option(DISABLE_PANIC_DEV "Make a build with fewer and less intrusive asserts" ON) From 04d6c52784894f994da84b0c66892d0eae7629e3 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 21 Jul 2024 15:34:31 +0000 Subject: [PATCH 126/251] NCCH: Remove unused saveData member --- include/loader/ncch.hpp | 2 -- src/core/loader/ncch.cpp | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/include/loader/ncch.hpp b/include/loader/ncch.hpp index 8e35643b..4aa2ede7 100644 --- a/include/loader/ncch.hpp +++ b/include/loader/ncch.hpp @@ -64,8 +64,6 @@ struct NCCH { // Contents of the .code file in the ExeFS std::vector codeFile; - // Contains of the cart's save data - std::vector saveData; // The cart region. Only the CXI's region matters to us. Necessary to get past region locking std::optional region = std::nullopt; std::vector smdh; diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 98574289..4be05549 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -155,8 +155,7 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn } } - const u64 saveDataSize = *(u64*)&exheader[0x1C0 + 0x0]; // Size of save data in bytes - saveData.resize(saveDataSize, 0xff); + [[maybe_unused]] const u64 saveDataSize = *(u64*)&exheader[0x1C0 + 0x0]; // Size of save data in bytes compressCode = (exheader[0xD] & 1) != 0; stackSize = *(u32*)&exheader[0x1C]; From 0a0f623c7c9ee44cccde032f8c99f1a6650264cf Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 21 Jul 2024 15:46:38 +0000 Subject: [PATCH 127/251] NCCH: Fix saveDataSize (Oops) --- include/loader/ncch.hpp | 3 ++- src/core/loader/ncch.cpp | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/include/loader/ncch.hpp b/include/loader/ncch.hpp index 4aa2ede7..92ad5040 100644 --- a/include/loader/ncch.hpp +++ b/include/loader/ncch.hpp @@ -50,6 +50,7 @@ struct NCCH { static constexpr u64 mediaUnit = 0x200; u64 size = 0; // Size of NCCH converted to bytes + u64 saveDataSize = 0; u32 stackSize = 0; u32 bssSize = 0; u32 exheaderSize = 0; @@ -76,7 +77,7 @@ struct NCCH { bool hasExeFS() { return exeFS.size != 0; } bool hasRomFS() { return romFS.size != 0; } bool hasCode() { return codeFile.size() != 0; } - bool hasSaveData() { return saveData.size() != 0; } + bool hasSaveData() { return saveDataSize != 0; } // Parse SMDH for region info and such. Returns false on failure, true on success bool parseSMDH(const std::vector &smdh); diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 4be05549..e363213c 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -155,7 +155,7 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn } } - [[maybe_unused]] const u64 saveDataSize = *(u64*)&exheader[0x1C0 + 0x0]; // Size of save data in bytes + saveDataSize = *(u64*)&exheader[0x1C0 + 0x0]; // Size of save data in bytes compressCode = (exheader[0xD] & 1) != 0; stackSize = *(u32*)&exheader[0x1C]; From 3d9a1a8b5d7a661f03d94a63d809b2c746e63228 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 21 Jul 2024 19:07:28 +0300 Subject: [PATCH 128/251] I should really squash this when I'm home --- src/core/loader/ncch.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index e363213c..a575d4f2 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -25,7 +25,6 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn } codeFile.clear(); - saveData.clear(); smdh.clear(); partitionInfo = info; From 9bd711958b8df7db2965b59cb97462ba61d8e054 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 21 Jul 2024 18:29:39 +0300 Subject: [PATCH 129/251] Add -Wno-interference-size flag for GNUC --- CMakeLists.txt | 4 ++++ include/loader/ncch.hpp | 5 ++--- src/core/loader/ncch.cpp | 4 +--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fdfe8a4a..df0e2bb8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -28,6 +28,10 @@ if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format-nonliteral -Wno-format-security") endif() +if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-interference-size") +endif() + option(DISABLE_PANIC_DEV "Make a build with fewer and less intrusive asserts" ON) option(GPU_DEBUG_INFO "Enable additional GPU debugging info" OFF) option(ENABLE_OPENGL "Enable OpenGL rendering backend" ON) diff --git a/include/loader/ncch.hpp b/include/loader/ncch.hpp index 8e35643b..92ad5040 100644 --- a/include/loader/ncch.hpp +++ b/include/loader/ncch.hpp @@ -50,6 +50,7 @@ struct NCCH { static constexpr u64 mediaUnit = 0x200; u64 size = 0; // Size of NCCH converted to bytes + u64 saveDataSize = 0; u32 stackSize = 0; u32 bssSize = 0; u32 exheaderSize = 0; @@ -64,8 +65,6 @@ struct NCCH { // Contents of the .code file in the ExeFS std::vector codeFile; - // Contains of the cart's save data - std::vector saveData; // The cart region. Only the CXI's region matters to us. Necessary to get past region locking std::optional region = std::nullopt; std::vector smdh; @@ -78,7 +77,7 @@ struct NCCH { bool hasExeFS() { return exeFS.size != 0; } bool hasRomFS() { return romFS.size != 0; } bool hasCode() { return codeFile.size() != 0; } - bool hasSaveData() { return saveData.size() != 0; } + bool hasSaveData() { return saveDataSize != 0; } // Parse SMDH for region info and such. Returns false on failure, true on success bool parseSMDH(const std::vector &smdh); diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 98574289..a575d4f2 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -25,7 +25,6 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn } codeFile.clear(); - saveData.clear(); smdh.clear(); partitionInfo = info; @@ -155,8 +154,7 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn } } - const u64 saveDataSize = *(u64*)&exheader[0x1C0 + 0x0]; // Size of save data in bytes - saveData.resize(saveDataSize, 0xff); + saveDataSize = *(u64*)&exheader[0x1C0 + 0x0]; // Size of save data in bytes compressCode = (exheader[0xD] & 1) != 0; stackSize = *(u32*)&exheader[0x1C]; From a8c68baa6f20628968c55d2443ad158d49bb9191 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 21 Jul 2024 21:49:45 +0300 Subject: [PATCH 130/251] Don't use -Wno-interference-size on Clang --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 54c64fa3..df0e2bb8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -25,7 +25,7 @@ if(APPLE) endif() if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format-nonliteral -Wno-format-security -Wno-interference-size") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format-nonliteral -Wno-format-security") endif() if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") From 6399cb55e222a767470cc64a1e7c334b84159a76 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 21 Jul 2024 23:04:44 +0300 Subject: [PATCH 131/251] GL: Remove duplicate scissor disable --- src/core/renderer_gl/renderer_gl.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 5e1462b9..f26158ae 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -531,7 +531,6 @@ void RendererGL::display() { gl.disableScissor(); gl.disableBlend(); gl.disableDepth(); - gl.disableScissor(); // This will work fine whether or not logic ops are enabled. We set logic op to copy instead of disabling to avoid state changes gl.setLogicOp(GL_COPY); gl.setColourMask(true, true, true, true); From 2d72b660423ddac57c56e5cbefe0ecec2f5d7d5f Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 22 Jul 2024 01:47:34 +0300 Subject: [PATCH 132/251] Initial shader decompilation work --- include/PICA/shader_decompiler.hpp | 85 ++++++++++++++++++++++++++++- src/core/PICA/shader_decompiler.cpp | 54 +++++++++++++++++- 2 files changed, 136 insertions(+), 3 deletions(-) diff --git a/include/PICA/shader_decompiler.hpp b/include/PICA/shader_decompiler.hpp index 18c950e1..abfa910c 100644 --- a/include/PICA/shader_decompiler.hpp +++ b/include/PICA/shader_decompiler.hpp @@ -1,9 +1,90 @@ #pragma once +#include #include +#include +#include #include "PICA/shader.hpp" #include "PICA/shader_gen_types.hpp" +struct EmulatorConfig; + namespace PICA::ShaderGen { - std::string decompileShader(PICAShader& shaderUnit); -} \ No newline at end of file + // Control flow analysis is partially based on + // https://github.com/PabloMK7/citra/blob/d0179559466ff09731d74474322ee880fbb44b00/src/video_core/shader/generator/glsl_shader_decompiler.cpp#L33 + struct ControlFlow { + struct Function { + using Labels = std::set; + + enum class ExitMode { + Unknown, // Can't guarantee whether we'll exit properly, fall back to CPU shaders (can happen with jmp shenanigans) + AlwaysReturn, // All paths reach the return point. + Conditional, // One or more code paths reach the return point or an END instruction conditionally. + AlwaysEnd, // All paths reach an END instruction. + }; + + u32 start; // Starting PC of the function + u32 end; // End PC of the function + Labels outLabels{}; // Labels this function can "goto" (jump) to + ExitMode exitMode = ExitMode::Unknown; + + explicit Function(u32 start, u32 end) : start(start), end(end) {} + // Use lexicographic comparison for functions in order to sort them in a set + bool operator<(const Function& other) const { return std::tie(start, end) < std::tie(other.start, other.end); } + }; + + std::set functions{}; + + // Tells us whether analysis of the shader we're trying to compile failed, in which case we'll need to fail back to shader emulation + // On the CPU + bool analysisFailed = false; + + void analyze(const PICAShader& shader, u32 entrypoint); + + // This will recursively add all functions called by the function too, as analyzeFunction will call addFunction on control flow instructions + const Function* addFunction(u32 start, u32 end) { + auto searchIterator = functions.find(Function(start, end)); + if (searchIterator != functions.end()) { + return &(*searchIterator); + } + + // Add this function and analyze it if it doesn't already exist + Function function(start, end); + function.exitMode = analyzeFunction(start, end, function.outLabels); + + // This function + if (function.exitMode == Function::ExitMode::Unknown) { + analysisFailed = true; + return nullptr; + } + + // Add function to our function list + auto [it, added] = functions.insert(std::move(function)); + return &(*it); + } + + Function::ExitMode analyzeFunction(u32 start, u32 end, Function::Labels& labels); + }; + + class ShaderDecompiler { + ControlFlow controlFlow{}; + + PICAShader& shader; + EmulatorConfig& config; + std::string decompiledShader; + + u32 entrypoint; + u32 currentPC; + + API api; + Language language; + + public: + ShaderDecompiler(PICAShader& shader, EmulatorConfig& config, u32 entrypoint, API api, Language language) + : shader(shader), entrypoint(entrypoint), currentPC(entrypoint), config(config), api(api), language(language), decompiledShader("") {} + + std::string decompile(); + }; + + std::string decompileShader(PICAShader& shader, EmulatorConfig& config, u32 entrypoint, API api, Language language); +} // namespace PICA::ShaderGen \ No newline at end of file diff --git a/src/core/PICA/shader_decompiler.cpp b/src/core/PICA/shader_decompiler.cpp index b4f8f155..4dccfa7d 100644 --- a/src/core/PICA/shader_decompiler.cpp +++ b/src/core/PICA/shader_decompiler.cpp @@ -1 +1,53 @@ -#include "PICA/shader_decompiler.hpp" \ No newline at end of file +#include "PICA/shader_decompiler.hpp" + +#include "config.hpp" + +using namespace PICA; +using namespace PICA::ShaderGen; +using Function = ControlFlow::Function; +using ExitMode = Function::ExitMode; + +void ControlFlow::analyze(const PICAShader& shader, u32 entrypoint) { + analysisFailed = false; + + const Function* function = addFunction(entrypoint, PICAShader::maxInstructionCount); + if (function == nullptr) { + analysisFailed = true; + } +} + +ExitMode analyzeFunction(u32 start, u32 end, Function::Labels& labels) { return ExitMode::Unknown; } + +std::string ShaderDecompiler::decompile() { + controlFlow.analyze(shader, entrypoint); + + if (controlFlow.analysisFailed) { + return ""; + } + + decompiledShader = ""; + + switch (api) { + case API::GL: decompiledShader += "#version 410 core"; break; + case API::GLES: decompiledShader += "#version 300 es"; break; + default: break; + } + + if (config.accurateShaderMul) { + // Safe multiplication handler from Citra: Handles the PICA's 0 * inf = 0 edge case + decompiledShader += R"( + vec4 safe_mul(vec4 a, vec4 b) { + vec4 res = a * b; + return mix(res, mix(mix(vec4(0.0), res, isnan(rhs)), product, isnan(lhs)), isnan(res)); + } + )"; + } + + return decompiledShader; +} + +std::string PICA::ShaderGen::decompileShader(PICAShader& shader, EmulatorConfig& config, u32 entrypoint, API api, Language language) { + ShaderDecompiler decompiler(shader, config, entrypoint, api, language); + + return decompiler.decompile(); +} \ No newline at end of file From 85af58f0a72df67237ac234fc476092edd3c5f5b Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 22 Jul 2024 02:06:24 +0300 Subject: [PATCH 133/251] Remove shader-related hallucinations --- include/PICA/shader.hpp | 6 +----- src/core/PICA/regs.cpp | 2 ++ src/core/PICA/shader_unit.cpp | 1 - 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/include/PICA/shader.hpp b/include/PICA/shader.hpp index 10f6ec88..cc055257 100644 --- a/include/PICA/shader.hpp +++ b/include/PICA/shader.hpp @@ -220,13 +220,9 @@ class PICAShader { public: static constexpr size_t maxInstructionCount = 4096; std::array loadedShader; // Currently loaded & active shader - std::array bufferedShader; // Shader to be transferred when the SH_CODETRANSFER_END reg gets written to PICAShader(ShaderType type) : type(type) {} - // Theese functions are in the header to be inlined more easily, though with LTO I hope I'll be able to move them - void finalize() { std::memcpy(&loadedShader[0], &bufferedShader[0], 4096 * sizeof(u32)); } - void setBufferIndex(u32 index) { bufferIndex = index & 0xfff; } void setOpDescriptorIndex(u32 index) { opDescriptorIndex = index & 0x7f; } @@ -235,7 +231,7 @@ class PICAShader { Helpers::panic("o no, shader upload overflew"); } - bufferedShader[bufferIndex++] = word; + loadedShader[bufferIndex++] = word; bufferIndex &= 0xfff; codeHashDirty = true; // Signal the JIT if necessary that the program hash has potentially changed diff --git a/src/core/PICA/regs.cpp b/src/core/PICA/regs.cpp index 99519272..f805de60 100644 --- a/src/core/PICA/regs.cpp +++ b/src/core/PICA/regs.cpp @@ -329,9 +329,11 @@ void GPU::writeInternalReg(u32 index, u32 value, u32 mask) { break; } + /* TODO: Find out if this actually does anything case VertexShaderTransferEnd: if (value != 0) shaderUnit.vs.finalize(); break; + */ case VertexShaderTransferIndex: shaderUnit.vs.setBufferIndex(value); break; diff --git a/src/core/PICA/shader_unit.cpp b/src/core/PICA/shader_unit.cpp index aa7b4c12..759849a8 100644 --- a/src/core/PICA/shader_unit.cpp +++ b/src/core/PICA/shader_unit.cpp @@ -9,7 +9,6 @@ void ShaderUnit::reset() { void PICAShader::reset() { loadedShader.fill(0); - bufferedShader.fill(0); operandDescriptors.fill(0); boolUniform = 0; From 0aa1ed21b2a1cf4e1fc0bd3e801bd4878d56fd4d Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 23 Jul 2024 01:22:26 +0300 Subject: [PATCH 134/251] More shader decompiler work --- include/PICA/shader.hpp | 16 +++- include/PICA/shader_decompiler.hpp | 42 ++++++++--- src/core/PICA/shader_decompiler.cpp | 110 ++++++++++++++++++++++++++-- 3 files changed, 150 insertions(+), 18 deletions(-) diff --git a/include/PICA/shader.hpp b/include/PICA/shader.hpp index cc055257..938a5408 100644 --- a/include/PICA/shader.hpp +++ b/include/PICA/shader.hpp @@ -1,6 +1,8 @@ #pragma once #include #include +#include +#include #include #include "PICA/float_types.hpp" @@ -90,9 +92,12 @@ class PICAShader { public: // These are placed close to the temp registers and co because it helps the JIT generate better code u32 entrypoint = 0; // Initial shader PC - u32 boolUniform; - std::array, 4> intUniforms; + + // We want these registers in this order & with this alignment for uploading them directly to a UBO + // When emulating shaders on the GPU alignas(16) std::array floatUniforms; + alignas(16) std::array, 4> intUniforms; + u32 boolUniform; alignas(16) std::array fixedAttributes; // Fixed vertex attributes alignas(16) std::array inputs; // Attributes passed to the shader @@ -291,4 +296,9 @@ class PICAShader { Hash getCodeHash(); Hash getOpdescHash(); -}; \ No newline at end of file +}; + +static_assert( + offsetof(PICAShader, intUniforms) == offsetof(PICAShader, floatUniforms) + 96 * sizeof(float) * 4 && + offsetof(PICAShader, boolUniform) == offsetof(PICAShader, intUniforms) + 4 * sizeof(u8) * 4 +); \ No newline at end of file diff --git a/include/PICA/shader_decompiler.hpp b/include/PICA/shader_decompiler.hpp index abfa910c..cbc569ae 100644 --- a/include/PICA/shader_decompiler.hpp +++ b/include/PICA/shader_decompiler.hpp @@ -2,6 +2,7 @@ #include #include #include +#include #include #include "PICA/shader.hpp" @@ -13,6 +14,15 @@ namespace PICA::ShaderGen { // Control flow analysis is partially based on // https://github.com/PabloMK7/citra/blob/d0179559466ff09731d74474322ee880fbb44b00/src/video_core/shader/generator/glsl_shader_decompiler.cpp#L33 struct ControlFlow { + // A continuous range of addresses + struct AddressRange { + u32 start, end; + AddressRange(u32 start, u32 end) : start(start), end(end) {} + + // Use lexicographic comparison for functions in order to sort them in a set + bool operator<(const AddressRange& other) const { return std::tie(start, end) < std::tie(other.start, other.end); } + }; + struct Function { using Labels = std::set; @@ -29,20 +39,22 @@ namespace PICA::ShaderGen { ExitMode exitMode = ExitMode::Unknown; explicit Function(u32 start, u32 end) : start(start), end(end) {} - // Use lexicographic comparison for functions in order to sort them in a set - bool operator<(const Function& other) const { return std::tie(start, end) < std::tie(other.start, other.end); } + bool operator<(const Function& other) const { return AddressRange(start, end) < AddressRange(other.start, other.end); } + + std::string getIdentifier() const { return "func_" + std::to_string(start) + "_to_" + std::to_string(end); } + std::string getForwardDecl() const { return "void " + getIdentifier() + "();\n"; } + std::string getCallStatement() const { return getIdentifier() + "()"; } }; std::set functions{}; + std::map exitMap{}; // Tells us whether analysis of the shader we're trying to compile failed, in which case we'll need to fail back to shader emulation // On the CPU bool analysisFailed = false; - void analyze(const PICAShader& shader, u32 entrypoint); - // This will recursively add all functions called by the function too, as analyzeFunction will call addFunction on control flow instructions - const Function* addFunction(u32 start, u32 end) { + const Function* addFunction(const PICAShader& shader, u32 start, u32 end) { auto searchIterator = functions.find(Function(start, end)); if (searchIterator != functions.end()) { return &(*searchIterator); @@ -50,9 +62,9 @@ namespace PICA::ShaderGen { // Add this function and analyze it if it doesn't already exist Function function(start, end); - function.exitMode = analyzeFunction(start, end, function.outLabels); + function.exitMode = analyzeFunction(shader, start, end, function.outLabels); - // This function + // This function could not be fully analyzed, report failure if (function.exitMode == Function::ExitMode::Unknown) { analysisFailed = true; return nullptr; @@ -63,10 +75,14 @@ namespace PICA::ShaderGen { return &(*it); } - Function::ExitMode analyzeFunction(u32 start, u32 end, Function::Labels& labels); + void analyze(const PICAShader& shader, u32 entrypoint); + Function::ExitMode analyzeFunction(const PICAShader& shader, u32 start, u32 end, Function::Labels& labels); }; class ShaderDecompiler { + using AddressRange = ControlFlow::AddressRange; + using Function = ControlFlow::Function; + ControlFlow controlFlow{}; PICAShader& shader; @@ -74,14 +90,20 @@ namespace PICA::ShaderGen { std::string decompiledShader; u32 entrypoint; - u32 currentPC; API api; Language language; + void compileInstruction(u32& pc, bool& finished); + void compileRange(const AddressRange& range); + void callFunction(const Function& function); + const Function* findFunction(const AddressRange& range); + + void writeAttributes(); + public: ShaderDecompiler(PICAShader& shader, EmulatorConfig& config, u32 entrypoint, API api, Language language) - : shader(shader), entrypoint(entrypoint), currentPC(entrypoint), config(config), api(api), language(language), decompiledShader("") {} + : shader(shader), entrypoint(entrypoint), config(config), api(api), language(language), decompiledShader("") {} std::string decompile(); }; diff --git a/src/core/PICA/shader_decompiler.cpp b/src/core/PICA/shader_decompiler.cpp index 4dccfa7d..91b07574 100644 --- a/src/core/PICA/shader_decompiler.cpp +++ b/src/core/PICA/shader_decompiler.cpp @@ -10,13 +10,75 @@ using ExitMode = Function::ExitMode; void ControlFlow::analyze(const PICAShader& shader, u32 entrypoint) { analysisFailed = false; - const Function* function = addFunction(entrypoint, PICAShader::maxInstructionCount); + const Function* function = addFunction(shader, entrypoint, PICAShader::maxInstructionCount); if (function == nullptr) { analysisFailed = true; } } -ExitMode analyzeFunction(u32 start, u32 end, Function::Labels& labels) { return ExitMode::Unknown; } +ExitMode ControlFlow::analyzeFunction(const PICAShader& shader, u32 start, u32 end, Function::Labels& labels) { + // Initialize exit mode to unknown by default, in order to detect things like unending loops + auto [it, inserted] = exitMap.emplace(AddressRange(start, end), ExitMode::Unknown); + // Function has already been analyzed and is in the map so it wasn't added, don't analyze again + if (!inserted) { + return it->second; + } + + // Make sure not to go out of bounds on the shader + for (u32 pc = start; pc < PICAShader::maxInstructionCount && pc != end; pc++) { + const u32 instruction = shader.loadedShader[pc]; + const u32 opcode = instruction >> 26; + + switch (opcode) { + case ShaderOpcodes::JMPC: Helpers::panic("Unimplemented control flow operation (JMPC)"); + case ShaderOpcodes::JMPU: Helpers::panic("Unimplemented control flow operation (JMPU)"); + case ShaderOpcodes::IFU: Helpers::panic("Unimplemented control flow operation (IFU)"); + case ShaderOpcodes::IFC: Helpers::panic("Unimplemented control flow operation (IFC)"); + case ShaderOpcodes::CALL: Helpers::panic("Unimplemented control flow operation (CALL)"); + case ShaderOpcodes::CALLC: Helpers::panic("Unimplemented control flow operation (CALLC)"); + case ShaderOpcodes::CALLU: Helpers::panic("Unimplemented control flow operation (CALLU)"); + case ShaderOpcodes::LOOP: Helpers::panic("Unimplemented control flow operation (LOOP)"); + case ShaderOpcodes::END: it->second = ExitMode::AlwaysEnd; return it->second; + + default: break; + } + } + + // A function without control flow instructions will always reach its "return point" and return + return ExitMode::AlwaysReturn; +} + +void ShaderDecompiler::compileRange(const AddressRange& range) { + u32 pc = range.start; + const u32 end = range.end >= range.start ? range.end : PICAShader::maxInstructionCount; + bool finished = false; + + while (pc < end && !finished) { + compileInstruction(pc, finished); + } +} + +const Function* ShaderDecompiler::findFunction(const AddressRange& range) { + for (const Function& func : controlFlow.functions) { + if (range.start == func.start && range.end == func.end) { + return &func; + } + } + + return nullptr; +} + +void ShaderDecompiler::writeAttributes() { + decompiledShader += R"( + layout(std140) uniform PICAShaderUniforms { + vec4 uniform_float[96]; + uvec4 uniform_int; + uint uniform_bool; + }; +)"; + + decompiledShader += "\n"; +} std::string ShaderDecompiler::decompile() { controlFlow.analyze(shader, entrypoint); @@ -28,11 +90,13 @@ std::string ShaderDecompiler::decompile() { decompiledShader = ""; switch (api) { - case API::GL: decompiledShader += "#version 410 core"; break; - case API::GLES: decompiledShader += "#version 300 es"; break; + case API::GL: decompiledShader += "#version 410 core\n"; break; + case API::GLES: decompiledShader += "#version 300 es\n"; break; default: break; } + writeAttributes(); + if (config.accurateShaderMul) { // Safe multiplication handler from Citra: Handles the PICA's 0 * inf = 0 edge case decompiledShader += R"( @@ -43,10 +107,46 @@ std::string ShaderDecompiler::decompile() { )"; } + // Forward declare every generated function first so that we can easily call anything from anywhere. + for (auto& func : controlFlow.functions) { + decompiledShader += func.getForwardDecl(); + } + + decompiledShader += "void pica_shader_main() {\n"; + AddressRange mainFunctionRange(entrypoint, PICAShader::maxInstructionCount); + callFunction(*findFunction(mainFunctionRange)); + decompiledShader += "}\n"; + + for (auto& func : controlFlow.functions) { + if (func.outLabels.size() > 0) { + Helpers::panic("Function with out labels"); + } + + decompiledShader += "void " + func.getIdentifier() + "() {\n"; + compileRange(AddressRange(func.start, func.end)); + decompiledShader += "}\n"; + } + return decompiledShader; } -std::string PICA::ShaderGen::decompileShader(PICAShader& shader, EmulatorConfig& config, u32 entrypoint, API api, Language language) { +void ShaderDecompiler::compileInstruction(u32& pc, bool& finished) { + const u32 instruction = shader.loadedShader[pc]; + const u32 opcode = instruction >> 26; + + switch (opcode) { + case ShaderOpcodes::DP4: decompiledShader += "dp4\n"; break; + case ShaderOpcodes::MOV: decompiledShader += "mov\n"; break; + case ShaderOpcodes::END: finished = true; return; + default: Helpers::warn("GLSL recompiler: Unknown opcode: %X", opcode); break; + } + + pc++; +} + +void ShaderDecompiler::callFunction(const Function& function) { decompiledShader += function.getCallStatement() + ";\n"; } + +std::string ShaderGen::decompileShader(PICAShader& shader, EmulatorConfig& config, u32 entrypoint, API api, Language language) { ShaderDecompiler decompiler(shader, config, entrypoint, api, language); return decompiler.decompile(); From 850aadb0f6d9b2130733e1d19ec0f69a7cb86ba6 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 23 Jul 2024 02:25:40 +0300 Subject: [PATCH 135/251] Update Linux version on CI --- .github/workflows/Linux_AppImage_Build.yml | 12 ++++++------ .github/workflows/Qt_Build.yml | 12 ++++++------ include/PICA/shader.hpp | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/Linux_AppImage_Build.yml b/.github/workflows/Linux_AppImage_Build.yml index 507187a3..7d198b9c 100644 --- a/.github/workflows/Linux_AppImage_Build.yml +++ b/.github/workflows/Linux_AppImage_Build.yml @@ -16,7 +16,7 @@ jobs: # well on Windows or Mac. You can convert this to a matrix build if you need # cross-platform coverage. # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -33,11 +33,11 @@ jobs: sudo ./llvm.sh 17 - name: Setup Vulkan SDK - run: | - wget -qO - http://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add - - sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-focal.list http://packages.lunarg.com/vulkan/lunarg-vulkan-focal.list - sudo apt update - sudo apt install vulkan-sdk + uses: humbletim/setup-vulkan-sdk@v1.2.0 + with: + vulkan-query-version: latest + vulkan-use-cache: true + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang - name: Configure CMake # Configure CMake in a 'build' subdirectory. `CMAKE_BUILD_TYPE` is only required if you are using a single-configuration generator such as make. diff --git a/.github/workflows/Qt_Build.yml b/.github/workflows/Qt_Build.yml index 5e622c54..1f9db49e 100644 --- a/.github/workflows/Qt_Build.yml +++ b/.github/workflows/Qt_Build.yml @@ -96,7 +96,7 @@ jobs: path: 'Alber.zip' Linux: - runs-on: ubuntu-20.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 @@ -117,11 +117,11 @@ jobs: sudo ./llvm.sh 17 - name: Setup Vulkan SDK - run: | - wget -qO - http://packages.lunarg.com/lunarg-signing-key-pub.asc | sudo apt-key add - - sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-focal.list http://packages.lunarg.com/vulkan/lunarg-vulkan-focal.list - sudo apt update - sudo apt install vulkan-sdk + uses: humbletim/setup-vulkan-sdk@v1.2.0 + with: + vulkan-query-version: latest + vulkan-use-cache: true + vulkan-components: Vulkan-Headers, Vulkan-Loader, SPIRV-Tools, Glslang - name: Configure CMake run: cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCMAKE_C_COMPILER=clang-17 -DCMAKE_CXX_COMPILER=clang++-17 -DENABLE_USER_BUILD=ON -DENABLE_QT_GUI=ON diff --git a/include/PICA/shader.hpp b/include/PICA/shader.hpp index 938a5408..44ca2a15 100644 --- a/include/PICA/shader.hpp +++ b/include/PICA/shader.hpp @@ -94,7 +94,7 @@ class PICAShader { u32 entrypoint = 0; // Initial shader PC // We want these registers in this order & with this alignment for uploading them directly to a UBO - // When emulating shaders on the GPU + // When emulating shaders on the GPU. Plus this alignment for float uniforms is necessary for doing SIMD in the shader->CPU recompilers. alignas(16) std::array floatUniforms; alignas(16) std::array, 4> intUniforms; u32 boolUniform; From fc397b2b58a9c04c6c3616a41d8fbeb19801586c Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 23 Jul 2024 02:33:53 +0300 Subject: [PATCH 136/251] Fix Linux Qt packages --- .github/workflows/Qt_Build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Qt_Build.yml b/.github/workflows/Qt_Build.yml index 1f9db49e..4d5c8b57 100644 --- a/.github/workflows/Qt_Build.yml +++ b/.github/workflows/Qt_Build.yml @@ -105,7 +105,7 @@ jobs: - name: Install misc packages run: | - sudo apt-get update && sudo apt install libx11-dev libgl1-mesa-glx mesa-common-dev libfuse2 libwayland-dev + sudo apt-get update && sudo apt install libx11-dev libgl1-mesa-glx mesa-common-dev libfuse2 libwayland-dev libgl1-mesa-dev sudo add-apt-repository -y ppa:savoury1/qt-6-2 sudo apt update sudo apt install qt6-base-dev qt6-base-private-dev From e4d4a356744f29a3f9d650bb4e915435dc0d411a Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 23 Jul 2024 04:11:12 +0300 Subject: [PATCH 137/251] Renderer GL: Add UB checks --- include/PICA/gpu.hpp | 3 ++- src/core/renderer_gl/renderer_gl.cpp | 13 +++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/include/PICA/gpu.hpp b/include/PICA/gpu.hpp index c4c8db5c..ac2a49e6 100644 --- a/include/PICA/gpu.hpp +++ b/include/PICA/gpu.hpp @@ -167,7 +167,8 @@ class GPU { u32 index = paddr - PhysicalAddrs::VRAM; return (T*)&vram[index]; } else [[unlikely]] { - Helpers::panic("[GPU] Tried to access unknown physical address: %08X", paddr); + Helpers::warn("[GPU] Tried to access unknown physical address: %08X", paddr); + return nullptr; } } diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index f26158ae..8b614d2d 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -659,7 +659,15 @@ OpenGL::Texture RendererGL::getTexture(Texture& tex) { if (buffer.has_value()) { return buffer.value().get().texture; } else { - const auto textureData = std::span{gpu.getPointerPhys(tex.location), tex.sizeInBytes()}; // Get pointer to the texture data in 3DS memory + const u8* startPointer = gpu.getPointerPhys(tex.location); + const usize sizeInBytes = tex.sizeInBytes(); + + if (startPointer == nullptr || (sizeInBytes > 0 && gpu.getPointerPhys(tex.location + sizeInBytes - 1) == nullptr)) [[unlikely]] { + Helpers::warn("Out-of-bounds texture fetch"); + return blankTexture; + } + + const auto textureData = std::span{startPointer, tex.sizeInBytes()}; // Get pointer to the texture data in 3DS memory Texture& newTex = textureCache.add(tex); newTex.decodeTexture(textureData); @@ -770,7 +778,8 @@ void RendererGL::textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 if (inputWidth != 0) [[likely]] { copyHeight = (copySize / inputWidth) * 8; } else { - copyHeight = 0; + Helpers::warn("Zero-width texture copy"); + return; } // Find the source surface. From 1fa9ce126b0f5b24707b4ca79111a964827cd787 Mon Sep 17 00:00:00 2001 From: Samuliak Date: Tue, 23 Jul 2024 10:54:01 +0200 Subject: [PATCH 138/251] add: period at the end of a sentence --- src/miniaudio.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/miniaudio.cpp b/src/miniaudio.cpp index e42fea68..a61979e0 100644 --- a/src/miniaudio.cpp +++ b/src/miniaudio.cpp @@ -1,5 +1,5 @@ // We do not need the ability to be able to encode or decode audio files for the time being -// So we disable said functionality to make the executable smaller +// So we disable said functionality to make the executable smaller. #define MA_NO_DECODING #define MA_NO_ENCODING #define MINIAUDIO_IMPLEMENTATION From 855a374f6702a40a35fc78e67f2679ccaf13a85f Mon Sep 17 00:00:00 2001 From: SamoZ256 <96914946+SamoZ256@users.noreply.github.com> Date: Tue, 23 Jul 2024 10:57:13 +0200 Subject: [PATCH 139/251] add: period at the end of a sentence (#553) --- src/miniaudio.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/miniaudio.cpp b/src/miniaudio.cpp index e42fea68..a61979e0 100644 --- a/src/miniaudio.cpp +++ b/src/miniaudio.cpp @@ -1,5 +1,5 @@ // We do not need the ability to be able to encode or decode audio files for the time being -// So we disable said functionality to make the executable smaller +// So we disable said functionality to make the executable smaller. #define MA_NO_DECODING #define MA_NO_ENCODING #define MINIAUDIO_IMPLEMENTATION From 0f80d0af7a2e2c9c8cad52c0b5ddc620b748f4b3 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 23 Jul 2024 15:22:15 +0300 Subject: [PATCH 140/251] Rename Handle to HorizonHandle, add metal-cpp submodule, format --- .gitmodules | 3 +++ include/kernel/handles.hpp | 8 ++++---- include/kernel/kernel.hpp | 2 ++ include/kernel/kernel_types.hpp | 10 +++++++++- include/memory.hpp | 10 +++++++++- include/services/ac.hpp | 2 ++ include/services/act.hpp | 4 +++- include/services/am.hpp | 4 +++- include/services/apt.hpp | 7 +++++-- include/services/boss.hpp | 7 +++++-- include/services/cam.hpp | 1 + include/services/cecd.hpp | 5 ++++- include/services/cfg.hpp | 15 +++++++++------ include/services/csnd.hpp | 6 +++--- include/services/dlp_srvr.hpp | 4 +++- include/services/dsp.hpp | 2 ++ include/services/frd.hpp | 11 +++++++---- include/services/fs.hpp | 4 +++- include/services/gsp_gpu.hpp | 2 ++ include/services/gsp_lcd.hpp | 2 ++ include/services/hid.hpp | 2 ++ include/services/http.hpp | 2 ++ include/services/ir_user.hpp | 2 ++ include/services/ldr_ro.hpp | 4 +++- include/services/mcu/mcu_hwc.hpp | 2 ++ include/services/mic.hpp | 6 ++++-- include/services/ndm.hpp | 11 +++++++++-- include/services/news_u.hpp | 2 ++ include/services/nfc.hpp | 2 ++ include/services/nim.hpp | 4 +++- include/services/nwm_uds.hpp | 2 ++ include/services/ptm.hpp | 2 +- include/services/service_manager.hpp | 2 ++ include/services/soc.hpp | 4 +++- include/services/ssl.hpp | 8 +++++--- include/services/y2r.hpp | 15 +++++++++------ src/core/kernel/address_arbiter.cpp | 2 +- src/core/kernel/events.cpp | 2 +- src/core/kernel/kernel.cpp | 2 +- src/core/kernel/memory_management.cpp | 2 +- src/core/kernel/ports.cpp | 6 +++--- src/core/kernel/threads.cpp | 6 +++--- src/core/kernel/timers.cpp | 2 +- src/core/services/fs.cpp | 6 +++--- src/core/services/service_manager.cpp | 2 +- third_party/metal-cpp | 1 + 46 files changed, 150 insertions(+), 60 deletions(-) create mode 160000 third_party/metal-cpp diff --git a/.gitmodules b/.gitmodules index 5a136acb..656e1f41 100644 --- a/.gitmodules +++ b/.gitmodules @@ -73,3 +73,6 @@ [submodule "third_party/hips"] path = third_party/hips url = https://github.com/wheremyfoodat/Hips +[submodule "third_party/metal-cpp"] + path = third_party/metal-cpp + url = https://github.com/Panda3DS-emu/metal-cpp diff --git a/include/kernel/handles.hpp b/include/kernel/handles.hpp index fe746b65..45400837 100644 --- a/include/kernel/handles.hpp +++ b/include/kernel/handles.hpp @@ -1,7 +1,7 @@ #pragma once #include "helpers.hpp" -using Handle = u32; +using HorizonHandle = u32; namespace KernelHandles { enum : u32 { @@ -61,17 +61,17 @@ namespace KernelHandles { }; // Returns whether "handle" belongs to one of the OS services - static constexpr bool isServiceHandle(Handle handle) { + static constexpr bool isServiceHandle(HorizonHandle handle) { return handle >= MinServiceHandle && handle <= MaxServiceHandle; } // Returns whether "handle" belongs to one of the OS services' shared memory areas - static constexpr bool isSharedMemHandle(Handle handle) { + static constexpr bool isSharedMemHandle(HorizonHandle handle) { return handle >= MinSharedMemHandle && handle <= MaxSharedMemHandle; } // Returns the name of a handle as a string based on the given handle - static const char* getServiceName(Handle handle) { + static const char* getServiceName(HorizonHandle handle) { switch (handle) { case AC: return "AC"; case ACT: return "ACT"; diff --git a/include/kernel/kernel.hpp b/include/kernel/kernel.hpp index e0c0651b..abc508ac 100644 --- a/include/kernel/kernel.hpp +++ b/include/kernel/kernel.hpp @@ -18,6 +18,8 @@ class CPU; struct Scheduler; class Kernel { + using Handle = HorizonHandle; + std::span regs; CPU& cpu; Memory& mem; diff --git a/include/kernel/kernel_types.hpp b/include/kernel/kernel_types.hpp index a68ef8d5..a3a60c34 100644 --- a/include/kernel/kernel_types.hpp +++ b/include/kernel/kernel_types.hpp @@ -47,7 +47,7 @@ enum class ProcessorID : s32 { struct AddressArbiter {}; struct ResourceLimits { - Handle handle; + HorizonHandle handle; s32 currentCommit = 0; }; @@ -91,6 +91,8 @@ struct Port { }; struct Session { + using Handle = HorizonHandle; + Handle portHandle; // The port this session is subscribed to Session(Handle portHandle) : portHandle(portHandle) {} }; @@ -109,6 +111,8 @@ enum class ThreadStatus { }; struct Thread { + using Handle = HorizonHandle; + u32 initialSP; // Initial r13 value u32 entrypoint; // Initial r15 value u32 priority; @@ -161,6 +165,8 @@ static const char* kernelObjectTypeToString(KernelObjectType t) { } struct Mutex { + using Handle = HorizonHandle; + u64 waitlist; // Refer to the getWaitlist function below for documentation Handle ownerThread = 0; // Index of the thread that holds the mutex if it's locked Handle handle; // Handle of the mutex itself @@ -203,6 +209,8 @@ struct MemoryBlock { // Generic kernel object class struct KernelObject { + using Handle = HorizonHandle; + Handle handle = 0; // A u32 the OS will use to identify objects void* data = nullptr; KernelObjectType type; diff --git a/include/memory.hpp b/include/memory.hpp index 33ccbae5..2f01aa35 100644 --- a/include/memory.hpp +++ b/include/memory.hpp @@ -102,6 +102,8 @@ namespace KernelMemoryTypes { } class Memory { + using Handle = HorizonHandle; + u8* fcram; u8* dspRam; // Provided to us by Audio u8* vram; // Provided to the memory class by the GPU class @@ -213,8 +215,14 @@ private: } enum class BatteryLevel { - Empty = 0, AlmostEmpty, OneBar, TwoBars, ThreeBars, FourBars + Empty = 0, + AlmostEmpty, + OneBar, + TwoBars, + ThreeBars, + FourBars, }; + u8 getBatteryState(bool adapterConnected, bool charging, BatteryLevel batteryLevel) { u8 value = static_cast(batteryLevel) << 2; // Bits 2:4 are the battery level from 0 to 5 if (adapterConnected) value |= 1 << 0; // Bit 0 shows if the charger is connected diff --git a/include/services/ac.hpp b/include/services/ac.hpp index 4ba53033..56acd436 100644 --- a/include/services/ac.hpp +++ b/include/services/ac.hpp @@ -8,6 +8,8 @@ #include "result/result.hpp" class ACService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::AC; Memory& mem; MAKE_LOG_FUNCTION(log, acLogger) diff --git a/include/services/act.hpp b/include/services/act.hpp index 92c69c60..3fe68993 100644 --- a/include/services/act.hpp +++ b/include/services/act.hpp @@ -6,6 +6,8 @@ #include "result/result.hpp" class ACTService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::ACT; Memory& mem; MAKE_LOG_FUNCTION(log, actLogger) @@ -15,7 +17,7 @@ class ACTService { void generateUUID(u32 messagePointer); void getAccountDataBlock(u32 messagePointer); -public: + public: ACTService(Memory& mem) : mem(mem) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/include/services/am.hpp b/include/services/am.hpp index 672909ff..f72a5efc 100644 --- a/include/services/am.hpp +++ b/include/services/am.hpp @@ -6,6 +6,8 @@ #include "result/result.hpp" class AMService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::AM; Memory& mem; MAKE_LOG_FUNCTION(log, amLogger) @@ -15,7 +17,7 @@ class AMService { void getPatchTitleInfo(u32 messagePointer); void listTitleInfo(u32 messagePointer); -public: + public: AMService(Memory& mem) : mem(mem) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/include/services/apt.hpp b/include/services/apt.hpp index 48a59c2d..624151c1 100644 --- a/include/services/apt.hpp +++ b/include/services/apt.hpp @@ -12,7 +12,8 @@ class Kernel; enum class ConsoleModel : u32 { - Old3DS, New3DS + Old3DS, + New3DS, }; // https://www.3dbrew.org/wiki/NS_and_APT_Services#Command @@ -41,6 +42,8 @@ namespace APT::Transitions { } class APTService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::APT; Memory& mem; Kernel& kernel; @@ -99,7 +102,7 @@ class APTService { u32 screencapPostPermission; -public: + public: APTService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel), appletManager(mem) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/include/services/boss.hpp b/include/services/boss.hpp index 769184e5..edc50dee 100644 --- a/include/services/boss.hpp +++ b/include/services/boss.hpp @@ -6,6 +6,8 @@ #include "result/result.hpp" class BOSSService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::BOSS; Memory& mem; MAKE_LOG_FUNCTION(log, bossLogger) @@ -17,7 +19,7 @@ class BOSSService { void getNewArrivalFlag(u32 messagePointer); void getNsDataIdList(u32 messagePointer, u32 commandWord); void getOptoutFlag(u32 messagePointer); - void getStorageEntryInfo(u32 messagePointer); // Unknown what this is, name taken from Citra + void getStorageEntryInfo(u32 messagePointer); // Unknown what this is, name taken from Citra void getTaskIdList(u32 messagePointer); void getTaskInfo(u32 messagePointer); void getTaskServiceStatus(u32 messagePointer); @@ -35,7 +37,8 @@ class BOSSService { void unregisterTask(u32 messagePointer); s8 optoutFlag; -public: + + public: BOSSService(Memory& mem) : mem(mem) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/include/services/cam.hpp b/include/services/cam.hpp index 60ede3b9..e5254997 100644 --- a/include/services/cam.hpp +++ b/include/services/cam.hpp @@ -12,6 +12,7 @@ class Kernel; class CAMService { + using Handle = HorizonHandle; using Event = std::optional; struct Port { diff --git a/include/services/cecd.hpp b/include/services/cecd.hpp index 656e38ad..4612c17b 100644 --- a/include/services/cecd.hpp +++ b/include/services/cecd.hpp @@ -1,5 +1,6 @@ #pragma once #include + #include "helpers.hpp" #include "kernel_types.hpp" #include "logger.hpp" @@ -9,6 +10,8 @@ class Kernel; class CECDService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::CECD; Memory& mem; Kernel& kernel; @@ -20,7 +23,7 @@ class CECDService { void getInfoEventHandle(u32 messagePointer); void openAndRead(u32 messagePointer); -public: + public: CECDService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/include/services/cfg.hpp b/include/services/cfg.hpp index 7241a409..e2ddffa8 100644 --- a/include/services/cfg.hpp +++ b/include/services/cfg.hpp @@ -1,5 +1,6 @@ #pragma once #include + #include "helpers.hpp" #include "logger.hpp" #include "memory.hpp" @@ -7,8 +8,10 @@ #include "result/result.hpp" class CFGService { + using Handle = HorizonHandle; + Memory& mem; - CountryCodes country = CountryCodes::US; // Default to USA + CountryCodes country = CountryCodes::US; // Default to USA MAKE_LOG_FUNCTION(log, cfgLogger) void writeStringU16(u32 pointer, const std::u16string& string); @@ -27,12 +30,12 @@ class CFGService { void getConfigInfo(u32 output, u32 blockID, u32 size, u32 permissionMask); -public: + public: enum class Type { - U, // cfg:u - I, // cfg:i - S, // cfg:s - NOR, // cfg:nor + U, // cfg:u + I, // cfg:i + S, // cfg:s + NOR, // cfg:nor }; CFGService(Memory& mem) : mem(mem) {} diff --git a/include/services/csnd.hpp b/include/services/csnd.hpp index 8f6d60f8..93fa941d 100644 --- a/include/services/csnd.hpp +++ b/include/services/csnd.hpp @@ -10,6 +10,8 @@ class Kernel; class CSNDService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::CSND; Memory& mem; Kernel& kernel; @@ -30,7 +32,5 @@ class CSNDService { void reset(); void handleSyncRequest(u32 messagePointer); - void setSharedMemory(u8* ptr) { - sharedMemory = ptr; - } + void setSharedMemory(u8* ptr) { sharedMemory = ptr; } }; \ No newline at end of file diff --git a/include/services/dlp_srvr.hpp b/include/services/dlp_srvr.hpp index 1e714283..ae9cc96f 100644 --- a/include/services/dlp_srvr.hpp +++ b/include/services/dlp_srvr.hpp @@ -8,6 +8,8 @@ // Please forgive me for how everything in this file is named // "dlp:SRVR" is not a nice name to work with class DlpSrvrService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::DLP_SRVR; Memory& mem; MAKE_LOG_FUNCTION(log, dlpSrvrLogger) @@ -15,7 +17,7 @@ class DlpSrvrService { // Service commands void isChild(u32 messagePointer); -public: + public: DlpSrvrService(Memory& mem) : mem(mem) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/include/services/dsp.hpp b/include/services/dsp.hpp index 5cbd4fd5..bc1adbca 100644 --- a/include/services/dsp.hpp +++ b/include/services/dsp.hpp @@ -14,6 +14,8 @@ class Kernel; class DSPService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::DSP; Memory& mem; Kernel& kernel; diff --git a/include/services/frd.hpp b/include/services/frd.hpp index b9b3b0fe..914d9251 100644 --- a/include/services/frd.hpp +++ b/include/services/frd.hpp @@ -1,5 +1,6 @@ #pragma once #include + #include "helpers.hpp" #include "kernel_types.hpp" #include "logger.hpp" @@ -15,6 +16,8 @@ struct FriendKey { static_assert(sizeof(FriendKey) == 16); class FRDService { + using Handle = HorizonHandle; + Memory& mem; MAKE_LOG_FUNCTION(log, frdLogger) @@ -51,11 +54,11 @@ class FRDService { }; static_assert(sizeof(Profile) == 8); -public: + public: enum class Type { - A, // frd:a - N, // frd:n - U, // frd:u + A, // frd:a + N, // frd:n + U, // frd:u }; FRDService(Memory& mem) : mem(mem) {} diff --git a/include/services/fs.hpp b/include/services/fs.hpp index 4a613121..3b3b3d44 100644 --- a/include/services/fs.hpp +++ b/include/services/fs.hpp @@ -16,6 +16,8 @@ class Kernel; class FSService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::FS; Memory& mem; Kernel& kernel; @@ -81,7 +83,7 @@ class FSService { // Used for set/get priority: Not sure what sort of priority this is referring to u32 priority; -public: + public: FSService(Memory& mem, Kernel& kernel, const EmulatorConfig& config) : mem(mem), saveData(mem), sharedExtSaveData_nand(mem, "../SharedFiles/NAND", true), extSaveData_sdmc(mem, "SDMC"), sdmc(mem), sdmcWriteOnly(mem, true), selfNcch(mem), ncch(mem), userSaveData1(mem, ArchiveID::UserSaveData1), diff --git a/include/services/gsp_gpu.hpp b/include/services/gsp_gpu.hpp index 0da4fcd0..d7244609 100644 --- a/include/services/gsp_gpu.hpp +++ b/include/services/gsp_gpu.hpp @@ -22,6 +22,8 @@ enum class GPUInterrupt : u8 { class Kernel; class GPUService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::GPU; Memory& mem; GPU& gpu; diff --git a/include/services/gsp_lcd.hpp b/include/services/gsp_lcd.hpp index e7672d4f..f34f59ab 100644 --- a/include/services/gsp_lcd.hpp +++ b/include/services/gsp_lcd.hpp @@ -6,6 +6,8 @@ #include "result/result.hpp" class LCDService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::LCD; Memory& mem; MAKE_LOG_FUNCTION(log, gspLCDLogger) diff --git a/include/services/hid.hpp b/include/services/hid.hpp index d9018a4f..86a55479 100644 --- a/include/services/hid.hpp +++ b/include/services/hid.hpp @@ -38,6 +38,8 @@ namespace HID::Keys { class Kernel; class HIDService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::HID; Memory& mem; Kernel& kernel; diff --git a/include/services/http.hpp b/include/services/http.hpp index 1e7f30c3..8b23fb2d 100644 --- a/include/services/http.hpp +++ b/include/services/http.hpp @@ -5,6 +5,8 @@ #include "memory.hpp" class HTTPService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::HTTP; Memory& mem; MAKE_LOG_FUNCTION(log, httpLogger) diff --git a/include/services/ir_user.hpp b/include/services/ir_user.hpp index 186d9717..d475bdaa 100644 --- a/include/services/ir_user.hpp +++ b/include/services/ir_user.hpp @@ -11,6 +11,8 @@ class Kernel; class IRUserService { + using Handle = HorizonHandle; + enum class DeviceID : u8 { CirclePadPro = 1, }; diff --git a/include/services/ldr_ro.hpp b/include/services/ldr_ro.hpp index 71516547..cf60e036 100644 --- a/include/services/ldr_ro.hpp +++ b/include/services/ldr_ro.hpp @@ -8,6 +8,8 @@ class Kernel; class LDRService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::LDR_RO; Memory& mem; Kernel& kernel; @@ -22,7 +24,7 @@ class LDRService { void loadCRR(u32 messagePointer); void unloadCRO(u32 messagePointer); -public: + public: LDRService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/include/services/mcu/mcu_hwc.hpp b/include/services/mcu/mcu_hwc.hpp index 354a0c20..4c6a8830 100644 --- a/include/services/mcu/mcu_hwc.hpp +++ b/include/services/mcu/mcu_hwc.hpp @@ -7,6 +7,8 @@ namespace MCU { class HWCService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::MCU_HWC; Memory& mem; MAKE_LOG_FUNCTION(log, mcuLogger) diff --git a/include/services/mic.hpp b/include/services/mic.hpp index f709c27f..f166c5aa 100644 --- a/include/services/mic.hpp +++ b/include/services/mic.hpp @@ -9,6 +9,8 @@ class Kernel; class MICService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::MIC; Memory& mem; Kernel& kernel; @@ -29,14 +31,14 @@ class MICService { void unmapSharedMem(u32 messagePointer); void theCaptainToadFunction(u32 messagePointer); - u8 gain = 0; // How loud our microphone input signal is + u8 gain = 0; // How loud our microphone input signal is bool micEnabled = false; bool shouldClamp = false; bool currentlySampling = false; std::optional eventHandle; -public: + public: MICService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/include/services/ndm.hpp b/include/services/ndm.hpp index 6d4e5ad8..67679403 100644 --- a/include/services/ndm.hpp +++ b/include/services/ndm.hpp @@ -6,7 +6,14 @@ #include "result/result.hpp" class NDMService { - enum class ExclusiveState : u32 { None = 0, Infrastructure = 1, LocalComms = 2, StreetPass = 3, StreetPassData = 4 }; + using Handle = HorizonHandle; + enum class ExclusiveState : u32 { + None = 0, + Infrastructure = 1, + LocalComms = 2, + StreetPass = 3, + StreetPassData = 4, + }; Handle handle = KernelHandles::NDM; Memory& mem; @@ -25,7 +32,7 @@ class NDMService { ExclusiveState exclusiveState = ExclusiveState::None; -public: + public: NDMService(Memory& mem) : mem(mem) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/include/services/news_u.hpp b/include/services/news_u.hpp index 61266e9a..15ae0b16 100644 --- a/include/services/news_u.hpp +++ b/include/services/news_u.hpp @@ -5,6 +5,8 @@ #include "memory.hpp" class NewsUService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::NEWS_U; Memory& mem; MAKE_LOG_FUNCTION(log, newsLogger) diff --git a/include/services/nfc.hpp b/include/services/nfc.hpp index 8eea8a41..e242a326 100644 --- a/include/services/nfc.hpp +++ b/include/services/nfc.hpp @@ -12,6 +12,8 @@ class Kernel; class NFCService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::NFC; Memory& mem; Kernel& kernel; diff --git a/include/services/nim.hpp b/include/services/nim.hpp index dfe13694..dbb3bb8b 100644 --- a/include/services/nim.hpp +++ b/include/services/nim.hpp @@ -6,6 +6,8 @@ #include "result/result.hpp" class NIMService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::NIM; Memory& mem; MAKE_LOG_FUNCTION(log, nimLogger) @@ -13,7 +15,7 @@ class NIMService { // Service commands void initialize(u32 messagePointer); -public: + public: NIMService(Memory& mem) : mem(mem) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/include/services/nwm_uds.hpp b/include/services/nwm_uds.hpp index bf116bcf..a3b342b8 100644 --- a/include/services/nwm_uds.hpp +++ b/include/services/nwm_uds.hpp @@ -10,6 +10,8 @@ class Kernel; class NwmUdsService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::NWM_UDS; Memory& mem; Kernel& kernel; diff --git a/include/services/ptm.hpp b/include/services/ptm.hpp index f752839b..5b797a1d 100644 --- a/include/services/ptm.hpp +++ b/include/services/ptm.hpp @@ -22,7 +22,7 @@ class PTMService { void getStepHistoryAll(u32 messagePointer); void getTotalStepCount(u32 messagePointer); -public: + public: enum class Type { U, // ptm:u SYSM, // ptm:sysm diff --git a/include/services/service_manager.hpp b/include/services/service_manager.hpp index 6679f98d..4fa1e665 100644 --- a/include/services/service_manager.hpp +++ b/include/services/service_manager.hpp @@ -42,6 +42,8 @@ struct EmulatorConfig; class Kernel; class ServiceManager { + using Handle = HorizonHandle; + std::span regs; Memory& mem; Kernel& kernel; diff --git a/include/services/soc.hpp b/include/services/soc.hpp index 88f0b456..ff334a2c 100644 --- a/include/services/soc.hpp +++ b/include/services/soc.hpp @@ -5,6 +5,8 @@ #include "memory.hpp" class SOCService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::SOC; Memory& mem; MAKE_LOG_FUNCTION(log, socLogger) @@ -14,7 +16,7 @@ class SOCService { // Service commands void initializeSockets(u32 messagePointer); -public: + public: SOCService(Memory& mem) : mem(mem) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/include/services/ssl.hpp b/include/services/ssl.hpp index 0282049a..4b45fc81 100644 --- a/include/services/ssl.hpp +++ b/include/services/ssl.hpp @@ -1,17 +1,19 @@ #pragma once +#include + #include "helpers.hpp" #include "kernel_types.hpp" #include "logger.hpp" #include "memory.hpp" -#include - class SSLService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::SSL; Memory& mem; MAKE_LOG_FUNCTION(log, sslLogger) - std::mt19937 rng; // Use a Mersenne Twister for RNG since this service is supposed to have better rng than just rand() + std::mt19937 rng; // Use a Mersenne Twister for RNG since this service is supposed to have better rng than just rand() bool initialized; // Service commands diff --git a/include/services/y2r.hpp b/include/services/y2r.hpp index 4aa96d7b..6afebdb8 100644 --- a/include/services/y2r.hpp +++ b/include/services/y2r.hpp @@ -1,6 +1,7 @@ #pragma once #include #include + #include "helpers.hpp" #include "kernel_types.hpp" #include "logger.hpp" @@ -10,6 +11,8 @@ class Kernel; class Y2RService { + using Handle = HorizonHandle; + Handle handle = KernelHandles::Y2R; Memory& mem; Kernel& kernel; @@ -20,7 +23,7 @@ class Y2RService { enum class BusyStatus : u32 { NotBusy = 0, - Busy = 1 + Busy = 1, }; enum class InputFormat : u32 { @@ -35,7 +38,7 @@ class Y2RService { RGB32 = 0, RGB24 = 1, RGB15 = 2, - RGB565 = 3 + RGB565 = 3, }; // Clockwise rotation @@ -43,12 +46,12 @@ class Y2RService { None = 0, Rotate90 = 1, Rotate180 = 2, - Rotate270 = 3 + Rotate270 = 3, }; enum class BlockAlignment : u32 { - Line = 0, // Output buffer's pixels are arranged linearly. Used when outputting to the framebuffer. - Block8x8 = 1, // Output buffer's pixels are morton swizzled. Used when outputting to a GPU texture. + Line = 0, // Output buffer's pixels are arranged linearly. Used when outputting to the framebuffer. + Block8x8 = 1, // Output buffer's pixels are morton swizzled. Used when outputting to a GPU texture. }; // https://github.com/citra-emu/citra/blob/ac9d72a95ca9a60de8d39484a14aecf489d6d016/src/core/hle/service/cam/y2r_u.cpp#L33 @@ -60,7 +63,7 @@ class Y2RService { {{0x12A, 0x1CA, 0x88, 0x36, 0x21C, -0x1F04, 0x99C, -0x2421}}, // ITU_Rec709_Scaling }}; - CoefficientSet conversionCoefficients; // Current conversion coefficients + CoefficientSet conversionCoefficients; // Current conversion coefficients InputFormat inputFmt; OutputFormat outputFmt; diff --git a/src/core/kernel/address_arbiter.cpp b/src/core/kernel/address_arbiter.cpp index 8c07b423..d15c81b8 100644 --- a/src/core/kernel/address_arbiter.cpp +++ b/src/core/kernel/address_arbiter.cpp @@ -12,7 +12,7 @@ static const char* arbitrationTypeToString(u32 type) { } } -Handle Kernel::makeArbiter() { +HorizonHandle Kernel::makeArbiter() { if (arbiterCount >= appResourceLimits.maxAddressArbiters) { Helpers::panic("Overflowed the number of address arbiters"); } diff --git a/src/core/kernel/events.cpp b/src/core/kernel/events.cpp index 7c0d3047..6d3dfbd7 100644 --- a/src/core/kernel/events.cpp +++ b/src/core/kernel/events.cpp @@ -12,7 +12,7 @@ const char* Kernel::resetTypeToString(u32 type) { } } -Handle Kernel::makeEvent(ResetType resetType, Event::CallbackType callback) { +HorizonHandle Kernel::makeEvent(ResetType resetType, Event::CallbackType callback) { Handle ret = makeObject(KernelObjectType::Event); objects[ret].data = new Event(resetType, callback); return ret; diff --git a/src/core/kernel/kernel.cpp b/src/core/kernel/kernel.cpp index 0d1efc15..d4229b55 100644 --- a/src/core/kernel/kernel.cpp +++ b/src/core/kernel/kernel.cpp @@ -82,7 +82,7 @@ void Kernel::setVersion(u8 major, u8 minor) { mem.kernelVersion = descriptor; // The memory objects needs a copy because you can read the kernel ver from config mem } -Handle Kernel::makeProcess(u32 id) { +HorizonHandle Kernel::makeProcess(u32 id) { const Handle processHandle = makeObject(KernelObjectType::Process); const Handle resourceLimitHandle = makeObject(KernelObjectType::ResourceLimit); diff --git a/src/core/kernel/memory_management.cpp b/src/core/kernel/memory_management.cpp index 0d234be5..aeac6269 100644 --- a/src/core/kernel/memory_management.cpp +++ b/src/core/kernel/memory_management.cpp @@ -154,7 +154,7 @@ void Kernel::mapMemoryBlock() { regs[0] = Result::Success; } -Handle Kernel::makeMemoryBlock(u32 addr, u32 size, u32 myPermission, u32 otherPermission) { +HorizonHandle Kernel::makeMemoryBlock(u32 addr, u32 size, u32 myPermission, u32 otherPermission) { Handle ret = makeObject(KernelObjectType::MemoryBlock); objects[ret].data = new MemoryBlock(addr, size, myPermission, otherPermission); diff --git a/src/core/kernel/ports.cpp b/src/core/kernel/ports.cpp index 6038de44..61ab26e3 100644 --- a/src/core/kernel/ports.cpp +++ b/src/core/kernel/ports.cpp @@ -1,7 +1,7 @@ #include "kernel.hpp" #include -Handle Kernel::makePort(const char* name) { +HorizonHandle Kernel::makePort(const char* name) { Handle ret = makeObject(KernelObjectType::Port); portHandles.push_back(ret); // Push the port handle to our cache of port handles objects[ret].data = new Port(name); @@ -9,7 +9,7 @@ Handle Kernel::makePort(const char* name) { return ret; } -Handle Kernel::makeSession(Handle portHandle) { +HorizonHandle Kernel::makeSession(Handle portHandle) { const auto port = getObject(portHandle, KernelObjectType::Port); if (port == nullptr) [[unlikely]] { Helpers::panic("Trying to make session for non-existent port"); @@ -23,7 +23,7 @@ Handle Kernel::makeSession(Handle portHandle) { // Get the handle of a port based on its name // If there's no such port, return nullopt -std::optional Kernel::getPortHandle(const char* name) { +std::optional Kernel::getPortHandle(const char* name) { for (auto handle : portHandles) { const auto data = objects[handle].getData(); if (std::strncmp(name, data->name, Port::maxNameLen) == 0) { diff --git a/src/core/kernel/threads.cpp b/src/core/kernel/threads.cpp index 3a6201c1..9eb7a197 100644 --- a/src/core/kernel/threads.cpp +++ b/src/core/kernel/threads.cpp @@ -109,7 +109,7 @@ void Kernel::rescheduleThreads() { } // Internal OS function to spawn a thread -Handle Kernel::makeThread(u32 entrypoint, u32 initialSP, u32 priority, ProcessorID id, u32 arg, ThreadStatus status) { +HorizonHandle Kernel::makeThread(u32 entrypoint, u32 initialSP, u32 priority, ProcessorID id, u32 arg, ThreadStatus status) { int index; // Index of the created thread in the threads array if (threadCount < appResourceLimits.maxThreads) [[likely]] { // If we have not yet created over too many threads @@ -161,7 +161,7 @@ Handle Kernel::makeThread(u32 entrypoint, u32 initialSP, u32 priority, Processor return ret; } -Handle Kernel::makeMutex(bool locked) { +HorizonHandle Kernel::makeMutex(bool locked) { Handle ret = makeObject(KernelObjectType::Mutex); objects[ret].data = new Mutex(locked, ret); @@ -201,7 +201,7 @@ void Kernel::releaseMutex(Mutex* moo) { } } -Handle Kernel::makeSemaphore(u32 initialCount, u32 maximumCount) { +HorizonHandle Kernel::makeSemaphore(u32 initialCount, u32 maximumCount) { Handle ret = makeObject(KernelObjectType::Semaphore); objects[ret].data = new Semaphore(initialCount, maximumCount); diff --git a/src/core/kernel/timers.cpp b/src/core/kernel/timers.cpp index 35fc57a4..8cfa4773 100644 --- a/src/core/kernel/timers.cpp +++ b/src/core/kernel/timers.cpp @@ -4,7 +4,7 @@ #include "kernel.hpp" #include "scheduler.hpp" -Handle Kernel::makeTimer(ResetType type) { +HorizonHandle Kernel::makeTimer(ResetType type) { Handle ret = makeObject(KernelObjectType::Timer); objects[ret].data = new Timer(type); diff --git a/src/core/services/fs.cpp b/src/core/services/fs.cpp index 2e102958..e81db6cd 100644 --- a/src/core/services/fs.cpp +++ b/src/core/services/fs.cpp @@ -105,7 +105,7 @@ ArchiveBase* FSService::getArchiveFromID(u32 id, const FSPath& archivePath) { } } -std::optional FSService::openFileHandle(ArchiveBase* archive, const FSPath& path, const FSPath& archivePath, const FilePerms& perms) { +std::optional FSService::openFileHandle(ArchiveBase* archive, const FSPath& path, const FSPath& archivePath, const FilePerms& perms) { FileDescriptor opened = archive->openFile(path, perms); if (opened.has_value()) { // If opened doesn't have a value, we failed to open the file auto handle = kernel.makeObject(KernelObjectType::File); @@ -119,7 +119,7 @@ std::optional FSService::openFileHandle(ArchiveBase* archive, const FSPa } } -Rust::Result FSService::openDirectoryHandle(ArchiveBase* archive, const FSPath& path) { +Rust::Result FSService::openDirectoryHandle(ArchiveBase* archive, const FSPath& path) { Rust::Result opened = archive->openDirectory(path); if (opened.isOk()) { // If opened doesn't have a value, we failed to open the directory auto handle = kernel.makeObject(KernelObjectType::Directory); @@ -132,7 +132,7 @@ Rust::Result FSService::openDirectoryHandle(Archi } } -Rust::Result FSService::openArchiveHandle(u32 archiveID, const FSPath& path) { +Rust::Result FSService::openArchiveHandle(u32 archiveID, const FSPath& path) { ArchiveBase* archive = getArchiveFromID(archiveID, path); if (archive == nullptr) [[unlikely]] { diff --git a/src/core/services/service_manager.cpp b/src/core/services/service_manager.cpp index 2a95b5c9..31e3d702 100644 --- a/src/core/services/service_manager.cpp +++ b/src/core/services/service_manager.cpp @@ -93,7 +93,7 @@ void ServiceManager::registerClient(u32 messagePointer) { } // clang-format off -static std::map serviceMap = { +static std::map serviceMap = { { "ac:u", KernelHandles::AC }, { "act:a", KernelHandles::ACT }, { "act:u", KernelHandles::ACT }, diff --git a/third_party/metal-cpp b/third_party/metal-cpp new file mode 160000 index 00000000..a63bd172 --- /dev/null +++ b/third_party/metal-cpp @@ -0,0 +1 @@ +Subproject commit a63bd172ddcba73a3d87ca32032b66ad41ddb9a6 From c319e595457fb5e25a959c043953419bd26e1f58 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 23 Jul 2024 20:13:14 +0000 Subject: [PATCH 141/251] Resizing window on Qt (#556) * Qt: Add screen resize * Qt: Allocate screen on heap for setCentralWidget * Fix header inclusion order * Switch to std::function for resize callback * rdeepfried --- include/panda_qt/main_window.hpp | 10 +++++++- include/panda_qt/screen.hpp | 16 ++++++++++++- src/panda_qt/main_window.cpp | 41 ++++++++++++++++++++++++-------- src/panda_qt/screen.cpp | 30 +++++++++++++++++++++-- 4 files changed, 83 insertions(+), 14 deletions(-) diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index 831074a2..c99fb4c2 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -50,6 +50,7 @@ class MainWindow : public QMainWindow { PressTouchscreen, ReleaseTouchscreen, ReloadUbershader, + SetScreenSize, }; // Tagged union representing our message queue messages @@ -81,6 +82,11 @@ class MainWindow : public QMainWindow { u16 x; u16 y; } touchscreen; + + struct { + u32 width; + u32 height; + } screenSize; }; }; @@ -95,7 +101,7 @@ class MainWindow : public QMainWindow { QMenuBar* menuBar = nullptr; InputMappings keyboardMappings; - ScreenWidget screen; + ScreenWidget* screen; AboutWindow* aboutWindow; ConfigWindow* configWindow; CheatsWindow* cheatsEditor; @@ -141,4 +147,6 @@ class MainWindow : public QMainWindow { void loadLuaScript(const std::string& code); void reloadShader(const std::string& shader); void editCheat(u32 handle, const std::vector& cheat, const std::function& callback); + + void handleScreenResize(u32 width, u32 height); }; diff --git a/include/panda_qt/screen.hpp b/include/panda_qt/screen.hpp index dcff3e90..1ed4966b 100644 --- a/include/panda_qt/screen.hpp +++ b/include/panda_qt/screen.hpp @@ -1,5 +1,6 @@ #pragma once #include +#include #include #include "gl/context.h" @@ -10,15 +11,28 @@ class ScreenWidget : public QWidget { Q_OBJECT public: - ScreenWidget(QWidget* parent = nullptr); + using ResizeCallback = std::function; + + ScreenWidget(ResizeCallback resizeCallback, QWidget* parent = nullptr); + void resizeEvent(QResizeEvent* event) override; + // Called by the emulator thread for resizing the actual GL surface, since the emulator thread owns the GL context + void resizeSurface(u32 width, u32 height); + GL::Context* getGLContext() { return glContext.get(); } // Dimensions of our output surface u32 surfaceWidth = 0; u32 surfaceHeight = 0; + WindowInfo windowInfo; + + // Cached "previous" dimensions, used when resizing our window + u32 previousWidth = 0; + u32 previousHeight = 0; private: std::unique_ptr glContext = nullptr; + ResizeCallback resizeCallback; + bool createGLContext(); qreal devicePixelRatioFromScreen() const; diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index cfa45e85..1f9b8123 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -11,13 +11,17 @@ #include "input_mappings.hpp" #include "services/dsp.hpp" -MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), keyboardMappings(InputMappings::defaultKeyboardMappings()), screen(this) { +MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), keyboardMappings(InputMappings::defaultKeyboardMappings()) { setWindowTitle("Alber"); // Enable drop events for loading ROMs setAcceptDrops(true); resize(800, 240 * 4); - screen.show(); + // We pass a callback to the screen widget that will be triggered every time we resize the screen + screen = new ScreenWidget([this](u32 width, u32 height) { handleScreenResize(width, height); }, this); + setCentralWidget(screen); + + screen->show(); appRunning = true; // Set our menu bar up @@ -69,7 +73,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) connect(aboutAction, &QAction::triggered, this, &MainWindow::showAboutMenu); emu = new Emulator(); - emu->setOutputSize(screen.surfaceWidth, screen.surfaceHeight); + emu->setOutputSize(screen->surfaceWidth, screen->surfaceHeight); // Set up misc objects aboutWindow = new AboutWindow(nullptr); @@ -101,7 +105,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) if (usingGL) { // Make GL context current for this thread, enable VSync - GL::Context* glContext = screen.getGLContext(); + GL::Context* glContext = screen->getGLContext(); glContext->MakeCurrent(); glContext->SetSwapInterval(emu->getConfig().vsyncEnabled ? 1 : 0); @@ -145,13 +149,13 @@ void MainWindow::emuThreadMainLoop() { // Unbind GL context if we're using GL, otherwise some setups seem to be unable to join this thread if (usingGL) { - screen.getGLContext()->DoneCurrent(); + screen->getGLContext()->DoneCurrent(); } } void MainWindow::swapEmuBuffer() { if (usingGL) { - screen.getGLContext()->SwapBuffers(); + screen->getGLContext()->SwapBuffers(); } else { Helpers::panic("[Qt] Don't know how to swap buffers for the current rendering backend :("); } @@ -360,6 +364,15 @@ void MainWindow::dispatchMessage(const EmulatorMessage& message) { emu->getRenderer()->setUbershader(*message.string.str); delete message.string.str; break; + + case MessageType::SetScreenSize: { + const u32 width = message.screenSize.width; + const u32 height = message.screenSize.height; + + emu->setOutputSize(width, height); + screen->resizeSurface(width, height); + break; + } } } @@ -423,13 +436,13 @@ void MainWindow::keyReleaseEvent(QKeyEvent* event) { void MainWindow::mousePressEvent(QMouseEvent* event) { if (event->button() == Qt::MouseButton::LeftButton) { const QPointF clickPos = event->globalPosition(); - const QPointF widgetPos = screen.mapFromGlobal(clickPos); + const QPointF widgetPos = screen->mapFromGlobal(clickPos); // Press is inside the screen area - if (widgetPos.x() >= 0 && widgetPos.x() < screen.width() && widgetPos.y() >= 0 && widgetPos.y() < screen.height()) { + if (widgetPos.x() >= 0 && widgetPos.x() < screen->width() && widgetPos.y() >= 0 && widgetPos.y() < screen->height()) { // Go from widget positions to [0, 400) for x and [0, 480) for y - uint x = (uint)std::round(widgetPos.x() / screen.width() * 400.f); - uint y = (uint)std::round(widgetPos.y() / screen.height() * 480.f); + uint x = (uint)std::round(widgetPos.x() / screen->width() * 400.f); + uint y = (uint)std::round(widgetPos.y() / screen->height() * 480.f); // Check if touch falls in the touch screen area if (y >= 240 && y <= 480 && x >= 40 && x < 40 + 320) { @@ -482,6 +495,14 @@ void MainWindow::editCheat(u32 handle, const std::vector& cheat, const sendMessage(message); } +void MainWindow::handleScreenResize(u32 width, u32 height) { + EmulatorMessage message{.type = MessageType::SetScreenSize}; + message.screenSize.width = width; + message.screenSize.height = height; + + sendMessage(message); +} + void MainWindow::initControllers() { // Make SDL use consistent positional button mapping SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0"); diff --git a/src/panda_qt/screen.cpp b/src/panda_qt/screen.cpp index 5a254e79..25ff576c 100644 --- a/src/panda_qt/screen.cpp +++ b/src/panda_qt/screen.cpp @@ -18,7 +18,7 @@ // and https://github.com/melonDS-emu/melonDS/blob/master/src/frontend/qt_sdl/main.cpp #ifdef PANDA3DS_ENABLE_OPENGL -ScreenWidget::ScreenWidget(QWidget* parent) : QWidget(parent) { +ScreenWidget::ScreenWidget(ResizeCallback resizeCallback, QWidget* parent) : QWidget(parent), resizeCallback(resizeCallback) { // Create a native window for use with our graphics API of choice resize(800, 240 * 4); @@ -35,6 +35,30 @@ ScreenWidget::ScreenWidget(QWidget* parent) : QWidget(parent) { } } +void ScreenWidget::resizeEvent(QResizeEvent* event) { + previousWidth = surfaceWidth; + previousHeight = surfaceHeight; + QWidget::resizeEvent(event); + + // Update surfaceWidth/surfaceHeight following the resize + std::optional windowInfo = getWindowInfo(); + if (windowInfo) { + this->windowInfo = *windowInfo; + } + + // This will call take care of calling resizeSurface from the emulator thread + resizeCallback(surfaceWidth, surfaceHeight); +} + +// Note: This will run on the emulator thread, we don't want any Qt calls happening there. +void ScreenWidget::resizeSurface(u32 width, u32 height) { + if (previousWidth != width || previousHeight != height) { + if (glContext) { + glContext->ResizeSurface(width, height); + } + } +} + bool ScreenWidget::createGLContext() { // List of GL context versions we will try. Anything 4.1+ is good static constexpr std::array versionsToTry = { @@ -45,6 +69,8 @@ bool ScreenWidget::createGLContext() { std::optional windowInfo = getWindowInfo(); if (windowInfo.has_value()) { + this->windowInfo = *windowInfo; + glContext = GL::Context::Create(*getWindowInfo(), versionsToTry); glContext->DoneCurrent(); } @@ -110,4 +136,4 @@ std::optional ScreenWidget::getWindowInfo() { return wi; } -#endif \ No newline at end of file +#endif From be75fa43a3b440510292831865339a4c582d1418 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 24 Jul 2024 02:00:45 +0300 Subject: [PATCH 142/251] More shader->GLSL recompiler work --- include/PICA/shader.hpp | 5 + include/PICA/shader_decompiler.hpp | 10 ++ src/core/PICA/shader_decompiler.cpp | 167 +++++++++++++++++++++++++++- 3 files changed, 176 insertions(+), 6 deletions(-) diff --git a/include/PICA/shader.hpp b/include/PICA/shader.hpp index 44ca2a15..68b16de8 100644 --- a/include/PICA/shader.hpp +++ b/include/PICA/shader.hpp @@ -58,6 +58,10 @@ namespace ShaderOpcodes { }; } +namespace PICA::ShaderGen { + class ShaderDecompiler; +}; + // Note: All PICA f24 vec4 registers must have the alignas(16) specifier to make them easier to access in SSE/NEON code in the JIT class PICAShader { using f24 = Floats::f24; @@ -135,6 +139,7 @@ class PICAShader { // Add these as friend classes for the JIT so it has access to all important state friend class ShaderJIT; friend class ShaderEmitter; + friend class PICA::ShaderGen::ShaderDecompiler; vec4f getSource(u32 source); vec4f& getDest(u32 dest); diff --git a/include/PICA/shader_decompiler.hpp b/include/PICA/shader_decompiler.hpp index cbc569ae..1253226f 100644 --- a/include/PICA/shader_decompiler.hpp +++ b/include/PICA/shader_decompiler.hpp @@ -101,6 +101,16 @@ namespace PICA::ShaderGen { void writeAttributes(); + std::string getSource(u32 source, u32 index) const; + std::string getDest(u32 dest) const; + std::string getSwizzlePattern(u32 swizzle) const; + std::string getDestSwizzle(u32 destinationMask) const; + + void setDest(u32 operandDescriptor, const std::string& dest, const std::string& value); + // Returns if the instruction uses the typical register encodings most instructions use + // With some exceptions like MAD/MADI, and the control flow instructions which are completely different + bool usesCommonEncoding(u32 instruction) const; + public: ShaderDecompiler(PICAShader& shader, EmulatorConfig& config, u32 entrypoint, API api, Language language) : shader(shader), entrypoint(entrypoint), config(config), api(api), language(language), decompiledShader("") {} diff --git a/src/core/PICA/shader_decompiler.cpp b/src/core/PICA/shader_decompiler.cpp index 91b07574..bdbef8f3 100644 --- a/src/core/PICA/shader_decompiler.cpp +++ b/src/core/PICA/shader_decompiler.cpp @@ -4,6 +4,8 @@ using namespace PICA; using namespace PICA::ShaderGen; +using namespace Helpers; + using Function = ControlFlow::Function; using ExitMode = Function::ExitMode; @@ -70,11 +72,16 @@ const Function* ShaderDecompiler::findFunction(const AddressRange& range) { void ShaderDecompiler::writeAttributes() { decompiledShader += R"( + layout(location = 0) in vec4 inputs[8]; + layout(std140) uniform PICAShaderUniforms { vec4 uniform_float[96]; uvec4 uniform_int; uint uniform_bool; }; + + vec4 temp_registers[16]; + vec4 dummy_vec = vec4(0.0); )"; decompiledShader += "\n"; @@ -130,24 +137,172 @@ std::string ShaderDecompiler::decompile() { return decompiledShader; } +std::string ShaderDecompiler::getSource(u32 source, [[maybe_unused]] u32 index) const { + if (source < 0x10) { + return "inputs[" + std::to_string(source) + "]"; + } else if (source < 0x20) { + return "temp_registers[" + std::to_string(source - 0x10) + "]"; + } else { + const usize floatIndex = (source - 0x20) & 0x7f; + + if (floatIndex >= 96) [[unlikely]] { + return "dummy_vec"; + } + return "uniform_float[" + std::to_string(floatIndex) + "]"; + } +} + +std::string ShaderDecompiler::getDest(u32 dest) const { + if (dest < 0x10) { + return "output_registers[" + std::to_string(dest) + "]"; + } else if (dest < 0x20) { + return "temp_registers[" + std::to_string(dest - 0x10) + "]"; + } else { + return "dummy_vec"; + } +} + +std::string ShaderDecompiler::getSwizzlePattern(u32 swizzle) const { + static constexpr std::array names = {'x', 'y', 'z', 'w'}; + std::string ret(". "); + + for (int i = 0; i < 4; i++) { + ret[3 - i + 1] = names[swizzle & 0x3]; + swizzle >>= 2; + } + + return ret; +} + +std::string ShaderDecompiler::getDestSwizzle(u32 destinationMask) const { + std::string ret = "."; + + if (destinationMask & 0b1000) { + ret += "x"; + } + + if (destinationMask & 0b100) { + ret += "y"; + } + + if (destinationMask & 0b10) { + ret += "z"; + } + + if (destinationMask & 0b1) { + ret += "w"; + } + + return ret; +} + +void ShaderDecompiler::setDest(u32 operandDescriptor, const std::string& dest, const std::string& value) { + u32 destinationMask = operandDescriptor & 0xF; + + std::string destSwizzle = getDestSwizzle(destinationMask); + // We subtract 1 for the "." character of the swizzle descriptor + u32 writtenLaneCount = destSwizzle.size() - 1; + + // All lanes are masked out, so the operation is a nop. + if (writtenLaneCount == 0) { + return; + } + + decompiledShader += dest + destSwizzle + " = "; + if (writtenLaneCount == 1) { + decompiledShader += "float(" + value + ");\n"; + } else { + decompiledShader += "vec" + std::to_string(writtenLaneCount) + "(" + value + ");\n"; + } +} + void ShaderDecompiler::compileInstruction(u32& pc, bool& finished) { const u32 instruction = shader.loadedShader[pc]; const u32 opcode = instruction >> 26; - switch (opcode) { - case ShaderOpcodes::DP4: decompiledShader += "dp4\n"; break; - case ShaderOpcodes::MOV: decompiledShader += "mov\n"; break; - case ShaderOpcodes::END: finished = true; return; - default: Helpers::warn("GLSL recompiler: Unknown opcode: %X", opcode); break; + if (usesCommonEncoding(instruction)) { + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x7f]; + const bool invertSources = (opcode == ShaderOpcodes::SLTI || opcode == ShaderOpcodes::SGEI || opcode == ShaderOpcodes::DPHI); + + // src1 and src2 indexes depend on whether this is one of the inverting instructions or not + const u32 src1Index = invertSources ? getBits<14, 5>(instruction) : getBits<12, 7>(instruction); + const u32 src2Index = invertSources ? getBits<7, 7>(instruction) : getBits<7, 5>(instruction); + + const u32 idx = getBits<19, 2>(instruction); + const u32 destIndex = getBits<21, 5>(instruction); + + const bool negate1 = (getBit<4>(operandDescriptor)) != 0; + const u32 swizzle1 = getBits<5, 8>(operandDescriptor); + const bool negate2 = (getBit<13>(operandDescriptor)) != 0; + const u32 swizzle2 = getBits<14, 8>(operandDescriptor); + + std::string src1 = negate1 ? "-" : ""; + src1 += getSource(src1Index, invertSources ? 0 : idx); + src1 += getSwizzlePattern(swizzle1); + + std::string src2 = negate2 ? "-" : ""; + src2 += getSource(src2Index, invertSources ? idx : 0); + src2 += getSwizzlePattern(swizzle2); + + std::string dest = getDest(destIndex); + + if (idx != 0) { + Helpers::panic("GLSL recompiler: Indexed instruction"); + } + + if (invertSources) { + Helpers::panic("GLSL recompiler: Inverted instruction"); + } + + switch (opcode) { + case ShaderOpcodes::DP4: setDest(operandDescriptor, dest, "vec4(dot(" + src1 + ", " + src2 + "))"); break; + case ShaderOpcodes::MOV: setDest(operandDescriptor, dest, src1); break; + default: Helpers::panic("GLSL recompiler: Unknown common opcode: %X", opcode); break; + } + } else { + switch (opcode) { + case ShaderOpcodes::END: finished = true; return; + default: Helpers::panic("GLSL recompiler: Unknown opcode: %X", opcode); break; + } } pc++; } + +bool ShaderDecompiler::usesCommonEncoding(u32 instruction) const { + const u32 opcode = instruction >> 26; + switch (opcode) { + case ShaderOpcodes::ADD: + case ShaderOpcodes::CMP1: + case ShaderOpcodes::CMP2: + case ShaderOpcodes::MUL: + case ShaderOpcodes::MIN: + case ShaderOpcodes::MAX: + case ShaderOpcodes::FLR: + case ShaderOpcodes::DP3: + case ShaderOpcodes::DP4: + case ShaderOpcodes::DPH: + case ShaderOpcodes::DPHI: + case ShaderOpcodes::LG2: + case ShaderOpcodes::EX2: + case ShaderOpcodes::RCP: + case ShaderOpcodes::RSQ: + case ShaderOpcodes::MOV: + case ShaderOpcodes::MOVA: + case ShaderOpcodes::SLT: + case ShaderOpcodes::SLTI: + case ShaderOpcodes::SGE: + case ShaderOpcodes::SGEI: return true; + + default: return false; + } +} + void ShaderDecompiler::callFunction(const Function& function) { decompiledShader += function.getCallStatement() + ";\n"; } std::string ShaderGen::decompileShader(PICAShader& shader, EmulatorConfig& config, u32 entrypoint, API api, Language language) { ShaderDecompiler decompiler(shader, config, entrypoint, api, language); return decompiler.decompile(); -} \ No newline at end of file +} From 156c3031a24d264f7d6d7653c986d5d80f452bf0 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 24 Jul 2024 16:47:46 +0300 Subject: [PATCH 143/251] More instructions in shader decompiler --- src/core/PICA/shader_decompiler.cpp | 48 ++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/src/core/PICA/shader_decompiler.cpp b/src/core/PICA/shader_decompiler.cpp index bdbef8f3..482aa36c 100644 --- a/src/core/PICA/shader_decompiler.cpp +++ b/src/core/PICA/shader_decompiler.cpp @@ -255,10 +255,56 @@ void ShaderDecompiler::compileInstruction(u32& pc, bool& finished) { } switch (opcode) { - case ShaderOpcodes::DP4: setDest(operandDescriptor, dest, "vec4(dot(" + src1 + ", " + src2 + "))"); break; case ShaderOpcodes::MOV: setDest(operandDescriptor, dest, src1); break; + case ShaderOpcodes::ADD: setDest(operandDescriptor, dest, src1 + " + " + src2); break; + case ShaderOpcodes::MUL: setDest(operandDescriptor, dest, src1 + " * " + src2); break; + case ShaderOpcodes::MAX: setDest(operandDescriptor, dest, "max(" + src1 + ", " + src2 + ")"); break; + case ShaderOpcodes::MIN: setDest(operandDescriptor, dest, "min(" + src1 + ", " + src2 + ")"); break; + + case ShaderOpcodes::DP3: setDest(operandDescriptor, dest, "vec4(dot(" + src1 + ".xyz, " + src2 + ".xyz))"); break; + case ShaderOpcodes::DP4: setDest(operandDescriptor, dest, "vec4(dot(" + src1 + ", " + src2 + "))"); break; + case ShaderOpcodes::RSQ: setDest(operandDescriptor, dest, "vec4(inversesqrt(" + src1 + ".x))"); break; + default: Helpers::panic("GLSL recompiler: Unknown common opcode: %X", opcode); break; } + } else if (opcode >= 0x30 && opcode <= 0x3F) { // MAD and MADI + const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x1f]; + const bool isMADI = getBit<29>(instruction) == 0; // We detect MADI based on bit 29 of the instruction + + // src1 and src2 indexes depend on whether this is one of the inverting instructions or not + const u32 src1Index = getBits<17, 5>(instruction); + const u32 src2Index = isMADI ? getBits<12, 5>(instruction) : getBits<10, 7>(instruction); + const u32 src3Index = isMADI ? getBits<5, 7>(instruction) : getBits<5, 5>(instruction); + const u32 idx = getBits<22, 2>(instruction); + const u32 destIndex = getBits<24, 5>(instruction); + + const bool negate1 = (getBit<4>(operandDescriptor)) != 0; + const u32 swizzle1 = getBits<5, 8>(operandDescriptor); + const bool negate2 = (getBit<13>(operandDescriptor)) != 0; + const u32 swizzle2 = getBits<14, 8>(operandDescriptor); + + const bool negate3 = (getBit<22>(operandDescriptor)) != 0; + const u32 swizzle3 = getBits<23, 8>(operandDescriptor); + + std::string src1 = negate1 ? "-" : ""; + src1 += getSource(src1Index, 0); + src1 += getSwizzlePattern(swizzle1); + + std::string src2 = negate2 ? "-" : ""; + src2 += getSource(src2Index, isMADI ? 0 : idx); + src2 += getSwizzlePattern(swizzle2); + + std::string src3 = negate3 ? "-" : ""; + src3 += getSource(src3Index, isMADI ? idx : 0); + src3 += getSwizzlePattern(swizzle3); + + std::string dest = getDest(destIndex); + + if (idx != 0) { + Helpers::panic("GLSL recompiler: Indexed instruction"); + } + + setDest(operandDescriptor, dest, src1 + " * " + src2 + " + " + src3); } else { switch (opcode) { case ShaderOpcodes::END: finished = true; return; From df5d14e3d8e0b5f618a0c65eefbee7533ad0baf4 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 25 Jul 2024 03:59:01 +0300 Subject: [PATCH 144/251] Shadergen: Remove unused vertex shader code --- src/core/PICA/shader_gen_glsl.cpp | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 9802be90..1db239f9 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -72,11 +72,6 @@ std::string FragmentGenerator::getDefaultVertexShader() { out float gl_ClipDistance[2]; #endif - vec4 abgr8888ToVec4(uint abgr) { - const float scale = 1.0 / 255.0; - return scale * vec4(float(abgr & 0xffu), float((abgr >> 8) & 0xffu), float((abgr >> 16) & 0xffu), float(abgr >> 24)); - } - void main() { gl_Position = a_coords; vec4 colourAbs = abs(a_vertexColour); @@ -677,4 +672,4 @@ void FragmentGenerator::compileFog(std::string& shader, const PICA::FragmentConf shader += "vec2 value = texelFetch(u_tex_luts, ivec2(int(clamped_index), 24), 0).rg;"; // fog LUT is past the light LUTs shader += "float fog_factor = clamp(value.r + value.g * delta, 0.0, 1.0);"; shader += "combinerOutput.rgb = mix(fog_color, combinerOutput.rgb, fog_factor);"; -} \ No newline at end of file +} From 19b69bbdc23a58a97faf88f0fff591b958dffef4 Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Thu, 25 Jul 2024 11:04:57 +0300 Subject: [PATCH 145/251] Qt: Stop emuThread on closeEvent --- include/panda_qt/main_window.hpp | 1 + src/panda_qt/main_window.cpp | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index c99fb4c2..ecdbc02e 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -139,6 +139,7 @@ class MainWindow : public QMainWindow { MainWindow(QApplication* app, QWidget* parent = nullptr); ~MainWindow(); + void closeEvent(QCloseEvent *event) override; void keyPressEvent(QKeyEvent* event) override; void keyReleaseEvent(QKeyEvent* event) override; void mousePressEvent(QMouseEvent* event) override; diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 1f9b8123..284e88ea 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -204,14 +204,19 @@ void MainWindow::selectLuaFile() { } } -// Cleanup when the main window closes -MainWindow::~MainWindow() { +// Stop emulator thread when the main window closes +void MainWindow::closeEvent(QCloseEvent *event) { appRunning = false; // Set our running atomic to false in order to make the emulator thread stop, and join it if (emuThread.joinable()) { emuThread.join(); } + SDL_Quit(); +} + +// Cleanup when the main window closes +MainWindow::~MainWindow() { delete emu; delete menuBar; delete aboutWindow; From da23ec1a0683e2317a247a2eb3e5577a1d10c0b5 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 25 Jul 2024 13:40:01 +0300 Subject: [PATCH 146/251] Don't deinit SDL from non-SDL thread --- src/panda_qt/main_window.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 284e88ea..65769116 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -211,8 +211,6 @@ void MainWindow::closeEvent(QCloseEvent *event) { if (emuThread.joinable()) { emuThread.join(); } - - SDL_Quit(); } // Cleanup when the main window closes @@ -602,4 +600,4 @@ void MainWindow::pollControllers() { } } } -} \ No newline at end of file +} From a0e506affc563ee54fa3c5658e8ccf30389aa574 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 25 Jul 2024 19:51:29 +0300 Subject: [PATCH 147/251] Share fragment UBO between shadergen programs --- include/renderer_gl/renderer_gl.hpp | 2 +- src/core/renderer_gl/renderer_gl.cpp | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index f5a964a3..42b8bba1 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -69,11 +69,11 @@ class RendererGL final : public Renderer { // The "default" vertex shader to use when using specialized shaders but not PICA vertex shader -> GLSL recompilation // We can compile this once and then link it with all other generated fragment shaders OpenGL::Shader defaultShadergenVs; + GLuint shadergenFragmentUBO; // Cached recompiled fragment shader struct CachedProgram { OpenGL::Program program; - uint uboBinding; }; std::unordered_map shaderCache; diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 8b614d2d..c513a186 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -77,6 +77,11 @@ void RendererGL::initGraphicsContextInternal() { gl.useProgram(displayProgram); glUniform1i(OpenGL::uniformLocation(displayProgram, "u_texture"), 0); // Init sampler object + // Allocate memory for the shadergen fragment uniform UBO + glGenBuffers(1, &shadergenFragmentUBO); + gl.bindUBO(shadergenFragmentUBO); + glBufferData(GL_UNIFORM_BUFFER, sizeof(PICA::FragmentUniforms), nullptr, GL_DYNAMIC_DRAW); + vbo.createFixedSize(sizeof(Vertex) * vertexBufferSize, GL_STREAM_DRAW); gl.bindVBO(vbo); vao.create(); @@ -853,17 +858,12 @@ OpenGL::Program& RendererGL::getSpecializedShader() { glUniform1i(OpenGL::uniformLocation(program, "u_tex2"), 2); glUniform1i(OpenGL::uniformLocation(program, "u_tex_luts"), 3); - // Allocate memory for the program UBO - glGenBuffers(1, &programEntry.uboBinding); - gl.bindUBO(programEntry.uboBinding); - glBufferData(GL_UNIFORM_BUFFER, sizeof(PICA::FragmentUniforms), nullptr, GL_DYNAMIC_DRAW); - // Set up the binding for our UBO. Sadly we can't specify it in the shader like normal people, // As it's an OpenGL 4.2 feature that MacOS doesn't support... uint uboIndex = glGetUniformBlockIndex(program.handle(), "FragmentUniforms"); glUniformBlockBinding(program.handle(), uboIndex, uboBlockBinding); } - glBindBufferBase(GL_UNIFORM_BUFFER, uboBlockBinding, programEntry.uboBinding); + glBindBufferBase(GL_UNIFORM_BUFFER, uboBlockBinding, shadergenFragmentUBO); // Upload uniform data to our shader's UBO PICA::FragmentUniforms uniforms; @@ -945,7 +945,7 @@ OpenGL::Program& RendererGL::getSpecializedShader() { } } - gl.bindUBO(programEntry.uboBinding); + gl.bindUBO(shadergenFragmentUBO); glBufferSubData(GL_UNIFORM_BUFFER, 0, sizeof(PICA::FragmentUniforms), &uniforms); return program; @@ -980,7 +980,6 @@ void RendererGL::clearShaderCache() { for (auto& shader : shaderCache) { CachedProgram& cachedProgram = shader.second; cachedProgram.program.free(); - glDeleteBuffers(1, &cachedProgram.uboBinding); } shaderCache.clear(); From 33cb3d9c9fdb29b28b639abe5badd64cd76c4b2e Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 25 Jul 2024 20:05:23 +0300 Subject: [PATCH 148/251] SDL: Add window resizing --- src/panda_sdl/frontend_sdl.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 0c78eea1..3c7ccc1d 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -35,7 +35,7 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, config.rendererType == RendererType::Software ? 3 : 4); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, config.rendererType == RendererType::Software ? 3 : 1); - window = SDL_CreateWindow("Alber", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 400, 480, SDL_WINDOW_OPENGL); + window = SDL_CreateWindow("Alber", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 400, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); if (window == nullptr) { Helpers::panic("Window creation failed: %s", SDL_GetError()); @@ -55,7 +55,7 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp #ifdef PANDA3DS_ENABLE_VULKAN if (config.rendererType == RendererType::Vulkan) { - window = SDL_CreateWindow("Alber", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 400, 480, SDL_WINDOW_VULKAN); + window = SDL_CreateWindow("Alber", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 400, 480, SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE); if (window == nullptr) { Helpers::warn("Window creation failed: %s", SDL_GetError()); @@ -289,6 +289,15 @@ void FrontendSDL::run() { } break; } + + case SDL_WINDOWEVENT: { + auto type = event.window.event; + if (type == SDL_WINDOWEVENT_RESIZED) { + const u32 width = event.window.data1; + const u32 height = event.window.data2; + emu.setOutputSize(width, height); + } + } } } From 32ddc287891cc96355d2506b4455b267bccac3e7 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 25 Jul 2024 20:18:30 +0300 Subject: [PATCH 149/251] Shadergen: Move fog colour to uniform --- include/PICA/pica_frag_config.hpp | 6 ------ include/PICA/pica_frag_uniforms.hpp | 4 +++- src/core/PICA/shader_gen_glsl.cpp | 7 ++----- src/core/renderer_gl/renderer_gl.cpp | 2 ++ 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index 337fd211..5d5f8420 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -35,9 +35,6 @@ namespace PICA { BitField<0, 3, FogMode> mode; BitField<3, 1, u32> flipDepth; - BitField<8, 8, u32> fogColorR; - BitField<16, 8, u32> fogColorG; - BitField<24, 8, u32> fogColorB; }; }; @@ -238,9 +235,6 @@ namespace PICA { if (fogConfig.mode == FogMode::Fog) { fogConfig.flipDepth = Helpers::getBit<16>(regs[InternalRegs::TexEnvUpdateBuffer]); - fogConfig.fogColorR = Helpers::getBits<0, 8>(regs[InternalRegs::FogColor]); - fogConfig.fogColorG = Helpers::getBits<8, 8>(regs[InternalRegs::FogColor]); - fogConfig.fogColorB = Helpers::getBits<16, 8>(regs[InternalRegs::FogColor]); } } }; diff --git a/include/PICA/pica_frag_uniforms.hpp b/include/PICA/pica_frag_uniforms.hpp index 09722d61..781fdcd3 100644 --- a/include/PICA/pica_frag_uniforms.hpp +++ b/include/PICA/pica_frag_uniforms.hpp @@ -34,8 +34,10 @@ namespace PICA { alignas(16) vec4 tevBufferColor; alignas(16) vec4 clipCoords; - // Note: We upload this as a u32 and decode on GPU + // Note: We upload these as a u32 and decode on GPU. + // Particularly the fog colour since fog is really uncommon and it doesn't matter if we decode on GPU. u32 globalAmbientLight; + u32 fogColor; // NOTE: THIS MUST BE LAST so that if lighting is disabled we can potentially omit uploading it LightUniform lightUniforms[8]; }; diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 1db239f9..41b33d88 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -27,6 +27,7 @@ static constexpr const char* uniformDefinition = R"( // Note: We upload this as a u32 and decode on GPU uint globalAmbientLight; + uint inFogColor; LightSource lightSources[8]; }; )"; @@ -656,10 +657,6 @@ void FragmentGenerator::compileFog(std::string& shader, const PICA::FragmentConf return; } - float r = config.fogConfig.fogColorR / 255.0f; - float g = config.fogConfig.fogColorG / 255.0f; - float b = config.fogConfig.fogColorB / 255.0f; - if (config.fogConfig.flipDepth) { shader += "float fog_index = (1.0 - depth) * 128.0;\n"; } else { @@ -668,7 +665,7 @@ void FragmentGenerator::compileFog(std::string& shader, const PICA::FragmentConf shader += "float clamped_index = clamp(floor(fog_index), 0.0, 127.0);"; shader += "float delta = fog_index - clamped_index;"; - shader += "vec3 fog_color = vec3(" + std::to_string(r) + ", " + std::to_string(g) + ", " + std::to_string(b) + ");"; + shader += "vec3 fog_color = (1.0 / 255.0) * vec3(float(inFogColor & 0xffu), float((inFogColor >> 8u) & 0xffu), float((inFogColor >> 16u) & 0xffu));"; shader += "vec2 value = texelFetch(u_tex_luts, ivec2(int(clamped_index), 24), 0).rg;"; // fog LUT is past the light LUTs shader += "float fog_factor = clamp(value.r + value.g * delta, 0.0, 1.0);"; shader += "combinerOutput.rgb = mix(fog_color, combinerOutput.rgb, fog_factor);"; diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index c513a186..f8fc31e7 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -903,6 +903,8 @@ OpenGL::Program& RendererGL::getSpecializedShader() { vec[3] = float((color >> 24) & 0xFF) / 255.0f; } + uniforms.fogColor = regs[PICA::InternalRegs::FogColor]; + // Append lighting uniforms if (fsConfig.lighting.enable) { uniforms.globalAmbientLight = regs[InternalRegs::LightGlobalAmbient]; From 1413cdaebfb3b9be814b5b5b05616677e53bdef3 Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Fri, 26 Jul 2024 09:41:48 +0300 Subject: [PATCH 150/251] SDL: Fix mouse coords --- src/panda_sdl/frontend_sdl.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 3c7ccc1d..b503dc42 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -162,8 +162,13 @@ void FrontendSDL::run() { if (emu.romType == ROMType::None) break; if (event.button.button == SDL_BUTTON_LEFT) { - const s32 x = event.button.x; - const s32 y = event.button.y; + // Get current window dimensions + int windowWidth, windowHeight; + SDL_GetWindowSize(window, &windowWidth, &windowHeight); + + // Go from window positions to [0, 400) for x and [0, 480) for y + const s32 x = (s32)std::round(event.button.x * 400.f / windowWidth); + const s32 y = (s32)std::round(event.button.y * 480.f / windowHeight); // Check if touch falls in the touch screen area if (y >= 240 && y <= 480 && x >= 40 && x < 40 + 320) { From 11e7eb7fd6c93a3a57a022573926275a1bc1f3e7 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 26 Jul 2024 12:05:33 +0300 Subject: [PATCH 151/251] SDL: Fixup touchscreen code --- CMakeLists.txt | 2 +- include/panda_sdl/frontend_sdl.hpp | 2 ++ src/panda_sdl/frontend_sdl.cpp | 12 ++++++------ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index a3fe41dd..448086ba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -502,7 +502,7 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE) ) else() set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp src/panda_sdl/mappings.cpp) - set(FRONTEND_HEADER_FILES "") + set(FRONTEND_HEADER_FILES "include/panda_sdl/frontend_sdl.hpp") endif() target_link_libraries(Alber PRIVATE AlberCore) diff --git a/include/panda_sdl/frontend_sdl.hpp b/include/panda_sdl/frontend_sdl.hpp index dd6ab6c0..07038962 100644 --- a/include/panda_sdl/frontend_sdl.hpp +++ b/include/panda_sdl/frontend_sdl.hpp @@ -23,6 +23,8 @@ class FrontendSDL { SDL_GameController* gameController = nullptr; InputMappings keyboardMappings; + u32 windowWidth = 400; + u32 windowHeight = 480; int gameControllerID; bool programRunning = true; diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index b503dc42..b2dc27b7 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -162,9 +162,9 @@ void FrontendSDL::run() { if (emu.romType == ROMType::None) break; if (event.button.button == SDL_BUTTON_LEFT) { - // Get current window dimensions - int windowWidth, windowHeight; - SDL_GetWindowSize(window, &windowWidth, &windowHeight); + if (windowWidth == 0 || windowHeight == 0) [[unlikely]] { + break; + } // Go from window positions to [0, 400) for x and [0, 480) for y const s32 x = (s32)std::round(event.button.x * 400.f / windowWidth); @@ -298,9 +298,9 @@ void FrontendSDL::run() { case SDL_WINDOWEVENT: { auto type = event.window.event; if (type == SDL_WINDOWEVENT_RESIZED) { - const u32 width = event.window.data1; - const u32 height = event.window.data2; - emu.setOutputSize(width, height); + windowWidth = event.window.data1; + windowHeight = event.window.data2; + emu.setOutputSize(windowWidth, windowHeight); } } } From 13bff602571bc9c0876bf48b7b71b113de537bee Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Fri, 26 Jul 2024 12:25:45 +0300 Subject: [PATCH 152/251] SDL: Fix mouse coords in gyroscope emulation --- src/panda_sdl/frontend_sdl.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index b2dc27b7..77b1f55f 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -247,8 +247,13 @@ void FrontendSDL::run() { // Handle "dragging" across the touchscreen if (hid.isTouchScreenPressed()) { - const s32 x = event.motion.x; - const s32 y = event.motion.y; + if (windowWidth == 0 || windowHeight == 0) [[unlikely]] { + break; + } + + // Go from window positions to [0, 400) for x and [0, 480) for y + const s32 x = (s32)std::round(event.motion.x * 400.f / windowWidth); + const s32 y = (s32)std::round(event.motion.y * 480.f / windowHeight); // Check if touch falls in the touch screen area and register the new touch screen position if (y >= 240 && y <= 480 && x >= 40 && x < 40 + 320) { From f095e6af0bd96ebfd540dd438bfdccc409dd79c5 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 26 Jul 2024 14:44:11 +0300 Subject: [PATCH 153/251] Shadergen: Move comments outside of emitted source code --- src/core/PICA/shader_gen_glsl.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 41b33d88..60887d56 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -4,6 +4,8 @@ using namespace PICA; using namespace PICA::ShaderGen; +// Note: We upload global ambient and fog colour as u32 and decode on the GPU +// This shouldn't matter much for GPU performance, especially fog since it's relatively rare static constexpr const char* uniformDefinition = R"( struct LightSource { vec3 specular0; @@ -24,8 +26,6 @@ static constexpr const char* uniformDefinition = R"( vec4 constantColors[6]; vec4 tevBufferColor; vec4 clipCoords; - - // Note: We upload this as a u32 and decode on GPU uint globalAmbientLight; uint inFogColor; LightSource lightSources[8]; From d0f13de4c5747c3082a81b9c6c01cd020886c8c1 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:25:38 +0300 Subject: [PATCH 154/251] Fix swapping loaded ELF files --- src/emulator.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/emulator.cpp b/src/emulator.cpp index db6c2e1f..921af08f 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -299,6 +299,11 @@ bool Emulator::load3DSX(const std::filesystem::path& path) { } bool Emulator::loadELF(const std::filesystem::path& path) { + // We can't open a new file with this ifstream if it's associated with a file + if (loadedELF.is_open()) { + loadedELF.close(); + } + loadedELF.open(path, std::ios_base::binary); // Open ROM in binary mode romType = ROMType::ELF; From e557bd29764f914053d3535fafb803a2974497dd Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 28 Jul 2024 19:03:05 +0300 Subject: [PATCH 155/251] Fix HLE__DSP::RecvData --- src/core/audio/hle_core.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 12c8f4c8..1f77974f 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -110,7 +110,7 @@ namespace Audio { Helpers::panic("Audio: invalid register in HLE frontend"); } - return dspState == DSPState::On; + return dspState != DSPState::On; } void HLE_DSP::writeProcessPipe(u32 channel, u32 size, u32 buffer) { From 908222f26fbbcdda2ecfc79cdf4c2eee7a823b37 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 28 Jul 2024 19:05:50 +0300 Subject: [PATCH 156/251] HLE DSP: Don't printf on buffer queue dirty --- src/core/audio/hle_core.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 1f77974f..d39bdbbf 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -317,7 +317,7 @@ namespace Audio { if (config.bufferQueueDirty) { config.bufferQueueDirty = 0; - printf("Buffer queue dirty for voice %d\n", source.index); + // printf("Buffer queue dirty for voice %d\n", source.index); } config.dirtyRaw = 0; From c7db6fe5dc39b6188e1eb7fbb61f0a0834d410c8 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 29 Jul 2024 19:54:46 +0300 Subject: [PATCH 157/251] FIx DSP region calculation --- include/audio/hle_core.hpp | 2 +- src/core/audio/hle_core.cpp | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index b59dc811..117d9ecb 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -142,7 +142,7 @@ namespace Audio { } else if (counter1 == 0xffff && counter0 != 0xfffe) { return 0; } else { - return counter0 > counter1 ? 0 : 0; + return counter0 > counter1 ? 0 : 1; } } diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index d39bdbbf..23a99786 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -216,6 +216,11 @@ namespace Audio { SharedMemory& read = readRegion(); SharedMemory& write = writeRegion(); + // TODO: Properly implement mixers + // The DSP checks the DSP configuration dirty bits on every frame, applies them, and clears them + read.dspConfiguration.dirtyRaw = 0; + // read.dspConfiguration.dirtyRaw2 = 0; + for (int i = 0; i < sourceCount; i++) { // Update source configuration from the read region of shared memory auto& config = read.sourceConfigurations.config[i]; @@ -401,6 +406,7 @@ namespace Audio { // samples.insert(samples.end(), source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); source.currentSamples.erase(source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); + source.samplePosition += sampleCount; outputCount += sampleCount; } } From 45dd69d62ae144fdac5048cdc118c0d2749c484f Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 29 Jul 2024 21:58:00 +0300 Subject: [PATCH 158/251] HLE DSP: Pop unused samples when loading new buffer --- src/core/audio/hle_core.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 23a99786..112db9d5 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include @@ -370,6 +371,13 @@ namespace Audio { break; } + // We're skipping the first samplePosition samples, so remove them from the buffer so as not to consume them later + if (source.samplePosition > 0) { + auto start = source.currentSamples.begin(); + auto end = std::next(start, source.samplePosition); + source.currentSamples.erase(start, end); + } + // If the buffer is a looping buffer, re-push it if (buffer.looping) { source.pushBuffer(buffer); From 57ecc18f325f09d629b5cf491b3f7844f927b4da Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 29 Jul 2024 23:03:17 +0300 Subject: [PATCH 159/251] HLE DSP: Implement buffer queue dirty bit --- src/core/audio/hle_core.cpp | 46 +++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 112db9d5..c01a8ccd 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -220,7 +220,7 @@ namespace Audio { // TODO: Properly implement mixers // The DSP checks the DSP configuration dirty bits on every frame, applies them, and clears them read.dspConfiguration.dirtyRaw = 0; - // read.dspConfiguration.dirtyRaw2 = 0; + read.dspConfiguration.dirtyRaw2 = 0; for (int i = 0; i < sourceCount; i++) { // Update source configuration from the read region of shared memory @@ -322,8 +322,40 @@ namespace Audio { } if (config.bufferQueueDirty) { - config.bufferQueueDirty = 0; // printf("Buffer queue dirty for voice %d\n", source.index); + + u16 dirtyBuffers = config.buffersDirty; + config.bufferQueueDirty = 0; + config.buffersDirty = 0; + + for (int i = 0; i < 4; i++) { + bool dirty = ((dirtyBuffers >> i) & 1) != 0; + if (dirty) { + const auto& buffer = config.buffers[i]; + + if (s32(buffer.length) >= 0) [[likely]] { + // TODO: Add sample format and channel count + Source::Buffer newBuffer{ + .paddr = buffer.physicalAddress, + .sampleCount = buffer.length, + .adpcmScale = u8(buffer.adpcmScale), + .previousSamples = {s16(buffer.adpcm_yn[0]), s16(buffer.adpcm_yn[1])}, + .adpcmDirty = buffer.adpcmDirty != 0, + .looping = buffer.isLooping != 0, + .bufferID = buffer.bufferID, + .playPosition = 0, + .format = source.sampleFormat, + .sourceType = source.sourceType, + .fromQueue = true, + .hasPlayedOnce = false, + }; + + source.buffers.emplace(std::move(newBuffer)); + } else { + printf("Buffer queue dirty: Invalid buffer size for DSP voice %d\n", source.index); + } + } + } } config.dirtyRaw = 0; @@ -371,17 +403,17 @@ namespace Audio { break; } + // If the buffer is a looping buffer, re-push it + if (buffer.looping) { + source.pushBuffer(buffer); + } + // We're skipping the first samplePosition samples, so remove them from the buffer so as not to consume them later if (source.samplePosition > 0) { auto start = source.currentSamples.begin(); auto end = std::next(start, source.samplePosition); source.currentSamples.erase(start, end); } - - // If the buffer is a looping buffer, re-push it - if (buffer.looping) { - source.pushBuffer(buffer); - } } void HLE_DSP::generateFrame(DSPSource& source) { From 6668ba3e37fe4b2dce6840c818d0ae78524f2242 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 29 Jul 2024 23:46:36 +0300 Subject: [PATCH 160/251] HLE DSP: Fix embedded buffer starting sample position --- src/core/audio/hle_core.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index c01a8ccd..f150482b 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -292,6 +292,9 @@ namespace Audio { } if (config.embeddedBufferDirty) { + // Annoyingly, and only for embedded buffer, whether we use config.playPosition depends on the relevant dirty bit + const u32 playPosition = config.playPositionDirty ? config.playPosition : 0; + config.embeddedBufferDirty = 0; if (s32(config.length) >= 0) [[likely]] { // TODO: Add sample format and channel count @@ -303,7 +306,7 @@ namespace Audio { .adpcmDirty = config.adpcmDirty != 0, .looping = config.isLooping != 0, .bufferID = config.bufferID, - .playPosition = config.playPosition, + .playPosition = playPosition, .format = source.sampleFormat, .sourceType = source.sourceType, .fromQueue = false, From e26f58595eb4667a07ef14fbeb41bc3a73590f58 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 30 Jul 2024 00:36:16 +0300 Subject: [PATCH 161/251] HLE DSP: Reset flags should take priority --- src/core/audio/hle_core.cpp | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index f150482b..84d62401 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -253,6 +253,17 @@ namespace Audio { return; } + // The reset flags take priority, as you can reset a source and set it up to be played again at the same time + if (config.resetFlag) { + config.resetFlag = 0; + source.reset(); + } + + if (config.partialResetFlag) { + config.partialResetFlag = 0; + source.buffers = {}; + } + if (config.enableDirty) { config.enableDirty = 0; source.enabled = config.enable != 0; @@ -272,16 +283,6 @@ namespace Audio { ); } - if (config.resetFlag) { - config.resetFlag = 0; - source.reset(); - } - - if (config.partialResetFlag) { - config.partialResetFlag = 0; - source.buffers = {}; - } - // TODO: Should we check bufferQueueDirty here too? if (config.formatDirty || config.embeddedBufferDirty) { source.sampleFormat = config.format; From f572373fc13468354c4d418faa759fdf711858dd Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:29:18 +0300 Subject: [PATCH 162/251] AES: Implement seed crypto --- include/crypto/aes_engine.hpp | 79 +++++++++++++++++------------- src/core/crypto/aes_engine.cpp | 89 +++++++++++++++++++++++++++------- src/core/loader/ncch.cpp | 34 ++++++++++--- src/emulator.cpp | 6 +++ 4 files changed, 151 insertions(+), 57 deletions(-) diff --git a/include/crypto/aes_engine.hpp b/include/crypto/aes_engine.hpp index 324f4adf..f8a2d7e4 100644 --- a/include/crypto/aes_engine.hpp +++ b/include/crypto/aes_engine.hpp @@ -1,20 +1,29 @@ #pragma once #include -#include -#include #include +#include +#include #include #include +#include #include "helpers.hpp" +#include "io_file.hpp" +#include "swap.hpp" namespace Crypto { - constexpr std::size_t AesKeySize = 0x10; + constexpr usize AesKeySize = 0x10; using AESKey = std::array; - template - static std::array rolArray(const std::array& value, std::size_t bits) { + struct Seed { + u64_le titleID; + AESKey seed; + std::array pad; + }; + + template + static std::array rolArray(const std::array& value, usize bits) { const auto bitWidth = N * CHAR_BIT; bits %= bitWidth; @@ -24,18 +33,18 @@ namespace Crypto { std::array result; - for (std::size_t i = 0; i < N; i++) { + for (usize i = 0; i < N; i++) { result[i] = ((value[(i + byteShift) % N] << bitShift) | (value[(i + byteShift + 1) % N] >> (CHAR_BIT - bitShift))) & UINT8_MAX; } return result; } - template + template static std::array addArray(const std::array& a, const std::array& b) { std::array result; - std::size_t sum = 0; - std::size_t carry = 0; + usize sum = 0; + usize carry = 0; for (std::int64_t i = N - 1; i >= 0; i--) { sum = a[i] + b[i] + carry; @@ -46,11 +55,11 @@ namespace Crypto { return result; } - template + template static std::array xorArray(const std::array& a, const std::array& b) { std::array result; - for (std::size_t i = 0; i < N; i++) { + for (usize i = 0; i < N; i++) { result[i] = a[i] ^ b[i]; } @@ -63,7 +72,7 @@ namespace Crypto { } AESKey rawKey; - for (std::size_t i = 0; i < rawKey.size(); i++) { + for (usize i = 0; i < rawKey.size(); i++) { rawKey[i] = static_cast(std::stoi(hex.substr(i * 2, 2), 0, 16)); } @@ -76,7 +85,7 @@ namespace Crypto { std::optional normalKey = std::nullopt; }; - enum KeySlotId : std::size_t { + enum KeySlotId : usize { NCCHKey0 = 0x2C, NCCHKey1 = 0x25, NCCHKey2 = 0x18, @@ -84,14 +93,18 @@ namespace Crypto { }; class AESEngine { - private: - constexpr static std::size_t AesKeySlotCount = 0x40; + private: + constexpr static usize AesKeySlotCount = 0x40; std::optional m_generator = std::nullopt; std::array m_slots; bool keysLoaded = false; - constexpr void updateNormalKey(std::size_t slotId) { + std::vector seeds; + IOFile seedDatabase; + bool seedsLoaded = false; + + constexpr void updateNormalKey(usize slotId) { if (m_generator.has_value() && hasKeyX(slotId) && hasKeyY(slotId)) { auto& keySlot = m_slots.at(slotId); AESKey keyX = keySlot.keyX.value(); @@ -101,13 +114,17 @@ namespace Crypto { } } - public: + public: AESEngine() {} void loadKeys(const std::filesystem::path& path); + void setSeedPath(const std::filesystem::path& path); + // Returns true on success, false on failure + bool loadSeeds(); + bool haveKeys() { return keysLoaded; } bool haveGenerator() { return m_generator.has_value(); } - constexpr bool hasKeyX(std::size_t slotId) { + constexpr bool hasKeyX(usize slotId) { if (slotId >= AesKeySlotCount) { return false; } @@ -115,18 +132,16 @@ namespace Crypto { return m_slots.at(slotId).keyX.has_value(); } - constexpr AESKey getKeyX(std::size_t slotId) { - return m_slots.at(slotId).keyX.value_or(AESKey{}); - } + constexpr AESKey getKeyX(usize slotId) { return m_slots.at(slotId).keyX.value_or(AESKey{}); } - constexpr void setKeyX(std::size_t slotId, const AESKey &key) { + constexpr void setKeyX(usize slotId, const AESKey& key) { if (slotId < AesKeySlotCount) { m_slots.at(slotId).keyX = key; updateNormalKey(slotId); } } - constexpr bool hasKeyY(std::size_t slotId) { + constexpr bool hasKeyY(usize slotId) { if (slotId >= AesKeySlotCount) { return false; } @@ -134,18 +149,16 @@ namespace Crypto { return m_slots.at(slotId).keyY.has_value(); } - constexpr AESKey getKeyY(std::size_t slotId) { - return m_slots.at(slotId).keyY.value_or(AESKey{}); - } + constexpr AESKey getKeyY(usize slotId) { return m_slots.at(slotId).keyY.value_or(AESKey{}); } - constexpr void setKeyY(std::size_t slotId, const AESKey &key) { + constexpr void setKeyY(usize slotId, const AESKey& key) { if (slotId < AesKeySlotCount) { m_slots.at(slotId).keyY = key; updateNormalKey(slotId); } } - constexpr bool hasNormalKey(std::size_t slotId) { + constexpr bool hasNormalKey(usize slotId) { if (slotId >= AesKeySlotCount) { return false; } @@ -153,14 +166,14 @@ namespace Crypto { return m_slots.at(slotId).normalKey.has_value(); } - constexpr AESKey getNormalKey(std::size_t slotId) { - return m_slots.at(slotId).normalKey.value_or(AESKey{}); - } + constexpr AESKey getNormalKey(usize slotId) { return m_slots.at(slotId).normalKey.value_or(AESKey{}); } - constexpr void setNormalKey(std::size_t slotId, const AESKey &key) { + constexpr void setNormalKey(usize slotId, const AESKey& key) { if (slotId < AesKeySlotCount) { m_slots.at(slotId).normalKey = key; } } + + std::optional getSeedFromDB(u64 titleID); }; -} \ No newline at end of file +} // namespace Crypto \ No newline at end of file diff --git a/src/core/crypto/aes_engine.cpp b/src/core/crypto/aes_engine.cpp index f4bf3494..dc3ae060 100644 --- a/src/core/crypto/aes_engine.cpp +++ b/src/core/crypto/aes_engine.cpp @@ -1,13 +1,15 @@ -#include -#include - #include "crypto/aes_engine.hpp" + +#include +#include +#include + #include "helpers.hpp" namespace Crypto { void AESEngine::loadKeys(const std::filesystem::path& path) { std::ifstream file(path, std::ios::in); - + if (file.fail()) { Helpers::warn("Keys: Couldn't read key file: %s", path.c_str()); return; @@ -58,18 +60,10 @@ namespace Crypto { } switch (keyType) { - case 'X': - setKeyX(slotId, key.value()); - break; - case 'Y': - setKeyY(slotId, key.value()); - break; - case 'N': - setNormalKey(slotId, key.value()); - break; - default: - Helpers::warn("Keys: Invalid key type %c", keyType); - break; + case 'X': setKeyX(slotId, key.value()); break; + case 'Y': setKeyY(slotId, key.value()); break; + case 'N': setNormalKey(slotId, key.value()); break; + default: Helpers::warn("Keys: Invalid key type %c", keyType); break; } } @@ -80,4 +74,65 @@ namespace Crypto { keysLoaded = true; } -}; \ No newline at end of file + + void AESEngine::setSeedPath(const std::filesystem::path& path) { seedDatabase.open(path, "rb"); } + + // Loads seeds from a seed file, return true on success and false on failure + bool AESEngine::loadSeeds() { + if (!seedDatabase.isOpen()) { + return false; + } + + // The # of seeds is stored at offset 0 + u32_le seedCount = 0; + + if (!seedDatabase.rewind()) { + return false; + } + + auto [success, size] = seedDatabase.readBytes(&seedCount, sizeof(u32)); + if (!success || size != sizeof(u32)) { + return false; + } + + // Key data starts from offset 16 + if (!seedDatabase.seek(16)) { + return false; + } + + Crypto::Seed seed; + for (uint i = 0; i < seedCount; i++) { + std::tie(success, size) = seedDatabase.readBytes(&seed, sizeof(seed)); + if (!success || size != sizeof(seed)) { + return false; + } + + seeds.push_back(seed); + } + + return true; + } + + std::optional AESEngine::getSeedFromDB(u64 titleID) { + // We don't have a seed db nor any seeds loaded, return nullopt + if (!seedDatabase.isOpen() && seeds.empty()) { + return std::nullopt; + } + + // We have a seed DB but haven't loaded the seeds yet, so load them + if (seedDatabase.isOpen() && seeds.empty()) { + bool success = loadSeeds(); + if (!success) { + return std::nullopt; + } + } + + for (Crypto::Seed& seed : seeds) { + if (seed.titleID == titleID) { + return seed.seed; + } + } + + return std::nullopt; + } +}; // namespace Crypto \ No newline at end of file diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index a575d4f2..3a7cb1f6 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -1,12 +1,15 @@ +#include "loader/ncch.hpp" + #include #include -#include -#include -#include "loader/lz77.hpp" -#include "loader/ncch.hpp" -#include "memory.hpp" +#include +#include #include +#include + +#include "loader/lz77.hpp" +#include "memory.hpp" bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSInfo &info) { // 0x200 bytes for the NCCH header @@ -70,8 +73,25 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn if (!seedCrypto) { secondaryKeyY = primaryKeyY; } else { - Helpers::warn("Seed crypto is not supported"); - gotCryptoKeys = false; + // In seed crypto mode, the secondary key is computed through a SHA256 hash of the primary key and a title-specific seed, which we fetch + // from seeddb.bin + std::optional seedOptional = aesEngine.getSeedFromDB(programID); + if (seedOptional.has_value()) { + auto seed = *seedOptional; + + CryptoPP::SHA256 shaEngine; + std::array data; + std::array hash; + + std::memcpy(&data[0], primaryKeyY.data(), primaryKeyY.size()); + std::memcpy(&data[16], seed.data(), seed.size()); + shaEngine.CalculateDigest(hash.data(), data.data(), data.size()); + // Note that SHA256 will produce a 256-bit hash, while we only need 128 bits cause this is an AES key + // So the latter 16 bytes of the SHA256 are thrown out. + std::memcpy(secondaryKeyY.data(), hash.data(), secondaryKeyY.size()); + } else { + Helpers::warn("Couldn't find a seed value for this title. Make sure you have a seeddb.bin file alongside your aes_keys.txt"); + } } auto primaryResult = getPrimaryKey(aesEngine, primaryKeyY); diff --git a/src/emulator.cpp b/src/emulator.cpp index 921af08f..e4bfc4af 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -220,6 +220,8 @@ bool Emulator::loadROM(const std::filesystem::path& path) { const std::filesystem::path appDataPath = getAppDataRoot(); const std::filesystem::path dataPath = appDataPath / path.filename().stem(); const std::filesystem::path aesKeysPath = appDataPath / "sysdata" / "aes_keys.txt"; + const std::filesystem::path seedDBPath = appDataPath / "sysdata" / "seeddb.bin"; + IOFile::setAppDataDir(dataPath); // Open the text file containing our AES keys if it exists. We use the std::filesystem::exists overload that takes an error code param to @@ -229,6 +231,10 @@ bool Emulator::loadROM(const std::filesystem::path& path) { aesEngine.loadKeys(aesKeysPath); } + if (std::filesystem::exists(seedDBPath, ec) && !ec) { + aesEngine.setSeedPath(seedDBPath); + } + kernel.initializeFS(); auto extension = path.extension(); bool success; // Tracks if we loaded the ROM successfully From e6c97edb1c41a5bca22aeaa8befe226ba1c511bb Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:32:42 +0300 Subject: [PATCH 163/251] AES: Remove unused seedsLoaded variable --- include/crypto/aes_engine.hpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/include/crypto/aes_engine.hpp b/include/crypto/aes_engine.hpp index f8a2d7e4..c96b36d3 100644 --- a/include/crypto/aes_engine.hpp +++ b/include/crypto/aes_engine.hpp @@ -102,7 +102,6 @@ namespace Crypto { std::vector seeds; IOFile seedDatabase; - bool seedsLoaded = false; constexpr void updateNormalKey(usize slotId) { if (m_generator.has_value() && hasKeyX(slotId) && hasKeyY(slotId)) { @@ -176,4 +175,4 @@ namespace Crypto { std::optional getSeedFromDB(u64 titleID); }; -} // namespace Crypto \ No newline at end of file +} // namespace Crypto From bec63c43a169204f2f022d535c8d36bdfb7c5565 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 30 Jul 2024 14:36:39 +0300 Subject: [PATCH 164/251] AES: Properly handle missing seeds --- src/core/loader/ncch.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/loader/ncch.cpp b/src/core/loader/ncch.cpp index 3a7cb1f6..96d13813 100644 --- a/src/core/loader/ncch.cpp +++ b/src/core/loader/ncch.cpp @@ -91,6 +91,7 @@ bool NCCH::loadFromHeader(Crypto::AESEngine &aesEngine, IOFile& file, const FSIn std::memcpy(secondaryKeyY.data(), hash.data(), secondaryKeyY.size()); } else { Helpers::warn("Couldn't find a seed value for this title. Make sure you have a seeddb.bin file alongside your aes_keys.txt"); + gotCryptoKeys = false; } } From e666afd1a30d80f69df1ea015ed835ac86af1eef Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 31 Jul 2024 02:51:40 +0300 Subject: [PATCH 165/251] DSP HLE: Fix buffer queue metadata --- include/audio/hle_core.hpp | 2 +- src/core/audio/hle_core.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index 117d9ecb..35c1c1b8 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -142,7 +142,7 @@ namespace Audio { } else if (counter1 == 0xffff && counter0 != 0xfffe) { return 0; } else { - return counter0 > counter1 ? 0 : 1; + return (counter0 > counter1) ? 0 : 1; } } diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 84d62401..ffab9301 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -342,7 +342,7 @@ namespace Audio { Source::Buffer newBuffer{ .paddr = buffer.physicalAddress, .sampleCount = buffer.length, - .adpcmScale = u8(buffer.adpcmScale), + .adpcmScale = u8(buffer.adpcm_ps), .previousSamples = {s16(buffer.adpcm_yn[0]), s16(buffer.adpcm_yn[1])}, .adpcmDirty = buffer.adpcmDirty != 0, .looping = buffer.isLooping != 0, From d1922798c5978cafb992f074f823a051ed0dc419 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:22:14 +0300 Subject: [PATCH 166/251] CMake: Disable /GS when using MSVC for user builds --- CMakeLists.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 448086ba..b55e2390 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,11 @@ if(BUILD_LIBRETRO_CORE) add_compile_definitions(__LIBRETRO__) endif() +if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND ENABLE_USER_BUILD) + # Disable stack buffer overflow checks in user builds + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /GS-") +endif() + add_library(AlberCore STATIC) include_directories(${PROJECT_SOURCE_DIR}/include/) From 195f3388e9a5f88e18ee2779c71069c60668f544 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 2 Aug 2024 22:53:51 +0300 Subject: [PATCH 167/251] PICA: Add LITP test + interpreter implementation --- include/PICA/shader.hpp | 3 +- src/core/PICA/shader_interpreter.cpp | 32 ++++ tests/PICA_LITP/Makefile | 255 ++++++++++++++++++++++++++ tests/PICA_LITP/source/main.c | 128 +++++++++++++ tests/PICA_LITP/source/vshader.v.pica | 73 ++++++++ 5 files changed, 490 insertions(+), 1 deletion(-) create mode 100644 tests/PICA_LITP/Makefile create mode 100644 tests/PICA_LITP/source/main.c create mode 100644 tests/PICA_LITP/source/vshader.v.pica diff --git a/include/PICA/shader.hpp b/include/PICA/shader.hpp index 68b16de8..e5f57c72 100644 --- a/include/PICA/shader.hpp +++ b/include/PICA/shader.hpp @@ -23,7 +23,7 @@ namespace ShaderOpcodes { DST = 0x04, EX2 = 0x05, LG2 = 0x06, - LIT = 0x07, + LITP = 0x07, MUL = 0x08, SGE = 0x09, SLT = 0x0A, @@ -161,6 +161,7 @@ class PICAShader { void jmpc(u32 instruction); void jmpu(u32 instruction); void lg2(u32 instruction); + void litp(u32 instruction); void loop(u32 instruction); void mad(u32 instruction); void madi(u32 instruction); diff --git a/src/core/PICA/shader_interpreter.cpp b/src/core/PICA/shader_interpreter.cpp index 003ef97a..a85c7464 100644 --- a/src/core/PICA/shader_interpreter.cpp +++ b/src/core/PICA/shader_interpreter.cpp @@ -74,6 +74,9 @@ void PICAShader::run() { break; } + // Undocumented, implementation based on 3DBrew and hw testing (see tests/PICA_LITP) + case ShaderOpcodes::LITP: [[unlikely]] litp(instruction); break; + default: Helpers::panic("Unimplemented PICA instruction %08X (Opcode = %02X)", instruction, opcode); } @@ -753,4 +756,33 @@ void PICAShader::jmpu(u32 instruction) { if (((boolUniform >> bit) & 1) == test) // Jump if the bool uniform is the value we want pc = dest; +} + +void PICAShader::litp(u32 instruction) { + const u32 operandDescriptor = operandDescriptors[instruction & 0x7f]; + u32 src = getBits<12, 7>(instruction); + const u32 idx = getBits<19, 2>(instruction); + const u32 dest = getBits<21, 5>(instruction); + + src = getIndexedSource(src, idx); + vec4f srcVec = getSourceSwizzled<1>(src, operandDescriptor); + vec4f& destVector = getDest(dest); + + // Compare registers are set based on whether src.x and src.w are >= 0.0 + cmpRegister[0] = (srcVec[0].toFloat32() >= 0.0f); + cmpRegister[1] = (srcVec[3].toFloat32() >= 0.0f); + + vec4f result; + // TODO: Does max here have the same non-IEEE NaN behavior as the max instruction? + result[0] = f24::fromFloat32(std::max(srcVec[0].toFloat32(), 0.0f)); + result[1] = f24::fromFloat32(std::clamp(srcVec[1].toFloat32(), -127.9961f, 127.9961f)); + result[2] = f24::zero(); + result[3] = f24::fromFloat32(std::max(srcVec[3].toFloat32(), 0.0f)); + + u32 componentMask = operandDescriptor & 0xf; + for (int i = 0; i < 4; i++) { + if (componentMask & (1 << i)) { + destVector[3 - i] = result[3 - i]; + } + } } \ No newline at end of file diff --git a/tests/PICA_LITP/Makefile b/tests/PICA_LITP/Makefile new file mode 100644 index 00000000..46a94048 --- /dev/null +++ b/tests/PICA_LITP/Makefile @@ -0,0 +1,255 @@ +#--------------------------------------------------------------------------------- +.SUFFIXES: +#--------------------------------------------------------------------------------- + +ifeq ($(strip $(DEVKITARM)),) +$(error "Please set DEVKITARM in your environment. export DEVKITARM=devkitARM") +endif + +TOPDIR ?= $(CURDIR) +include $(DEVKITARM)/3ds_rules + +#--------------------------------------------------------------------------------- +# TARGET is the name of the output +# BUILD is the directory where object files & intermediate files will be placed +# SOURCES is a list of directories containing source code +# DATA is a list of directories containing data files +# INCLUDES is a list of directories containing header files +# GRAPHICS is a list of directories containing graphics files +# GFXBUILD is the directory where converted graphics files will be placed +# If set to $(BUILD), it will statically link in the converted +# files as if they were data files. +# +# NO_SMDH: if set to anything, no SMDH file is generated. +# ROMFS is the directory which contains the RomFS, relative to the Makefile (Optional) +# APP_TITLE is the name of the app stored in the SMDH file (Optional) +# APP_DESCRIPTION is the description of the app stored in the SMDH file (Optional) +# APP_AUTHOR is the author of the app stored in the SMDH file (Optional) +# ICON is the filename of the icon (.png), relative to the project folder. +# If not set, it attempts to use one of the following (in this order): +# - .png +# - icon.png +# - /default_icon.png +#--------------------------------------------------------------------------------- +TARGET := $(notdir $(CURDIR)) +BUILD := build +SOURCES := source +DATA := data +INCLUDES := include +GRAPHICS := gfx +GFXBUILD := $(BUILD) +#ROMFS := romfs +#GFXBUILD := $(ROMFS)/gfx + +#--------------------------------------------------------------------------------- +# options for code generation +#--------------------------------------------------------------------------------- +ARCH := -march=armv6k -mtune=mpcore -mfloat-abi=hard -mtp=soft + +CFLAGS := -g -Wall -O2 -mword-relocations \ + -ffunction-sections \ + $(ARCH) + +CFLAGS += $(INCLUDE) -D__3DS__ + +CXXFLAGS := $(CFLAGS) -fno-rtti -fno-exceptions -std=gnu++11 + +ASFLAGS := -g $(ARCH) +LDFLAGS = -specs=3dsx.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) + +LIBS := -lcitro3d -lctru -lm + +#--------------------------------------------------------------------------------- +# list of directories containing libraries, this must be the top level containing +# include and lib +#--------------------------------------------------------------------------------- +LIBDIRS := $(CTRULIB) + + +#--------------------------------------------------------------------------------- +# no real need to edit anything past this point unless you need to add additional +# rules for different file extensions +#--------------------------------------------------------------------------------- +ifneq ($(BUILD),$(notdir $(CURDIR))) +#--------------------------------------------------------------------------------- + +export OUTPUT := $(CURDIR)/$(TARGET) +export TOPDIR := $(CURDIR) + +export VPATH := $(foreach dir,$(SOURCES),$(CURDIR)/$(dir)) \ + $(foreach dir,$(GRAPHICS),$(CURDIR)/$(dir)) \ + $(foreach dir,$(DATA),$(CURDIR)/$(dir)) + +export DEPSDIR := $(CURDIR)/$(BUILD) + +CFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.c))) +CPPFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.cpp))) +SFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.s))) +PICAFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.v.pica))) +SHLISTFILES := $(foreach dir,$(SOURCES),$(notdir $(wildcard $(dir)/*.shlist))) +GFXFILES := $(foreach dir,$(GRAPHICS),$(notdir $(wildcard $(dir)/*.t3s))) +BINFILES := $(foreach dir,$(DATA),$(notdir $(wildcard $(dir)/*.*))) + +#--------------------------------------------------------------------------------- +# use CXX for linking C++ projects, CC for standard C +#--------------------------------------------------------------------------------- +ifeq ($(strip $(CPPFILES)),) +#--------------------------------------------------------------------------------- + export LD := $(CC) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- + export LD := $(CXX) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- + +#--------------------------------------------------------------------------------- +ifeq ($(GFXBUILD),$(BUILD)) +#--------------------------------------------------------------------------------- +export T3XFILES := $(GFXFILES:.t3s=.t3x) +#--------------------------------------------------------------------------------- +else +#--------------------------------------------------------------------------------- +export ROMFS_T3XFILES := $(patsubst %.t3s, $(GFXBUILD)/%.t3x, $(GFXFILES)) +export T3XHFILES := $(patsubst %.t3s, $(BUILD)/%.h, $(GFXFILES)) +#--------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------- + +export OFILES_SOURCES := $(CPPFILES:.cpp=.o) $(CFILES:.c=.o) $(SFILES:.s=.o) + +export OFILES_BIN := $(addsuffix .o,$(BINFILES)) \ + $(PICAFILES:.v.pica=.shbin.o) $(SHLISTFILES:.shlist=.shbin.o) \ + $(addsuffix .o,$(T3XFILES)) + +export OFILES := $(OFILES_BIN) $(OFILES_SOURCES) + +export HFILES := $(PICAFILES:.v.pica=_shbin.h) $(SHLISTFILES:.shlist=_shbin.h) \ + $(addsuffix .h,$(subst .,_,$(BINFILES))) \ + $(GFXFILES:.t3s=.h) + +export INCLUDE := $(foreach dir,$(INCLUDES),-I$(CURDIR)/$(dir)) \ + $(foreach dir,$(LIBDIRS),-I$(dir)/include) \ + -I$(CURDIR)/$(BUILD) + +export LIBPATHS := $(foreach dir,$(LIBDIRS),-L$(dir)/lib) + +export _3DSXDEPS := $(if $(NO_SMDH),,$(OUTPUT).smdh) + +ifeq ($(strip $(ICON)),) + icons := $(wildcard *.png) + ifneq (,$(findstring $(TARGET).png,$(icons))) + export APP_ICON := $(TOPDIR)/$(TARGET).png + else + ifneq (,$(findstring icon.png,$(icons))) + export APP_ICON := $(TOPDIR)/icon.png + endif + endif +else + export APP_ICON := $(TOPDIR)/$(ICON) +endif + +ifeq ($(strip $(NO_SMDH)),) + export _3DSXFLAGS += --smdh=$(CURDIR)/$(TARGET).smdh +endif + +ifneq ($(ROMFS),) + export _3DSXFLAGS += --romfs=$(CURDIR)/$(ROMFS) +endif + +.PHONY: all clean + +#--------------------------------------------------------------------------------- +all: $(BUILD) $(GFXBUILD) $(DEPSDIR) $(ROMFS_T3XFILES) $(T3XHFILES) + @$(MAKE) --no-print-directory -C $(BUILD) -f $(CURDIR)/Makefile + +$(BUILD): + @mkdir -p $@ + +ifneq ($(GFXBUILD),$(BUILD)) +$(GFXBUILD): + @mkdir -p $@ +endif + +ifneq ($(DEPSDIR),$(BUILD)) +$(DEPSDIR): + @mkdir -p $@ +endif + +#--------------------------------------------------------------------------------- +clean: + @echo clean ... + @rm -fr $(BUILD) $(TARGET).3dsx $(OUTPUT).smdh $(TARGET).elf $(GFXBUILD) + +#--------------------------------------------------------------------------------- +$(GFXBUILD)/%.t3x $(BUILD)/%.h : %.t3s +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @tex3ds -i $< -H $(BUILD)/$*.h -d $(DEPSDIR)/$*.d -o $(GFXBUILD)/$*.t3x + +#--------------------------------------------------------------------------------- +else + +#--------------------------------------------------------------------------------- +# main targets +#--------------------------------------------------------------------------------- +$(OUTPUT).3dsx : $(OUTPUT).elf $(_3DSXDEPS) + +$(OFILES_SOURCES) : $(HFILES) + +$(OUTPUT).elf : $(OFILES) + +#--------------------------------------------------------------------------------- +# you need a rule like this for each extension you use as binary data +#--------------------------------------------------------------------------------- +%.bin.o %_bin.h : %.bin +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +#--------------------------------------------------------------------------------- +.PRECIOUS : %.t3x +#--------------------------------------------------------------------------------- +%.t3x.o %_t3x.h : %.t3x +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @$(bin2o) + +#--------------------------------------------------------------------------------- +# rules for assembling GPU shaders +#--------------------------------------------------------------------------------- +define shader-as + $(eval CURBIN := $*.shbin) + $(eval DEPSFILE := $(DEPSDIR)/$*.shbin.d) + echo "$(CURBIN).o: $< $1" > $(DEPSFILE) + echo "extern const u8" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"_end[];" > `(echo $(CURBIN) | tr . _)`.h + echo "extern const u8" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`"[];" >> `(echo $(CURBIN) | tr . _)`.h + echo "extern const u32" `(echo $(CURBIN) | sed -e 's/^\([0-9]\)/_\1/' | tr . _)`_size";" >> `(echo $(CURBIN) | tr . _)`.h + picasso -o $(CURBIN) $1 + bin2s $(CURBIN) | $(AS) -o $*.shbin.o +endef + +%.shbin.o %_shbin.h : %.v.pica %.g.pica + @echo $(notdir $^) + @$(call shader-as,$^) + +%.shbin.o %_shbin.h : %.v.pica + @echo $(notdir $<) + @$(call shader-as,$<) + +%.shbin.o %_shbin.h : %.shlist + @echo $(notdir $<) + @$(call shader-as,$(foreach file,$(shell cat $<),$(dir $<)$(file))) + +#--------------------------------------------------------------------------------- +%.t3x %.h : %.t3s +#--------------------------------------------------------------------------------- + @echo $(notdir $<) + @tex3ds -i $< -H $*.h -d $*.d -o $*.t3x + +-include $(DEPSDIR)/*.d + +#--------------------------------------------------------------------------------------- +endif +#--------------------------------------------------------------------------------------- diff --git a/tests/PICA_LITP/source/main.c b/tests/PICA_LITP/source/main.c new file mode 100644 index 00000000..3c2a3f0c --- /dev/null +++ b/tests/PICA_LITP/source/main.c @@ -0,0 +1,128 @@ +#include <3ds.h> +#include +#include +#include "vshader_shbin.h" + +#define CLEAR_COLOR 0x68B0D8FF + +#define DISPLAY_TRANSFER_FLAGS \ + (GX_TRANSFER_FLIP_VERT(0) | GX_TRANSFER_OUT_TILED(0) | GX_TRANSFER_RAW_COPY(0) | \ + GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGBA8) | GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB8) | \ + GX_TRANSFER_SCALING(GX_TRANSFER_SCALE_NO)) + +static DVLB_s* vshader_dvlb; +static shaderProgram_s program; +static int uLoc_projection; +static C3D_Mtx projection; + +static void sceneInit(void) +{ + // Load the vertex shader, create a shader program and bind it + vshader_dvlb = DVLB_ParseFile((u32*)vshader_shbin, vshader_shbin_size); + shaderProgramInit(&program); + shaderProgramSetVsh(&program, &vshader_dvlb->DVLE[0]); + C3D_BindProgram(&program); + + // Get the location of the uniforms + uLoc_projection = shaderInstanceGetUniformLocation(program.vertexShader, "projection"); + + // Configure attributes for use with the vertex shader + // Attribute format and element count are ignored in immediate mode + C3D_AttrInfo* attrInfo = C3D_GetAttrInfo(); + AttrInfo_Init(attrInfo); + AttrInfo_AddLoader(attrInfo, 0, GPU_FLOAT, 3); // v0=position + AttrInfo_AddLoader(attrInfo, 1, GPU_FLOAT, 3); // v1=color + + // Compute the projection matrix + Mtx_OrthoTilt(&projection, 0.0, 400.0, 0.0, 240.0, 0.0, 1.0, true); + + // Configure the first fragment shading substage to just pass through the vertex color + // See https://www.opengl.org/sdk/docs/man2/xhtml/glTexEnv.xml for more insight + C3D_TexEnv* env = C3D_GetTexEnv(0); + C3D_TexEnvInit(env); + C3D_TexEnvSrc(env, C3D_Both, GPU_PRIMARY_COLOR, 0, 0); + C3D_TexEnvFunc(env, C3D_Both, GPU_REPLACE); +} + +static void sceneRender(void) +{ + // Update the uniforms + C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, uLoc_projection, &projection); + + // Draw the triangle directly + C3D_ImmDrawBegin(GPU_TRIANGLES); + // Triangle 1 + // This vertex has r >= 0 and a >= 0 so the shader should output magenta (cmp.x = cmp.y = 1) + C3D_ImmSendAttrib(200.0f, 200.0f, 0.5f, 0.0f); // v0=position + C3D_ImmSendAttrib(1.0f, 0.0f, 0.0f, 1.0f); // v1=color + + // This vertex only has a >= 0, so the shader should output lime (cmp.x = 0, cmp.y = 1) + C3D_ImmSendAttrib(100.0f, 40.0f, 0.5f, 0.0f); + C3D_ImmSendAttrib(-0.5f, 1.0f, 0.0f, 1.0f); + + // This vertex only has r >= 0, so the shader should output cyan (cmp.x = 1, cmp.y = 0) + C3D_ImmSendAttrib(300.0f, 40.0f, 0.5f, 0.0f); + C3D_ImmSendAttrib(0.5f, 0.0f, 1.0f, -1.0f); + + // Triangle 2 + // The next 3 vertices have r < 0, a < 0, so the output of the shader should be the output of litp with alpha set to 1 (cmp.x = cmp.y = 0) + C3D_ImmSendAttrib(10.0f, 20.0f, 0.5f, 0.0f); + // Output g component should be 64 / 128 = 0.5 + C3D_ImmSendAttrib(-1.0f, 64.0f, 0.0f, -1.0f); + + C3D_ImmSendAttrib(90.0f, 20.0f, 0.5f, 0.0f); + // Output g component should be 128 / 128 = 1.0 + C3D_ImmSendAttrib(-1.0f, 256.0f, 1.0f, -1.0f); + + C3D_ImmSendAttrib(40.0f, 40.0f, 0.5f, 0.0f); + // Output g component should be 0 / 128 = 0 + C3D_ImmSendAttrib(-1.0f, 0.0f, 0.5f, -1.0f); + C3D_ImmDrawEnd(); +} + +static void sceneExit(void) +{ + // Free the shader program + shaderProgramFree(&program); + DVLB_Free(vshader_dvlb); +} + +int main() +{ + // Initialize graphics + gfxInitDefault(); + C3D_Init(C3D_DEFAULT_CMDBUF_SIZE); + + // Initialize the render target + C3D_RenderTarget* target = C3D_RenderTargetCreate(240, 400, GPU_RB_RGBA8, GPU_RB_DEPTH24_STENCIL8); + C3D_RenderTargetSetOutput(target, GFX_TOP, GFX_LEFT, DISPLAY_TRANSFER_FLAGS); + + // Initialize the scene + sceneInit(); + + // Main loop + while (aptMainLoop()) + { + hidScanInput(); + + // Respond to user input + u32 kDown = hidKeysDown(); + if (kDown & KEY_START) + break; // break in order to return to hbmenu + + // Render the scene + C3D_FrameBegin(C3D_FRAME_SYNCDRAW); + C3D_RenderTargetClear(target, C3D_CLEAR_ALL, CLEAR_COLOR, 0); + C3D_FrameDrawOn(target); + sceneRender(); + C3D_FrameEnd(0); + } + + // Deinitialize the scene + sceneExit(); + + // Deinitialize graphics + C3D_Fini(); + gfxExit(); + return 0; +} \ No newline at end of file diff --git a/tests/PICA_LITP/source/vshader.v.pica b/tests/PICA_LITP/source/vshader.v.pica new file mode 100644 index 00000000..d745f939 --- /dev/null +++ b/tests/PICA_LITP/source/vshader.v.pica @@ -0,0 +1,73 @@ +; Example PICA200 vertex shader + +; Uniforms +.fvec projection[4] + +; Constants +.constf myconst(0.0, 1.0, -1.0, 0.1) +.constf myconst2(0.3, 0.0, 0.0, 0.0) +.alias zeros myconst.xxxx ; Vector full of zeros +.alias ones myconst.yyyy ; Vector full of ones + +.constf magenta(0.8, 0.192, 0.812, 1.0) +.constf cyan(0.137, 0.949, 0.906, 1.0) +.constf lime(0.286, 0.929, 0.412, 1.0) + +.constf normalize_y(1.0, 1.0/128.0, 1.0, 1.0) + +; Outputs +.out outpos position +.out outclr color + +; Inputs (defined as aliases for convenience) +.alias inpos v0 +.alias inclr v1 + +.bool test + +.proc main + ; Force the w component of inpos to be 1.0 + mov r0.xyz, inpos + mov r0.w, ones + + ; outpos = projectionMatrix * inpos + dp4 outpos.x, projection[0], r0 + dp4 outpos.y, projection[1], r0 + dp4 outpos.z, projection[2], r0 + dp4 outpos.w, projection[3], r0 + + ; Test litp via the output fragment colour + ; r1 = input colour + mov r1, inclr + + ; This should perform the following operation: + ; cmp = (x >= 0, w >= 0) + ; dest = ( max(x, 0), clamp(y, -128, +128 ), 0, max(w, 0) ); + litp r2, r1 + + ifc cmp.x + ifc cmp.y + ; cmp.x = 1, cmp.y = 1, write magenta + mov outclr, magenta + end + .else + ; cmp.x = 1, cmp.y = 0, write cyan + mov outclr, cyan + end + .end + .else + ifc cmp.y + ; cmp.x = 0, cmp.y + mov outclr, lime + end + .end + .end + + ; cmp.x 0, cmp.y = 0, write output of litp to out colour, with y normalized to [-1, 1] + mul r2.xyz, normalize_y, r2 + ; Set alpha to one + mov r2.a, ones.a + + mov outclr, r2 + end +.end \ No newline at end of file From 24c4e02143f2803cfbee7723bb6c480605911d4d Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 2 Aug 2024 22:59:33 +0300 Subject: [PATCH 168/251] Format litp test --- tests/PICA_LITP/source/main.c | 79 ++++++++++++++++------------------- 1 file changed, 37 insertions(+), 42 deletions(-) diff --git a/tests/PICA_LITP/source/main.c b/tests/PICA_LITP/source/main.c index 3c2a3f0c..9bcab5b9 100644 --- a/tests/PICA_LITP/source/main.c +++ b/tests/PICA_LITP/source/main.c @@ -1,22 +1,22 @@ #include <3ds.h> #include #include + #include "vshader_shbin.h" + #define CLEAR_COLOR 0x68B0D8FF -#define DISPLAY_TRANSFER_FLAGS \ - (GX_TRANSFER_FLIP_VERT(0) | GX_TRANSFER_OUT_TILED(0) | GX_TRANSFER_RAW_COPY(0) | \ - GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGBA8) | GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB8) | \ - GX_TRANSFER_SCALING(GX_TRANSFER_SCALE_NO)) +#define DISPLAY_TRANSFER_FLAGS \ + (GX_TRANSFER_FLIP_VERT(0) | GX_TRANSFER_OUT_TILED(0) | GX_TRANSFER_RAW_COPY(0) | GX_TRANSFER_IN_FORMAT(GX_TRANSFER_FMT_RGBA8) | \ + GX_TRANSFER_OUT_FORMAT(GX_TRANSFER_FMT_RGB8) | GX_TRANSFER_SCALING(GX_TRANSFER_SCALE_NO)) static DVLB_s* vshader_dvlb; static shaderProgram_s program; static int uLoc_projection; static C3D_Mtx projection; -static void sceneInit(void) -{ +static void sceneInit(void) { // Load the vertex shader, create a shader program and bind it vshader_dvlb = DVLB_ParseFile((u32*)vshader_shbin, vshader_shbin_size); shaderProgramInit(&program); @@ -30,8 +30,8 @@ static void sceneInit(void) // Attribute format and element count are ignored in immediate mode C3D_AttrInfo* attrInfo = C3D_GetAttrInfo(); AttrInfo_Init(attrInfo); - AttrInfo_AddLoader(attrInfo, 0, GPU_FLOAT, 3); // v0=position - AttrInfo_AddLoader(attrInfo, 1, GPU_FLOAT, 3); // v1=color + AttrInfo_AddLoader(attrInfo, 0, GPU_FLOAT, 3); // v0=position + AttrInfo_AddLoader(attrInfo, 1, GPU_FLOAT, 3); // v1=color // Compute the projection matrix Mtx_OrthoTilt(&projection, 0.0, 400.0, 0.0, 240.0, 0.0, 1.0, true); @@ -44,51 +44,48 @@ static void sceneInit(void) C3D_TexEnvFunc(env, C3D_Both, GPU_REPLACE); } -static void sceneRender(void) -{ +static void sceneRender(void) { // Update the uniforms C3D_FVUnifMtx4x4(GPU_VERTEX_SHADER, uLoc_projection, &projection); // Draw the triangle directly C3D_ImmDrawBegin(GPU_TRIANGLES); - // Triangle 1 - // This vertex has r >= 0 and a >= 0 so the shader should output magenta (cmp.x = cmp.y = 1) - C3D_ImmSendAttrib(200.0f, 200.0f, 0.5f, 0.0f); // v0=position - C3D_ImmSendAttrib(1.0f, 0.0f, 0.0f, 1.0f); // v1=color + // Triangle 1 + // This vertex has r >= 0 and a >= 0 so the shader should output magenta (cmp.x = cmp.y = 1) + C3D_ImmSendAttrib(200.0f, 200.0f, 0.5f, 0.0f); // v0=position + C3D_ImmSendAttrib(1.0f, 0.0f, 0.0f, 1.0f); // v1=color - // This vertex only has a >= 0, so the shader should output lime (cmp.x = 0, cmp.y = 1) - C3D_ImmSendAttrib(100.0f, 40.0f, 0.5f, 0.0f); - C3D_ImmSendAttrib(-0.5f, 1.0f, 0.0f, 1.0f); + // This vertex only has a >= 0, so the shader should output lime (cmp.x = 0, cmp.y = 1) + C3D_ImmSendAttrib(100.0f, 40.0f, 0.5f, 0.0f); + C3D_ImmSendAttrib(-0.5f, 1.0f, 0.0f, 1.0f); - // This vertex only has r >= 0, so the shader should output cyan (cmp.x = 1, cmp.y = 0) - C3D_ImmSendAttrib(300.0f, 40.0f, 0.5f, 0.0f); - C3D_ImmSendAttrib(0.5f, 0.0f, 1.0f, -1.0f); + // This vertex only has r >= 0, so the shader should output cyan (cmp.x = 1, cmp.y = 0) + C3D_ImmSendAttrib(300.0f, 40.0f, 0.5f, 0.0f); + C3D_ImmSendAttrib(0.5f, 0.0f, 1.0f, -1.0f); - // Triangle 2 - // The next 3 vertices have r < 0, a < 0, so the output of the shader should be the output of litp with alpha set to 1 (cmp.x = cmp.y = 0) - C3D_ImmSendAttrib(10.0f, 20.0f, 0.5f, 0.0f); - // Output g component should be 64 / 128 = 0.5 - C3D_ImmSendAttrib(-1.0f, 64.0f, 0.0f, -1.0f); + // Triangle 2 + // The next 3 vertices have r < 0, a < 0, so the output of the shader should be the output of litp with alpha set to 1 (cmp.x = cmp.y = 0) + C3D_ImmSendAttrib(10.0f, 20.0f, 0.5f, 0.0f); + // Output g component should be 64 / 128 = 0.5 + C3D_ImmSendAttrib(-1.0f, 64.0f, 0.0f, -1.0f); - C3D_ImmSendAttrib(90.0f, 20.0f, 0.5f, 0.0f); - // Output g component should be 128 / 128 = 1.0 - C3D_ImmSendAttrib(-1.0f, 256.0f, 1.0f, -1.0f); + C3D_ImmSendAttrib(90.0f, 20.0f, 0.5f, 0.0f); + // Output g component should be 128 / 128 = 1.0 + C3D_ImmSendAttrib(-1.0f, 256.0f, 1.0f, -1.0f); - C3D_ImmSendAttrib(40.0f, 40.0f, 0.5f, 0.0f); - // Output g component should be 0 / 128 = 0 - C3D_ImmSendAttrib(-1.0f, 0.0f, 0.5f, -1.0f); + C3D_ImmSendAttrib(40.0f, 40.0f, 0.5f, 0.0f); + // Output g component should be 0 / 128 = 0 + C3D_ImmSendAttrib(-1.0f, 0.0f, 0.5f, -1.0f); C3D_ImmDrawEnd(); } -static void sceneExit(void) -{ +static void sceneExit(void) { // Free the shader program shaderProgramFree(&program); DVLB_Free(vshader_dvlb); } -int main() -{ +int main() { // Initialize graphics gfxInitDefault(); C3D_Init(C3D_DEFAULT_CMDBUF_SIZE); @@ -101,20 +98,18 @@ int main() sceneInit(); // Main loop - while (aptMainLoop()) - { + while (aptMainLoop()) { hidScanInput(); // Respond to user input u32 kDown = hidKeysDown(); - if (kDown & KEY_START) - break; // break in order to return to hbmenu + if (kDown & KEY_START) break; // break in order to return to hbmenu // Render the scene C3D_FrameBegin(C3D_FRAME_SYNCDRAW); - C3D_RenderTargetClear(target, C3D_CLEAR_ALL, CLEAR_COLOR, 0); - C3D_FrameDrawOn(target); - sceneRender(); + C3D_RenderTargetClear(target, C3D_CLEAR_ALL, CLEAR_COLOR, 0); + C3D_FrameDrawOn(target); + sceneRender(); C3D_FrameEnd(0); } From 68a6d73a1851168211ee1dd2047bb3ce41497c3f Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Sat, 3 Aug 2024 10:32:26 +0300 Subject: [PATCH 169/251] Libretro: Add support for cheats --- src/libretro_core.cpp | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index 3e0436b8..c91460b8 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -1,5 +1,6 @@ #include #include +#include #include @@ -381,5 +382,24 @@ void* retro_get_memory_data(uint id) { return nullptr; } -void retro_cheat_set(uint index, bool enabled, const char* code) {} -void retro_cheat_reset() {} +void retro_cheat_set(uint index, bool enabled, const char* code) { + std::string cheatCode = std::regex_replace(code, std::regex("[^0-9a-fA-F]"), ""); + std::vector bytes; + + for (size_t i = 0; i < cheatCode.size(); i += 2) { + std::string hex = cheatCode.substr(i, 2); + bytes.push_back((u8)std::stoul(hex, nullptr, 16)); + } + + u32 id = emulator->getCheats().addCheat(bytes.data(), bytes.size()); + + if (enabled) { + emulator->getCheats().enableCheat(id); + } else { + emulator->getCheats().disableCheat(id); + } +} + +void retro_cheat_reset() { + emulator->getCheats().reset(); +} From 6e65367e07456b0b71cb1bfe945fd6c9a0e2cc3b Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 3 Aug 2024 12:52:52 +0300 Subject: [PATCH 170/251] size_t -> usize --- src/libretro_core.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index c91460b8..b099067f 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -386,7 +386,7 @@ void retro_cheat_set(uint index, bool enabled, const char* code) { std::string cheatCode = std::regex_replace(code, std::regex("[^0-9a-fA-F]"), ""); std::vector bytes; - for (size_t i = 0; i < cheatCode.size(); i += 2) { + for (usize i = 0; i < cheatCode.size(); i += 2) { std::string hex = cheatCode.substr(i, 2); bytes.push_back((u8)std::stoul(hex, nullptr, 16)); } From 85bae2e94eadb187ddbfcf5fe2005ac7bcd3bd5e Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 4 Aug 2024 16:46:43 +0300 Subject: [PATCH 171/251] HLE DSP: Handle cycle drifting --- include/audio/dsp_core.hpp | 2 +- include/audio/hle_core.hpp | 4 +++- include/audio/null_core.hpp | 2 +- include/audio/teakra_core.hpp | 2 +- src/core/audio/hle_core.cpp | 21 ++++++++++++++------- src/core/audio/null_core.cpp | 2 +- src/emulator.cpp | 2 +- 7 files changed, 22 insertions(+), 13 deletions(-) diff --git a/include/audio/dsp_core.hpp b/include/audio/dsp_core.hpp index a4fb1ab1..5addfd19 100644 --- a/include/audio/dsp_core.hpp +++ b/include/audio/dsp_core.hpp @@ -43,7 +43,7 @@ namespace Audio { virtual ~DSPCore() {} virtual void reset() = 0; - virtual void runAudioFrame() = 0; + virtual void runAudioFrame(u64 eventTimestamp) = 0; virtual u8* getDspMemory() = 0; virtual u16 recvData(u32 regId) = 0; diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index 35c1c1b8..c0e0896f 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -42,6 +42,7 @@ namespace Audio { return this->bufferID > other.bufferID; } }; + // Buffer of decoded PCM16 samples. TODO: Are there better alternatives to use over deque? using SampleBuffer = std::deque>; @@ -53,6 +54,7 @@ namespace Audio { std::array gain0, gain1, gain2; u32 samplePosition; // Sample number into the current audio buffer + float rateMultiplier; u16 syncCount; u16 currentBufferID; u16 previousBufferID; @@ -185,7 +187,7 @@ namespace Audio { ~HLE_DSP() override {} void reset() override; - void runAudioFrame() override; + void runAudioFrame(u64 eventTimestamp) override; u8* getDspMemory() override { return dspRam.rawMemory.data(); } diff --git a/include/audio/null_core.hpp b/include/audio/null_core.hpp index 7d6f1c9e..bedec8d3 100644 --- a/include/audio/null_core.hpp +++ b/include/audio/null_core.hpp @@ -27,7 +27,7 @@ namespace Audio { ~NullDSP() override {} void reset() override; - void runAudioFrame() override; + void runAudioFrame(u64 eventTimestamp) override; u8* getDspMemory() override { return dspRam.data(); } diff --git a/include/audio/teakra_core.hpp b/include/audio/teakra_core.hpp index 6a011231..17104985 100644 --- a/include/audio/teakra_core.hpp +++ b/include/audio/teakra_core.hpp @@ -83,7 +83,7 @@ namespace Audio { void reset() override; // Run 1 slice of DSP instructions and schedule the next audio frame - void runAudioFrame() override { + void runAudioFrame(u64 eventTimestamp) override { runSlice(); scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::lleSlice * 2); } diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index ffab9301..d1297ad8 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -95,7 +95,7 @@ namespace Audio { scheduler.removeEvent(Scheduler::EventType::RunDSP); } - void HLE_DSP::runAudioFrame() { + void HLE_DSP::runAudioFrame(u64 eventTimestamp) { // Signal audio pipe when an audio frame is done if (dspState == DSPState::On) [[likely]] { dspService.triggerPipeEvent(DSPPipeType::Audio); @@ -103,7 +103,10 @@ namespace Audio { // TODO: Should this be called if dspState != DSPState::On? outputFrame(); - scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::cyclesPerFrame); + + // How many cycles we were late + const u64 cycleDrift = scheduler.currentTimestamp - eventTimestamp; + scheduler.addEvent(Scheduler::EventType::RunDSP, scheduler.currentTimestamp + Audio::cyclesPerFrame - cycleDrift); } u16 HLE_DSP::recvData(u32 regId) { @@ -237,10 +240,9 @@ namespace Audio { auto& status = write.sourceStatuses.status[i]; status.enabled = source.enabled; status.syncCount = source.syncCount; - status.currentBufferIDDirty = source.isBufferIDDirty ? 1 : 0; + status.currentBufferIDDirty = (source.isBufferIDDirty ? 1 : 0); status.currentBufferID = source.currentBufferID; status.previousBufferID = source.previousBufferID; - // TODO: Properly update sample position status.samplePosition = source.samplePosition; source.isBufferIDDirty = false; @@ -292,6 +294,10 @@ namespace Audio { source.sourceType = config.monoOrStereo; } + if (config.rateMultiplierDirty) { + source.rateMultiplier = (config.rateMultiplier > 0.f) ? config.rateMultiplier : 1.f; + } + if (config.embeddedBufferDirty) { // Annoyingly, and only for embedded buffer, whether we use config.playPosition depends on the relevant dirty bit const u32 playPosition = config.playPositionDirty ? config.playPosition : 0; @@ -434,7 +440,7 @@ namespace Audio { decodeBuffer(source); } else { - constexpr uint maxSampleCount = Audio::samplesInFrame; + uint maxSampleCount = uint(float(Audio::samplesInFrame) * source.rateMultiplier); uint outputCount = 0; while (outputCount < maxSampleCount) { @@ -447,9 +453,9 @@ namespace Audio { } const uint sampleCount = std::min(maxSampleCount - outputCount, source.currentSamples.size()); - // samples.insert(samples.end(), source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); - source.currentSamples.erase(source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); + // samples.insert(samples.end(), source.currentSamples.begin(), source.currentSamples.begin() + sampleCount); + source.currentSamples.erase(source.currentSamples.begin(), std::next(source.currentSamples.begin(), sampleCount)); source.samplePosition += sampleCount; outputCount += sampleCount; } @@ -618,6 +624,7 @@ namespace Audio { previousBufferID = 0; currentBufferID = 0; syncCount = 0; + rateMultiplier = 1.f; buffers = {}; } diff --git a/src/core/audio/null_core.cpp b/src/core/audio/null_core.cpp index ec073ae7..93c746cb 100644 --- a/src/core/audio/null_core.cpp +++ b/src/core/audio/null_core.cpp @@ -74,7 +74,7 @@ namespace Audio { scheduler.removeEvent(Scheduler::EventType::RunDSP); } - void NullDSP::runAudioFrame() { + void NullDSP::runAudioFrame(u64 eventTimestamp) { // Signal audio pipe when an audio frame is done if (dspState == DSPState::On) [[likely]] { dspService.triggerPipeEvent(DSPPipeType::Audio); diff --git a/src/emulator.cpp b/src/emulator.cpp index 921af08f..8ce71e43 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -167,7 +167,7 @@ void Emulator::pollScheduler() { case Scheduler::EventType::UpdateTimers: kernel.pollTimers(); break; case Scheduler::EventType::RunDSP: { - dsp->runAudioFrame(); + dsp->runAudioFrame(time); break; } From 860eacc7e6b94da8f2d977880d4e99cf5bd97d96 Mon Sep 17 00:00:00 2001 From: Paris Oplopoios Date: Thu, 8 Aug 2024 17:29:44 +0300 Subject: [PATCH 172/251] Add createFromBinary function (#573) * Add createFromBinary function * Indentation --------- Co-authored-by: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> --- third_party/opengl/opengl.hpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/third_party/opengl/opengl.hpp b/third_party/opengl/opengl.hpp index 4a08650a..607815fa 100644 --- a/third_party/opengl/opengl.hpp +++ b/third_party/opengl/opengl.hpp @@ -432,6 +432,25 @@ namespace OpenGL { return m_handle != 0; } + bool createFromBinary(const uint8_t* binary, size_t size, GLenum format) { + m_handle = glCreateProgram(); + glProgramBinary(m_handle, format, binary, size); + + GLint success; + glGetProgramiv(m_handle, GL_LINK_STATUS, &success); + + if (!success) { + char buf[4096]; + glGetProgramInfoLog(m_handle, 4096, nullptr, buf); + fprintf(stderr, "Failed to link program\nError: %s\n", buf); + glDeleteProgram(m_handle); + + m_handle = 0; + } + + return m_handle != 0; + } + GLuint handle() const { return m_handle; } bool exists() const { return m_handle != 0; } void use() const { glUseProgram(m_handle); } From 88e0782f71e7fbf5544dbf899e7b0315012a38df Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 14 Aug 2024 20:13:38 +0300 Subject: [PATCH 173/251] HLE DSP: Fix source resetting --- src/core/audio/hle_core.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index d1297ad8..83271a43 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -627,5 +627,6 @@ namespace Audio { rateMultiplier = 1.f; buffers = {}; + currentSamples.clear(); } } // namespace Audio From 17b9699c24d89acec9af6b33f11498d280d4f42e Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 14 Aug 2024 17:58:26 +0000 Subject: [PATCH 174/251] Workaround MacOS runner image breaking again --- .github/workflows/MacOS_Build.yml | 2 +- .github/workflows/Qt_Build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/MacOS_Build.yml b/.github/workflows/MacOS_Build.yml index f6fafde9..912c8568 100644 --- a/.github/workflows/MacOS_Build.yml +++ b/.github/workflows/MacOS_Build.yml @@ -40,7 +40,7 @@ jobs: run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - name: Install bundle dependencies - run: brew install dylibbundler imagemagick + run: brew install --overwrite python@3.12 && brew install dylibbundler imagemagick - name: Run bundle script run: ./.github/mac-bundle.sh diff --git a/.github/workflows/Qt_Build.yml b/.github/workflows/Qt_Build.yml index 4d5c8b57..40141fb1 100644 --- a/.github/workflows/Qt_Build.yml +++ b/.github/workflows/Qt_Build.yml @@ -67,7 +67,7 @@ jobs: - name: Install bundle dependencies run: | - brew install dylibbundler imagemagick + brew install --overwrite python@3.12 && brew install dylibbundler imagemagick - name: Install qt run: brew install qt && which macdeployqt From d208c24c0cf88ce6c1ffb4266edb365116a6cbf3 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 14 Aug 2024 22:35:02 +0300 Subject: [PATCH 175/251] Implement controller gyroscope in SDL --- CMakeLists.txt | 1 + include/panda_sdl/frontend_sdl.hpp | 2 ++ include/sdl_gyro.hpp | 20 ++++++++++++++++++++ include/services/hid.hpp | 2 ++ src/core/services/hid.cpp | 1 - src/panda_sdl/frontend_sdl.cpp | 29 +++++++++++++++++++++++++++++ 6 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 include/sdl_gyro.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b55e2390..2865a3f8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -260,6 +260,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp include/PICA/pica_frag_config.hpp include/PICA/pica_frag_uniforms.hpp include/PICA/shader_gen_types.hpp include/PICA/shader_decompiler.hpp + include/sdl_gyro.hpp ) cmrc_add_resource_library( diff --git a/include/panda_sdl/frontend_sdl.hpp b/include/panda_sdl/frontend_sdl.hpp index 07038962..cbd0b88e 100644 --- a/include/panda_sdl/frontend_sdl.hpp +++ b/include/panda_sdl/frontend_sdl.hpp @@ -37,4 +37,6 @@ class FrontendSDL { // And so the user can still use the keyboard to control the analog bool keyboardAnalogX = false; bool keyboardAnalogY = false; + + void setupControllerSensors(SDL_GameController* controller); }; \ No newline at end of file diff --git a/include/sdl_gyro.hpp b/include/sdl_gyro.hpp new file mode 100644 index 00000000..17faab94 --- /dev/null +++ b/include/sdl_gyro.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include + +#include "services/hid.hpp" + +namespace Gyro::SDL { + // Convert the rotation data we get from SDL sensor events to rotation data we can feed right to HID + // Returns [pitch, roll, yaw] + static glm::vec3 convertRotation(glm::vec3 rotation) { + // Flip axes + glm::vec3 ret = -rotation; + // Convert from radians/s to deg/s and scale by the gyroscope coefficient from the HID service + ret *= 180.f / std::numbers::pi; + ret *= HIDService::gyroscopeCoeff; + + return ret; + } +} // namespace Gyro::SDL \ No newline at end of file diff --git a/include/services/hid.hpp b/include/services/hid.hpp index 86a55479..bce2cc1b 100644 --- a/include/services/hid.hpp +++ b/include/services/hid.hpp @@ -88,6 +88,8 @@ class HIDService { } public: + static constexpr float gyroscopeCoeff = 14.375f; // Same as retail 3DS + HIDService(Memory& mem, Kernel& kernel) : mem(mem), kernel(kernel) {} void reset(); void handleSyncRequest(u32 messagePointer); diff --git a/src/core/services/hid.cpp b/src/core/services/hid.cpp index ef6cbb41..aa13096c 100644 --- a/src/core/services/hid.cpp +++ b/src/core/services/hid.cpp @@ -103,7 +103,6 @@ void HIDService::getGyroscopeLowCalibrateParam(u32 messagePointer) { void HIDService::getGyroscopeCoefficient(u32 messagePointer) { log("HID::GetGyroscopeLowRawToDpsCoefficient\n"); - constexpr float gyroscopeCoeff = 14.375f; // Same as retail 3DS mem.write32(messagePointer, IPC::responseHeader(0x15, 2, 0)); mem.write32(messagePointer + 4, Result::Success); mem.write32(messagePointer + 8, Helpers::bit_cast(gyroscopeCoeff)); diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 77b1f55f..703fb1c7 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -2,6 +2,8 @@ #include +#include "sdl_gyro.hpp" + FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMappings()) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) { Helpers::panic("Failed to initialize SDL2"); @@ -20,6 +22,8 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp SDL_Joystick* stick = SDL_GameControllerGetJoystick(gameController); gameControllerID = SDL_JoystickInstanceID(stick); } + + setupControllerSensors(gameController); } const EmulatorConfig& config = emu.getConfig(); @@ -200,6 +204,8 @@ void FrontendSDL::run() { if (gameController == nullptr) { gameController = SDL_GameControllerOpen(event.cdevice.which); gameControllerID = event.cdevice.which; + + setupControllerSensors(gameController); } break; @@ -280,6 +286,21 @@ void FrontendSDL::run() { } break; } + + case SDL_CONTROLLERSENSORUPDATE: { + if (event.csensor.sensor == SDL_SENSOR_GYRO) { + glm::vec3 rotation = Gyro::SDL::convertRotation({ + event.csensor.data[0], + event.csensor.data[1], + event.csensor.data[2], + }); + + hid.setPitch(s16(rotation.x)); + hid.setRoll(s16(rotation.y)); + hid.setYaw(s16(rotation.z)); + } + break; + } case SDL_DROPFILE: { char* droppedDir = event.drop.file; @@ -342,3 +363,11 @@ void FrontendSDL::run() { SDL_GL_SwapWindow(window); } } + +void FrontendSDL::setupControllerSensors(SDL_GameController* controller) { + bool haveGyro = SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) == SDL_TRUE; + + if (haveGyro) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); + } +} \ No newline at end of file From 520e00c5531f92bd9dca36901835bc546fbe1dd9 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 14 Aug 2024 22:57:45 +0300 Subject: [PATCH 176/251] Qt: Add controller gyroscope --- include/panda_qt/main_window.hpp | 1 + src/panda_qt/main_window.cpp | 28 ++++++++++++++++++++++++++++ src/panda_sdl/frontend_sdl.cpp | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index ecdbc02e..3ff16a1d 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -122,6 +122,7 @@ class MainWindow : public QMainWindow { void showAboutMenu(); void initControllers(); void pollControllers(); + void setupControllerSensors(SDL_GameController* controller); void sendMessage(const EmulatorMessage& message); void dispatchMessage(const EmulatorMessage& message); diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 65769116..f1949da7 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -9,6 +9,7 @@ #include "cheats.hpp" #include "input_mappings.hpp" +#include "sdl_gyro.hpp" #include "services/dsp.hpp" MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), keyboardMappings(InputMappings::defaultKeyboardMappings()) { @@ -521,6 +522,8 @@ void MainWindow::initControllers() { SDL_Joystick* stick = SDL_GameControllerGetJoystick(gameController); gameControllerID = SDL_JoystickInstanceID(stick); } + + setupControllerSensors(gameController); } } @@ -558,6 +561,8 @@ void MainWindow::pollControllers() { if (gameController == nullptr) { gameController = SDL_GameControllerOpen(event.cdevice.which); gameControllerID = event.cdevice.which; + + setupControllerSensors(gameController); } break; @@ -598,6 +603,29 @@ void MainWindow::pollControllers() { } break; } + + case SDL_CONTROLLERSENSORUPDATE: { + if (event.csensor.sensor == SDL_SENSOR_GYRO) { + auto rotation = Gyro::SDL::convertRotation({ + event.csensor.data[0], + event.csensor.data[1], + event.csensor.data[2], + }); + + hid.setPitch(s16(rotation.x)); + hid.setRoll(s16(rotation.y)); + hid.setYaw(s16(rotation.z)); + } + break; + } } } } + +void MainWindow::setupControllerSensors(SDL_GameController* controller) { + bool haveGyro = SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) == SDL_TRUE; + + if (haveGyro) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); + } +} \ No newline at end of file diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 703fb1c7..8f9f4240 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -289,7 +289,7 @@ void FrontendSDL::run() { case SDL_CONTROLLERSENSORUPDATE: { if (event.csensor.sensor == SDL_SENSOR_GYRO) { - glm::vec3 rotation = Gyro::SDL::convertRotation({ + auto rotation = Gyro::SDL::convertRotation({ event.csensor.data[0], event.csensor.data[1], event.csensor.data[2], From ff7e0f9ca88e10900a3132c8e646aecaee688760 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 14 Aug 2024 23:41:48 +0300 Subject: [PATCH 177/251] Optimize gyro calculation --- include/sdl_gyro.hpp | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/include/sdl_gyro.hpp b/include/sdl_gyro.hpp index 17faab94..e2df18df 100644 --- a/include/sdl_gyro.hpp +++ b/include/sdl_gyro.hpp @@ -8,13 +8,10 @@ namespace Gyro::SDL { // Convert the rotation data we get from SDL sensor events to rotation data we can feed right to HID // Returns [pitch, roll, yaw] - static glm::vec3 convertRotation(glm::vec3 rotation) { - // Flip axes - glm::vec3 ret = -rotation; - // Convert from radians/s to deg/s and scale by the gyroscope coefficient from the HID service - ret *= 180.f / std::numbers::pi; - ret *= HIDService::gyroscopeCoeff; - - return ret; + static glm::vec3 convertRotation(glm::vec3 rotation) { + // Convert the rotation from rad/s to deg/s and scale by the gyroscope coefficient in HID + constexpr float scale = 180.f / std::numbers::pi * HIDService::gyroscopeCoeff; + // The axes are also inverted, so invert scale before the multiplication. + return rotation * -scale; } } // namespace Gyro::SDL \ No newline at end of file From c772b1c7026ced40084e63c1fe5366f39e5b4bcd Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 15 Aug 2024 17:31:55 +0300 Subject: [PATCH 178/251] Initial accelerometer support --- include/services/hid.hpp | 12 ++++++++++++ src/core/services/hid.cpp | 22 +++++++++++++++++++--- src/panda_qt/main_window.cpp | 7 +++++++ src/panda_sdl/frontend_sdl.cpp | 7 +++++++ 4 files changed, 45 insertions(+), 3 deletions(-) diff --git a/include/services/hid.hpp b/include/services/hid.hpp index bce2cc1b..a0eefb1c 100644 --- a/include/services/hid.hpp +++ b/include/services/hid.hpp @@ -56,6 +56,7 @@ class HIDService { s16 circlePadX, circlePadY; // Circlepad state s16 touchScreenX, touchScreenY; // Touchscreen state s16 roll, pitch, yaw; // Gyroscope state + s16 accelX, accelY, accelZ; // Accelerometer state bool accelerometerEnabled; bool eventsInitialized; @@ -87,6 +88,11 @@ class HIDService { *(T*)&sharedMem[offset] = value; } + template + T* getSharedMemPointer(size_t offset) { + return (T*)&sharedMem[offset]; + } + public: static constexpr float gyroscopeCoeff = 14.375f; // Same as retail 3DS @@ -130,6 +136,12 @@ class HIDService { void setPitch(s16 value) { pitch = value; } void setYaw(s16 value) { yaw = value; } + void setAccel(s16 x, s16 y, s16 z) { + accelX = x; + accelY = y; + accelZ = z; + } + void updateInputs(u64 currentTimestamp); void setSharedMem(u8* ptr) { diff --git a/src/core/services/hid.cpp b/src/core/services/hid.cpp index aa13096c..a7b9b13b 100644 --- a/src/core/services/hid.cpp +++ b/src/core/services/hid.cpp @@ -35,6 +35,7 @@ void HIDService::reset() { circlePadX = circlePadY = 0; touchScreenX = touchScreenY = 0; roll = pitch = yaw = 0; + accelX = accelY = accelZ = 0; } void HIDService::handleSyncRequest(u32 messagePointer) { @@ -189,6 +190,20 @@ void HIDService::updateInputs(u64 currentTick) { writeSharedMem(0x108, currentTick); // Write new tick count } writeSharedMem(0x118, nextAccelerometerIndex); // Index last updated by the HID module + const size_t accelEntryOffset = 0x128 + (nextAccelerometerIndex * 6); // Offset in the array of 8 accelerometer entries + + // Raw data of current accelerometer entry + // TODO: How is the "raw" data actually calculated? + s16* accelerometerDataRaw = getSharedMemPointer(0x120); + accelerometerDataRaw[0] = accelX; + accelerometerDataRaw[1] = accelY; + accelerometerDataRaw[2] = accelZ; + + // Accelerometer entry in entry table + s16* accelerometerData = getSharedMemPointer(accelEntryOffset); + accelerometerData[0] = accelX; + accelerometerData[1] = accelY; + accelerometerData[2] = accelZ; nextAccelerometerIndex = (nextAccelerometerIndex + 1) % 8; // Move to next entry // Next, update gyro state @@ -197,9 +212,10 @@ void HIDService::updateInputs(u64 currentTick) { writeSharedMem(0x158, currentTick); // Write new tick count } const size_t gyroEntryOffset = 0x178 + (nextGyroIndex * 6); // Offset in the array of 8 touchscreen entries - writeSharedMem(gyroEntryOffset, pitch); - writeSharedMem(gyroEntryOffset + 2, yaw); - writeSharedMem(gyroEntryOffset + 4, roll); + s16* gyroData = getSharedMemPointer(gyroEntryOffset); + gyroData[0] = pitch; + gyroData[1] = yaw; + gyroData[2] = roll; // Since gyroscope euler angles are relative, we zero them out here and the frontend will update them again when we receive a new rotation roll = pitch = yaw = 0; diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index f1949da7..fab77d2e 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -615,6 +615,8 @@ void MainWindow::pollControllers() { hid.setPitch(s16(rotation.x)); hid.setRoll(s16(rotation.y)); hid.setYaw(s16(rotation.z)); + } else if (event.csensor.sensor == SDL_SENSOR_ACCEL) { + hid.setAccel(s16(event.csensor.data[0]), s16(-event.csensor.data[1]), s16(event.csensor.data[2])); } break; } @@ -624,8 +626,13 @@ void MainWindow::pollControllers() { void MainWindow::setupControllerSensors(SDL_GameController* controller) { bool haveGyro = SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) == SDL_TRUE; + bool haveAccelerometer = SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) == SDL_TRUE; if (haveGyro) { SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); } + + if (haveAccelerometer) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); + } } \ No newline at end of file diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 8f9f4240..80014884 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -298,6 +298,8 @@ void FrontendSDL::run() { hid.setPitch(s16(rotation.x)); hid.setRoll(s16(rotation.y)); hid.setYaw(s16(rotation.z)); + } else if (event.csensor.sensor == SDL_SENSOR_ACCEL) { + hid.setAccel(s16(event.csensor.data[0]), s16(-event.csensor.data[1]), s16(event.csensor.data[2])); } break; } @@ -366,8 +368,13 @@ void FrontendSDL::run() { void FrontendSDL::setupControllerSensors(SDL_GameController* controller) { bool haveGyro = SDL_GameControllerHasSensor(controller, SDL_SENSOR_GYRO) == SDL_TRUE; + bool haveAccelerometer = SDL_GameControllerHasSensor(controller, SDL_SENSOR_ACCEL) == SDL_TRUE; if (haveGyro) { SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); } + + if (haveAccelerometer) { + SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_ACCEL, SDL_TRUE); + } } \ No newline at end of file From 98b5d560215d5899d12bea2ff0f161263bc71d8b Mon Sep 17 00:00:00 2001 From: Samuliak Date: Fri, 16 Aug 2024 10:06:56 +0200 Subject: [PATCH 179/251] metal: add all the files --- .gitignore | 4 + CMakeLists.txt | 86 +- .../renderer_mtl/mtl_blit_pipeline_cache.hpp | 75 ++ .../renderer_mtl/mtl_depth_stencil_cache.hpp | 86 ++ .../renderer_mtl/mtl_draw_pipeline_cache.hpp | 174 ++++ include/renderer_mtl/mtl_render_target.hpp | 92 +++ include/renderer_mtl/mtl_texture.hpp | 77 ++ .../renderer_mtl/mtl_vertex_buffer_cache.hpp | 80 ++ include/renderer_mtl/objc_helper.hpp | 16 + include/renderer_mtl/pica_to_mtl.hpp | 155 ++++ include/renderer_mtl/renderer_mtl.hpp | 189 +++++ src/core/renderer_mtl/metal_cpp_impl.cpp | 6 + src/core/renderer_mtl/mtl_etc1.cpp | 124 +++ src/core/renderer_mtl/mtl_texture.cpp | 312 +++++++ src/core/renderer_mtl/objc_helper.mm | 12 + src/core/renderer_mtl/renderer_mtl.cpp | 774 +++++++++++++++++ .../metal_copy_to_lut_texture.metal | 9 + src/host_shaders/metal_shaders.metal | 782 ++++++++++++++++++ 18 files changed, 3041 insertions(+), 12 deletions(-) create mode 100644 include/renderer_mtl/mtl_blit_pipeline_cache.hpp create mode 100644 include/renderer_mtl/mtl_depth_stencil_cache.hpp create mode 100644 include/renderer_mtl/mtl_draw_pipeline_cache.hpp create mode 100644 include/renderer_mtl/mtl_render_target.hpp create mode 100644 include/renderer_mtl/mtl_texture.hpp create mode 100644 include/renderer_mtl/mtl_vertex_buffer_cache.hpp create mode 100644 include/renderer_mtl/objc_helper.hpp create mode 100644 include/renderer_mtl/pica_to_mtl.hpp create mode 100644 include/renderer_mtl/renderer_mtl.hpp create mode 100644 src/core/renderer_mtl/metal_cpp_impl.cpp create mode 100644 src/core/renderer_mtl/mtl_etc1.cpp create mode 100644 src/core/renderer_mtl/mtl_texture.cpp create mode 100644 src/core/renderer_mtl/objc_helper.mm create mode 100644 src/core/renderer_mtl/renderer_mtl.cpp create mode 100644 src/host_shaders/metal_copy_to_lut_texture.metal create mode 100644 src/host_shaders/metal_shaders.metal diff --git a/.gitignore b/.gitignore index 528462ad..817786a3 100644 --- a/.gitignore +++ b/.gitignore @@ -64,5 +64,9 @@ fb.bat *.elf *.smdh +# Compiled Metal shader files +*.ir +*.metallib + config.toml CMakeSettings.json diff --git a/CMakeLists.txt b/CMakeLists.txt index 2865a3f8..31fdd9f2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,16 +26,17 @@ endif() if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format-nonliteral -Wno-format-security") -endif() +endif() if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-interference-size") -endif() +endif() option(DISABLE_PANIC_DEV "Make a build with fewer and less intrusive asserts" ON) option(GPU_DEBUG_INFO "Enable additional GPU debugging info" OFF) option(ENABLE_OPENGL "Enable OpenGL rendering backend" ON) option(ENABLE_VULKAN "Enable Vulkan rendering backend" ON) +option(ENABLE_METAL "Enable Metal rendering backend (if available)" ON) option(ENABLE_LTO "Enable link-time optimization" OFF) option(ENABLE_TESTS "Compile unit-tests" OFF) option(ENABLE_USER_BUILD "Make a user-facing build. These builds have various assertions disabled, LTO, and more" OFF) @@ -55,11 +56,6 @@ if(BUILD_LIBRETRO_CORE) add_compile_definitions(__LIBRETRO__) endif() -if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND ENABLE_USER_BUILD) - # Disable stack buffer overflow checks in user builds - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /GS-") -endif() - add_library(AlberCore STATIC) include_directories(${PROJECT_SOURCE_DIR}/include/) @@ -240,7 +236,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/services/mic.hpp include/services/cecd.hpp include/services/ac.hpp include/services/am.hpp include/services/boss.hpp include/services/frd.hpp include/services/nim.hpp include/fs/archive_ext_save_data.hpp include/fs/archive_ncch.hpp include/services/mcu/mcu_hwc.hpp - include/colour.hpp include/services/y2r.hpp include/services/cam.hpp include/services/ssl.hpp + include/colour.hpp include/services/y2r.hpp include/services/cam.hpp include/services/ssl.hpp include/services/ldr_ro.hpp include/ipc.hpp include/services/act.hpp include/services/nfc.hpp include/system_models.hpp include/services/dlp_srvr.hpp include/PICA/dynapica/pica_recs.hpp include/PICA/dynapica/x64_regs.hpp include/PICA/dynapica/vertex_loader_rec.hpp include/PICA/dynapica/shader_rec.hpp @@ -251,7 +247,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/config.hpp include/services/ir_user.hpp include/http_server.hpp include/cheats.hpp include/action_replay.hpp include/renderer_sw/renderer_sw.hpp include/compiler_builtins.hpp include/fs/romfs.hpp include/fs/ivfc.hpp include/discord_rpc.hpp include/services/http.hpp include/result/result_cfg.hpp - include/applets/applet.hpp include/applets/mii_selector.hpp include/math_util.hpp include/services/soc.hpp + include/applets/applet.hpp include/applets/mii_selector.hpp include/math_util.hpp include/services/soc.hpp include/services/news_u.hpp include/applets/software_keyboard.hpp include/applets/applet_manager.hpp include/fs/archive_user_save_data.hpp include/services/amiibo_device.hpp include/services/nfc_types.hpp include/swap.hpp include/services/csnd.hpp include/services/nwm_uds.hpp include/fs/archive_system_save_data.hpp include/lua_manager.hpp include/memory_mapped_file.hpp include/hydra_icon.hpp @@ -260,7 +256,6 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp include/PICA/pica_frag_config.hpp include/PICA/pica_frag_uniforms.hpp include/PICA/shader_gen_types.hpp include/PICA/shader_decompiler.hpp - include/sdl_gyro.hpp ) cmrc_add_resource_library( @@ -418,8 +413,75 @@ if(ENABLE_VULKAN) target_link_libraries(AlberCore PRIVATE Vulkan::Vulkan resources_renderer_vk) endif() +if(ENABLE_METAL AND APPLE) + set(RENDERER_MTL_INCLUDE_FILES include/renderer_mtl/renderer_mtl.hpp + include/renderer_mtl/mtl_depth_stencil_cache.hpp + include/renderer_mtl/mtl_blit_pipeline_cache.hpp + include/renderer_mtl/mtl_draw_pipeline_cache.hpp + include/renderer_mtl/mtl_render_target.hpp + include/renderer_mtl/mtl_texture.hpp + include/renderer_mtl/mtl_vertex_buffer_cache.hpp + include/renderer_mtl/pica_to_mtl.hpp + include/renderer_mtl/objc_helper.hpp + ) + + set(RENDERER_MTL_SOURCE_FILES src/core/renderer_mtl/metal_cpp_impl.cpp + src/core/renderer_mtl/renderer_mtl.cpp + src/core/renderer_mtl/mtl_texture.cpp + src/core/renderer_mtl/mtl_etc1.cpp + src/core/renderer_mtl/objc_helper.mm + src/host_shaders/metal_shaders.metal + src/host_shaders/metal_copy_to_lut_texture.metal + ) + + set(HEADER_FILES ${HEADER_FILES} ${RENDERER_MTL_INCLUDE_FILES}) + source_group("Source Files\\Core\\Metal Renderer" FILES ${RENDERER_MTL_SOURCE_FILES}) + + set(RENDERER_MTL_HOST_SHADERS_SOURCES) + function (add_metal_shader SHADER) + set(SHADER_SOURCE "${CMAKE_SOURCE_DIR}/src/host_shaders/${SHADER}.metal") + set(SHADER_IR "${CMAKE_SOURCE_DIR}/src/host_shaders/${SHADER}.ir") + set(SHADER_METALLIB "${CMAKE_SOURCE_DIR}/src/host_shaders/${SHADER}.metallib") + # TODO: only include sources in debug builds + add_custom_command( + OUTPUT ${SHADER_IR} + COMMAND xcrun -sdk macosx metal -gline-tables-only -frecord-sources -o ${SHADER_IR} -c ${SHADER_SOURCE} + DEPENDS ${SHADER_SOURCE} + VERBATIM) + add_custom_command( + OUTPUT ${SHADER_METALLIB} + COMMAND xcrun -sdk macosx metallib -o ${SHADER_METALLIB} ${SHADER_IR} + DEPENDS ${SHADER_IR} + VERBATIM) + set(RENDERER_MTL_HOST_SHADERS_SOURCES ${RENDERER_MTL_HOST_SHADERS_SOURCES} ${SHADER_METALLIB}) + endfunction() + + add_metal_shader(metal_shaders) + add_metal_shader(metal_copy_to_lut_texture) + + add_custom_target( + compile_msl_shaders + DEPENDS ${RENDERER_MTL_HOST_SHADERS_SOURCES} + ) + + cmrc_add_resource_library( + resources_renderer_mtl + NAMESPACE RendererMTL + WHENCE "src/host_shaders/" + "src/host_shaders/metal_shaders.metallib" + "src/host_shaders/metal_copy_to_lut_texture.metallib" + ) + add_dependencies(resources_renderer_mtl compile_msl_shaders) + + target_sources(AlberCore PRIVATE ${RENDERER_MTL_SOURCE_FILES}) + target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_METAL=1") + target_include_directories(AlberCore PRIVATE third_party/metal-cpp) + # TODO: check if all of them are needed + target_link_libraries(AlberCore PRIVATE "-framework Metal" "-framework Foundation" "-framework QuartzCore" resources_renderer_mtl) +endif() + source_group("Header Files\\Core" FILES ${HEADER_FILES}) -set(ALL_SOURCES ${SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERNEL_SOURCE_FILES} +set(ALL_SOURCES ${SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERNEL_SOURCE_FILES} ${LOADER_SOURCE_FILES} ${SERVICE_SOURCE_FILES} ${APPLET_SOURCE_FILES} ${RENDERER_SW_SOURCE_FILES} ${PICA_SOURCE_FILES} ${THIRD_PARTY_SOURCE_FILES} ${AUDIO_SOURCE_FILES} ${HEADER_FILES} ${FRONTEND_HEADER_FILES}) target_sources(AlberCore PRIVATE ${ALL_SOURCES}) @@ -508,7 +570,7 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE) ) else() set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp src/panda_sdl/mappings.cpp) - set(FRONTEND_HEADER_FILES "include/panda_sdl/frontend_sdl.hpp") + set(FRONTEND_HEADER_FILES "") endif() target_link_libraries(Alber PRIVATE AlberCore) diff --git a/include/renderer_mtl/mtl_blit_pipeline_cache.hpp b/include/renderer_mtl/mtl_blit_pipeline_cache.hpp new file mode 100644 index 00000000..26422635 --- /dev/null +++ b/include/renderer_mtl/mtl_blit_pipeline_cache.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include + +#include "pica_to_mtl.hpp" + +using namespace PICA; + +namespace Metal { + +struct BlitPipelineHash { + // Formats + ColorFmt colorFmt; + DepthFmt depthFmt; +}; + +// This pipeline only caches the pipeline with all of its color and depth attachment variations +class BlitPipelineCache { +public: + BlitPipelineCache() = default; + + ~BlitPipelineCache() { + reset(); + vertexFunction->release(); + fragmentFunction->release(); + } + + void set(MTL::Device* dev, MTL::Function* vert, MTL::Function* frag) { + device = dev; + vertexFunction = vert; + fragmentFunction = frag; + } + + MTL::RenderPipelineState* get(BlitPipelineHash hash) { + u8 intHash = ((u8)hash.colorFmt << 3) | (u8)hash.depthFmt; + auto& pipeline = pipelineCache[intHash]; + if (!pipeline) { + MTL::RenderPipelineDescriptor* desc = MTL::RenderPipelineDescriptor::alloc()->init(); + desc->setVertexFunction(vertexFunction); + desc->setFragmentFunction(fragmentFunction); + + auto colorAttachment = desc->colorAttachments()->object(0); + colorAttachment->setPixelFormat(toMTLPixelFormatColor(hash.colorFmt)); + + desc->setDepthAttachmentPixelFormat(toMTLPixelFormatDepth(hash.depthFmt)); + + NS::Error* error = nullptr; + desc->setLabel(toNSString("Blit pipeline")); + pipeline = device->newRenderPipelineState(desc, &error); + if (error) { + Helpers::panic("Error creating blit pipeline state: %s", error->description()->cString(NS::ASCIIStringEncoding)); + } + + desc->release(); + } + + return pipeline; + } + + void reset() { + for (auto& pair : pipelineCache) { + pair.second->release(); + } + pipelineCache.clear(); + } + +private: + std::map pipelineCache; + + MTL::Device* device; + MTL::Function* vertexFunction; + MTL::Function* fragmentFunction; +}; + +} // namespace Metal diff --git a/include/renderer_mtl/mtl_depth_stencil_cache.hpp b/include/renderer_mtl/mtl_depth_stencil_cache.hpp new file mode 100644 index 00000000..90721b70 --- /dev/null +++ b/include/renderer_mtl/mtl_depth_stencil_cache.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include + +#include "pica_to_mtl.hpp" + +using namespace PICA; + +namespace Metal { + +struct DepthStencilHash { + bool depthStencilWrite; + u8 depthFunc; + u32 stencilConfig; + u16 stencilOpConfig; +}; + +class DepthStencilCache { +public: + DepthStencilCache() = default; + + ~DepthStencilCache() { + reset(); + } + + void set(MTL::Device* dev) { + device = dev; + } + + MTL::DepthStencilState* get(DepthStencilHash hash) { + u64 intHash = ((u64)hash.depthStencilWrite << 56) | ((u64)hash.depthFunc << 48) | ((u64)hash.stencilConfig << 16) | (u64)hash.stencilOpConfig; + auto& depthStencilState = depthStencilCache[intHash]; + if (!depthStencilState) { + MTL::DepthStencilDescriptor* desc = MTL::DepthStencilDescriptor::alloc()->init(); + desc->setDepthWriteEnabled(hash.depthStencilWrite); + desc->setDepthCompareFunction(toMTLCompareFunc(hash.depthFunc)); + + const bool stencilEnable = Helpers::getBit<0>(hash.stencilConfig); + MTL::StencilDescriptor* stencilDesc = nullptr; + if (stencilEnable) { + const u8 stencilFunc = Helpers::getBits<4, 3>(hash.stencilConfig); + const u8 stencilRefMask = Helpers::getBits<24, 8>(hash.stencilConfig); + + const u32 stencilBufferMask = hash.depthStencilWrite ? Helpers::getBits<8, 8>(hash.stencilConfig) : 0; + + const u8 stencilFailOp = Helpers::getBits<0, 3>(hash.stencilOpConfig); + const u8 depthFailOp = Helpers::getBits<4, 3>(hash.stencilOpConfig); + const u8 passOp = Helpers::getBits<8, 3>(hash.stencilOpConfig); + + stencilDesc = MTL::StencilDescriptor::alloc()->init(); + stencilDesc->setStencilFailureOperation(toMTLStencilOperation(stencilFailOp)); + stencilDesc->setDepthFailureOperation(toMTLStencilOperation(depthFailOp)); + stencilDesc->setDepthStencilPassOperation(toMTLStencilOperation(passOp)); + stencilDesc->setStencilCompareFunction(toMTLCompareFunc(stencilFunc)); + stencilDesc->setReadMask(stencilRefMask); + stencilDesc->setWriteMask(stencilBufferMask); + + desc->setFrontFaceStencil(stencilDesc); + desc->setBackFaceStencil(stencilDesc); + } + + depthStencilState = device->newDepthStencilState(desc); + + desc->release(); + if (stencilDesc) { + stencilDesc->release(); + } + } + + return depthStencilState; + } + + void reset() { + for (auto& pair : depthStencilCache) { + pair.second->release(); + } + depthStencilCache.clear(); + } + +private: + std::map depthStencilCache; + + MTL::Device* device; +}; + +} // namespace Metal diff --git a/include/renderer_mtl/mtl_draw_pipeline_cache.hpp b/include/renderer_mtl/mtl_draw_pipeline_cache.hpp new file mode 100644 index 00000000..8bfea636 --- /dev/null +++ b/include/renderer_mtl/mtl_draw_pipeline_cache.hpp @@ -0,0 +1,174 @@ +#pragma once + +#include + +#include "pica_to_mtl.hpp" + +using namespace PICA; + +namespace Metal { + +struct DrawFragmentFunctionHash { + bool lightingEnabled; // 1 bit + u8 lightingNumLights; // 3 bits + u32 lightingConfig1; // 32 bits (TODO: check this) + // | ref | func | on | + u16 alphaControl; // 12 bits (mask: 11111111 0111 0001) +}; + +//bool operator==(const DrawFragmentFunctionHash& l, const DrawFragmentFunctionHash& r) { +// return ((l.lightingEnabled == r.lightingEnabled) && (l.lightingNumLights == r.lightingNumLights) && +// (l.lightingConfig1 == r.lightingConfig1) && (l.alphaControl == r.alphaControl)); +//} + +inline bool operator<(const DrawFragmentFunctionHash& l, const DrawFragmentFunctionHash& r) { + if (!l.lightingEnabled && r.lightingEnabled) return true; + if (l.lightingNumLights < r.lightingNumLights) return true; + if (l.lightingConfig1 < r.lightingConfig1) return true; + if (l.alphaControl < r.alphaControl) return true; + + return false; +} + +struct DrawPipelineHash { // 56 bits + // Formats + ColorFmt colorFmt; // 3 bits + DepthFmt depthFmt; // 3 bits + + // Blending + bool blendEnabled; // 1 bit + // | functions | aeq | ceq | + u32 blendControl; // 22 bits (mask: 1111111111111111 00000111 00000111) + u8 colorWriteMask; // 4 bits + + DrawFragmentFunctionHash fragHash; +}; + +//bool operator==(const DrawPipelineHash& l, const DrawPipelineHash& r) { +// return (((u32)l.colorFmt == (u32)r.colorFmt) && ((u32)l.depthFmt == (u32)r.depthFmt) && +// (l.blendEnabled == r.blendEnabled) && (l.blendControl == r.blendControl) && +// (l.colorWriteMask == r.colorWriteMask) && (l.fragHash == r.fragHash)); +//} + +inline bool operator<(const DrawPipelineHash& l, const DrawPipelineHash& r) { + if ((u32)l.colorFmt < (u32)r.colorFmt) return true; + if ((u32)l.depthFmt < (u32)r.depthFmt) return true; + if (!l.blendEnabled && r.blendEnabled) return true; + if (l.blendControl < r.blendControl) return true; + if (l.colorWriteMask < r.colorWriteMask) return true; + if (l.fragHash < r.fragHash) return true; + + return false; +} + +// Bind the vertex buffer to binding 30 so that it doesn't occupy the lower indices +#define VERTEX_BUFFER_BINDING_INDEX 30 + +// This pipeline only caches the pipeline with all of its color and depth attachment variations +class DrawPipelineCache { +public: + DrawPipelineCache() = default; + + ~DrawPipelineCache() { + reset(); + vertexDescriptor->release(); + vertexFunction->release(); + } + + void set(MTL::Device* dev, MTL::Library* lib, MTL::Function* vert, MTL::VertexDescriptor* vertDesc) { + device = dev; + library = lib; + vertexFunction = vert; + vertexDescriptor = vertDesc; + } + + MTL::RenderPipelineState* get(DrawPipelineHash hash) { + //u32 fragmentFunctionHash = ((u32)hash.lightingEnabled << 22) | ((u32)hash.lightingNumLights << 19) | ((u32)hash.lightingConfig1 << 12) | ((((u32)hash.alphaControl & 0b1111111100000000) >> 8) << 4) | ((((u32)hash.alphaControl & 0b01110000) >> 4) << 1) | ((u32)hash.alphaControl & 0b0001); + //u64 pipelineHash = ((u64)hash.colorFmt << 53) | ((u64)hash.depthFmt << 50) | ((u64)hash.blendEnabled << 49) | ((u64)hash.colorWriteMask << 45) | ((((u64)hash.blendControl & 0b11111111111111110000000000000000) >> 16) << 29) | ((((u64)hash.blendControl & 0b0000011100000000) >> 8) << 26) | (((u64)hash.blendControl & 0b00000111) << 23) | fragmentFunctionHash; + auto& pipeline = pipelineCache[hash]; + if (!pipeline) { + auto& fragmentFunction = fragmentFunctionCache[hash.fragHash]; + if (!fragmentFunction) { + MTL::FunctionConstantValues* constants = MTL::FunctionConstantValues::alloc()->init(); + constants->setConstantValue(&hash.fragHash.lightingEnabled, MTL::DataTypeBool, NS::UInteger(0)); + constants->setConstantValue(&hash.fragHash.lightingNumLights, MTL::DataTypeUChar, NS::UInteger(1)); + constants->setConstantValue(&hash.fragHash.lightingConfig1, MTL::DataTypeUInt, NS::UInteger(2)); + constants->setConstantValue(&hash.fragHash.alphaControl, MTL::DataTypeUShort, NS::UInteger(3)); + + NS::Error* error = nullptr; + fragmentFunction = library->newFunction(NS::String::string("fragmentDraw", NS::ASCIIStringEncoding), constants, &error); + if (error) { + Helpers::panic("Error creating draw fragment function: %s", error->description()->cString(NS::ASCIIStringEncoding)); + } + constants->release(); + } + + MTL::RenderPipelineDescriptor* desc = MTL::RenderPipelineDescriptor::alloc()->init(); + desc->setVertexFunction(vertexFunction); + desc->setFragmentFunction(fragmentFunction); + desc->setVertexDescriptor(vertexDescriptor); + + auto colorAttachment = desc->colorAttachments()->object(0); + colorAttachment->setPixelFormat(toMTLPixelFormatColor(hash.colorFmt)); + MTL::ColorWriteMask writeMask = 0; + if (hash.colorWriteMask & 0x1) writeMask |= MTL::ColorWriteMaskRed; + if (hash.colorWriteMask & 0x2) writeMask |= MTL::ColorWriteMaskGreen; + if (hash.colorWriteMask & 0x4) writeMask |= MTL::ColorWriteMaskBlue; + if (hash.colorWriteMask & 0x8) writeMask |= MTL::ColorWriteMaskAlpha; + colorAttachment->setWriteMask(writeMask); + if (hash.blendEnabled) { + const u8 rgbEquation = hash.blendControl & 0x7; + const u8 alphaEquation = Helpers::getBits<8, 3>(hash.blendControl); + + // Get blending functions + const u8 rgbSourceFunc = Helpers::getBits<16, 4>(hash.blendControl); + const u8 rgbDestFunc = Helpers::getBits<20, 4>(hash.blendControl); + const u8 alphaSourceFunc = Helpers::getBits<24, 4>(hash.blendControl); + const u8 alphaDestFunc = Helpers::getBits<28, 4>(hash.blendControl); + + colorAttachment->setBlendingEnabled(true); + colorAttachment->setRgbBlendOperation(toMTLBlendOperation(rgbEquation)); + colorAttachment->setAlphaBlendOperation(toMTLBlendOperation(alphaEquation)); + colorAttachment->setSourceRGBBlendFactor(toMTLBlendFactor(rgbSourceFunc)); + colorAttachment->setDestinationRGBBlendFactor(toMTLBlendFactor(rgbDestFunc)); + colorAttachment->setSourceAlphaBlendFactor(toMTLBlendFactor(alphaSourceFunc)); + colorAttachment->setDestinationAlphaBlendFactor(toMTLBlendFactor(alphaDestFunc)); + } + + desc->setDepthAttachmentPixelFormat(toMTLPixelFormatDepth(hash.depthFmt)); + + NS::Error* error = nullptr; + desc->setLabel(toNSString("Draw pipeline")); + pipeline = device->newRenderPipelineState(desc, &error); + if (error) { + Helpers::panic("Error creating draw pipeline state: %s", error->description()->cString(NS::ASCIIStringEncoding)); + } + + desc->release(); + } + + return pipeline; + } + + void reset() { + for (auto& pair : pipelineCache) { + pair.second->release(); + } + pipelineCache.clear(); + for (auto& pair : fragmentFunctionCache) { + pair.second->release(); + } + fragmentFunctionCache.clear(); + } + +private: + std::map pipelineCache; + std::map fragmentFunctionCache; + + MTL::Device* device; + MTL::Library* library; + MTL::Function* vertexFunction; + MTL::VertexDescriptor* vertexDescriptor; +}; + +} // namespace Metal diff --git a/include/renderer_mtl/mtl_render_target.hpp b/include/renderer_mtl/mtl_render_target.hpp new file mode 100644 index 00000000..73be45f4 --- /dev/null +++ b/include/renderer_mtl/mtl_render_target.hpp @@ -0,0 +1,92 @@ +#pragma once +#include +#include +#include +#include "boost/icl/interval.hpp" +#include "helpers.hpp" +#include "math_util.hpp" +#include "opengl.hpp" +#include "pica_to_mtl.hpp" +#include "objc_helper.hpp" + +template +using Interval = boost::icl::right_open_interval; + +namespace Metal { + +template +struct RenderTarget { + MTL::Device* device; + + u32 location; + Format_t format; + OpenGL::uvec2 size; + bool valid; + + // Range of VRAM taken up by buffer + Interval range; + + MTL::Texture* texture = nullptr; + + RenderTarget() : valid(false) {} + + RenderTarget(MTL::Device* dev, u32 loc, Format_t format, u32 x, u32 y, bool valid = true) + : device(dev), location(loc), format(format), size({x, y}), valid(valid) { + u64 endLoc = (u64)loc + sizeInBytes(); + // Check if start and end are valid here + range = Interval(loc, (u32)endLoc); + } + + Math::Rect getSubRect(u32 inputAddress, u32 width, u32 height) { + const u32 startOffset = (inputAddress - location) / sizePerPixel(format); + const u32 x0 = (startOffset % (size.x() * 8)) / 8; + const u32 y0 = (startOffset / (size.x() * 8)) * 8; + return Math::Rect{x0, size.y() - y0, x0 + width, size.y() - height - y0}; + } + + // 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(RenderTarget& other) { + return location == other.location && format == other.format && + size.x() == other.size.x() && size.y() == other.size.y(); + } + + void allocate() { + MTL::PixelFormat pixelFormat = MTL::PixelFormatInvalid; + if (std::is_same::value) { + pixelFormat = PICA::toMTLPixelFormatColor((PICA::ColorFmt)format); + } else if (std::is_same::value) { + pixelFormat = PICA::toMTLPixelFormatDepth((PICA::DepthFmt)format); + } else { + panic("Invalid format type"); + } + + MTL::TextureDescriptor* descriptor = MTL::TextureDescriptor::alloc()->init(); + descriptor->setTextureType(MTL::TextureType2D); + descriptor->setPixelFormat(pixelFormat); + descriptor->setWidth(size.u()); + descriptor->setHeight(size.v()); + descriptor->setUsage(MTL::TextureUsageRenderTarget | MTL::TextureUsageShaderRead); + descriptor->setStorageMode(MTL::StorageModePrivate); + texture = device->newTexture(descriptor); + texture->setLabel(toNSString(std::string(std::is_same::value ? "Color" : "Depth") + " render target " + std::to_string(size.u()) + "x" + std::to_string(size.v()))); + descriptor->release(); + } + + void free() { + valid = false; + + if (texture) { + texture->release(); + } + } + + u64 sizeInBytes() { + return (size_t)size.x() * (size_t)size.y() * PICA::sizePerPixel(format); + } +}; + +typedef RenderTarget ColorRenderTarget; +typedef RenderTarget DepthStencilRenderTarget; + +} // namespace Metal diff --git a/include/renderer_mtl/mtl_texture.hpp b/include/renderer_mtl/mtl_texture.hpp new file mode 100644 index 00000000..590132bd --- /dev/null +++ b/include/renderer_mtl/mtl_texture.hpp @@ -0,0 +1,77 @@ +#pragma once +#include +#include +#include +#include "PICA/regs.hpp" +#include "boost/icl/interval.hpp" +#include "helpers.hpp" +#include "math_util.hpp" +#include "opengl.hpp" +#include "renderer_mtl/pica_to_mtl.hpp" + +template +using Interval = boost::icl::right_open_interval; + +namespace Metal { + +struct Texture { + MTL::Device* device; + + u32 location; + u32 config; // Magnification/minification filter, wrapping configs, etc + PICA::TextureFmt format; + OpenGL::uvec2 size; + bool valid; + + // Range of VRAM taken up by buffer + Interval range; + + PICA::PixelFormatInfo formatInfo; + MTL::Texture* texture = nullptr; + MTL::SamplerState* sampler = nullptr; + + Texture() : valid(false) {} + + Texture(MTL::Device* dev, u32 loc, PICA::TextureFmt format, u32 x, u32 y, u32 config, bool valid = true) + : device(dev), 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(loc, (u32)endLoc); + } + + // 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(); + } + + void allocate(); + void setNewConfig(u32 newConfig); + void decodeTexture(std::span data); + void free(); + u64 sizeInBytes(); + + u8 decodeTexelU8(u32 u, u32 v, PICA::TextureFmt fmt, std::span data); + u16 decodeTexelU16(u32 u, u32 v, PICA::TextureFmt fmt, std::span data); + u32 decodeTexelU32(u32 u, u32 v, PICA::TextureFmt fmt, std::span data); + + // 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 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 data); + u32 decodeETC(u32 alpha, u32 u, u32 v, u64 colourData); +}; + +} // namespace Metal diff --git a/include/renderer_mtl/mtl_vertex_buffer_cache.hpp b/include/renderer_mtl/mtl_vertex_buffer_cache.hpp new file mode 100644 index 00000000..1760cdfa --- /dev/null +++ b/include/renderer_mtl/mtl_vertex_buffer_cache.hpp @@ -0,0 +1,80 @@ +#pragma once + +#include "pica_to_mtl.hpp" + +using namespace PICA; + +namespace Metal { + +struct BufferHandle { + MTL::Buffer* buffer; + size_t offset; +}; + +// 64MB buffer for caching vertex data +#define CACHE_BUFFER_SIZE 64 * 1024 * 1024 + +class VertexBufferCache { +public: + VertexBufferCache() = default; + + ~VertexBufferCache() { + endFrame(); + buffer->release(); + } + + void set(MTL::Device* dev) { + device = dev; + create(); + } + + void endFrame() { + ptr = 0; + for (auto buffer : additionalAllocations) { + buffer->release(); + } + additionalAllocations.clear(); + } + + BufferHandle get(const void* data, size_t size) { + // If the vertex buffer is too large, just create a new one + if (ptr + size > CACHE_BUFFER_SIZE) { + MTL::Buffer* newBuffer = device->newBuffer(data, size, MTL::ResourceStorageModeShared); + newBuffer->setLabel(toNSString("Additional vertex buffer")); + additionalAllocations.push_back(newBuffer); + Helpers::warn("Vertex buffer doesn't have enough space, creating a new buffer"); + + return BufferHandle{newBuffer, 0}; + } + + // Copy the data into the buffer + memcpy((char*)buffer->contents() + ptr, data, size); + + size_t oldPtr = ptr; + ptr += size; + + return BufferHandle{buffer, oldPtr}; + } + + void reset() { + endFrame(); + if (buffer) { + buffer->release(); + create(); + } + } + +private: + MTL::Buffer* buffer = nullptr; + size_t ptr = 0; + std::vector additionalAllocations; + + MTL::Device* device; + + void create() { + buffer = device->newBuffer(CACHE_BUFFER_SIZE, MTL::ResourceStorageModeShared); + buffer->setLabel(toNSString("Shared vertex buffer")); + } +}; + +} // namespace Metal diff --git a/include/renderer_mtl/objc_helper.hpp b/include/renderer_mtl/objc_helper.hpp new file mode 100644 index 00000000..91756d24 --- /dev/null +++ b/include/renderer_mtl/objc_helper.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include + +#include + +namespace Metal { + +dispatch_data_t createDispatchData(const void* data, size_t size); + +} // namespace Metal + +// Cast from std::string to NS::String* +inline NS::String* toNSString(const std::string& str) { + return NS::String::string(str.c_str(), NS::ASCIIStringEncoding); +} diff --git a/include/renderer_mtl/pica_to_mtl.hpp b/include/renderer_mtl/pica_to_mtl.hpp new file mode 100644 index 00000000..de76dc3b --- /dev/null +++ b/include/renderer_mtl/pica_to_mtl.hpp @@ -0,0 +1,155 @@ +#pragma once + +#include +#include "PICA/regs.hpp" + +namespace PICA { + +struct PixelFormatInfo { + MTL::PixelFormat pixelFormat; + size_t bytesPerTexel; +}; + +constexpr PixelFormatInfo pixelFormatInfos[14] = { + {MTL::PixelFormatRGBA8Unorm, 4}, // RGBA8 + {MTL::PixelFormatRGBA8Unorm, 4}, // RGB8 + {MTL::PixelFormatBGR5A1Unorm, 2}, // RGBA5551 + {MTL::PixelFormatB5G6R5Unorm, 2}, // RGB565 + {MTL::PixelFormatABGR4Unorm, 2}, // RGBA4 + {MTL::PixelFormatRGBA8Unorm, 4}, // IA8 + {MTL::PixelFormatRG8Unorm, 2}, // RG8 + {MTL::PixelFormatRGBA8Unorm, 4}, // I8 + {MTL::PixelFormatA8Unorm, 1}, // A8 + {MTL::PixelFormatABGR4Unorm, 2}, // IA4 + {MTL::PixelFormatABGR4Unorm, 2}, // I4 + {MTL::PixelFormatA8Unorm, 1}, // A4 + {MTL::PixelFormatRGBA8Unorm, 4}, // ETC1 + {MTL::PixelFormatRGBA8Unorm, 4}, // ETC1A4 +}; + +inline PixelFormatInfo getPixelFormatInfo(TextureFmt format) { + return pixelFormatInfos[static_cast(format)]; +} + +inline MTL::PixelFormat toMTLPixelFormatColor(ColorFmt format) { + switch (format) { + case ColorFmt::RGBA8: return MTL::PixelFormatRGBA8Unorm; + case ColorFmt::RGB8: return MTL::PixelFormatRGBA8Unorm; + case ColorFmt::RGBA5551: return MTL::PixelFormatRGBA8Unorm; // TODO: use MTL::PixelFormatBGR5A1Unorm? + case ColorFmt::RGB565: return MTL::PixelFormatRGBA8Unorm; // TODO: use MTL::PixelFormatB5G6R5Unorm? + case ColorFmt::RGBA4: return MTL::PixelFormatABGR4Unorm; + } +} + +inline MTL::PixelFormat toMTLPixelFormatDepth(DepthFmt format) { + switch (format) { + case DepthFmt::Depth16: return MTL::PixelFormatDepth16Unorm; + case DepthFmt::Unknown1: return MTL::PixelFormatInvalid; + case DepthFmt::Depth24: return MTL::PixelFormatDepth32Float; // Metal does not support 24-bit depth formats + // Apple sillicon doesn't support 24-bit depth buffers, so we use 32-bit instead + case DepthFmt::Depth24Stencil8: return MTL::PixelFormatDepth32Float_Stencil8; + } +} + +inline MTL::CompareFunction toMTLCompareFunc(u8 func) { + switch (func) { + case 0: return MTL::CompareFunctionNever; + case 1: return MTL::CompareFunctionAlways; + case 2: return MTL::CompareFunctionEqual; + case 3: return MTL::CompareFunctionNotEqual; + case 4: return MTL::CompareFunctionLess; + case 5: return MTL::CompareFunctionLessEqual; + case 6: return MTL::CompareFunctionGreater; + case 7: return MTL::CompareFunctionGreaterEqual; + default: panic("Unknown compare function %u", func); + } + + return MTL::CompareFunctionAlways; +} + +inline MTL::BlendOperation toMTLBlendOperation(u8 op) { + switch (op) { + case 0: return MTL::BlendOperationAdd; + case 1: return MTL::BlendOperationSubtract; + case 2: return MTL::BlendOperationReverseSubtract; + case 3: return MTL::BlendOperationMin; + case 4: return MTL::BlendOperationMax; + case 5: return MTL::BlendOperationAdd; // Unused (same as 0) + case 6: return MTL::BlendOperationAdd; // Unused (same as 0) + case 7: return MTL::BlendOperationAdd; // Unused (same as 0) + default: panic("Unknown blend operation %u", op); + } + + return MTL::BlendOperationAdd; +} + +inline MTL::BlendFactor toMTLBlendFactor(u8 factor) { + switch (factor) { + case 0: return MTL::BlendFactorZero; + case 1: return MTL::BlendFactorOne; + case 2: return MTL::BlendFactorSourceColor; + case 3: return MTL::BlendFactorOneMinusSourceColor; + case 4: return MTL::BlendFactorDestinationColor; + case 5: return MTL::BlendFactorOneMinusDestinationColor; + case 6: return MTL::BlendFactorSourceAlpha; + case 7: return MTL::BlendFactorOneMinusSourceAlpha; + case 8: return MTL::BlendFactorDestinationAlpha; + case 9: return MTL::BlendFactorOneMinusDestinationAlpha; + case 10: return MTL::BlendFactorBlendColor; + case 11: return MTL::BlendFactorOneMinusBlendColor; + case 12: return MTL::BlendFactorBlendAlpha; + case 13: return MTL::BlendFactorOneMinusBlendAlpha; + case 14: return MTL::BlendFactorSourceAlphaSaturated; + case 15: return MTL::BlendFactorOne; // Undocumented + default: panic("Unknown blend factor %u", factor); + } + + return MTL::BlendFactorOne; +} + +inline MTL::StencilOperation toMTLStencilOperation(u8 op) { + switch (op) { + case 0: return MTL::StencilOperationKeep; + case 1: return MTL::StencilOperationZero; + case 2: return MTL::StencilOperationReplace; + case 3: return MTL::StencilOperationIncrementClamp; + case 4: return MTL::StencilOperationDecrementClamp; + case 5: return MTL::StencilOperationInvert; + case 6: return MTL::StencilOperationIncrementWrap; + case 7: return MTL::StencilOperationDecrementWrap; + default: panic("Unknown stencil operation %u", op); + } + + return MTL::StencilOperationKeep; +} + +inline MTL::PrimitiveType toMTLPrimitiveType(PrimType primType) { + switch (primType) { + case PrimType::TriangleList: return MTL::PrimitiveTypeTriangle; + case PrimType::TriangleStrip: return MTL::PrimitiveTypeTriangleStrip; + case PrimType::TriangleFan: + Helpers::warn("Triangle fans are not supported on Metal, using triangles instead"); + return MTL::PrimitiveTypeTriangle; + case PrimType::GeometryPrimitive: + //Helpers::warn("Geometry primitives are not yet, using triangles instead"); + return MTL::PrimitiveTypeTriangle; + } +} + +inline MTL::SamplerAddressMode toMTLSamplerAddressMode(u8 addrMode) { + switch (addrMode) { + case 0: return MTL::SamplerAddressModeClampToEdge; + case 1: return MTL::SamplerAddressModeClampToBorderColor; + case 2: return MTL::SamplerAddressModeRepeat; + case 3: return MTL::SamplerAddressModeMirrorRepeat; + case 4: return MTL::SamplerAddressModeClampToEdge; + case 5: return MTL::SamplerAddressModeClampToBorderColor; + case 6: return MTL::SamplerAddressModeRepeat; + case 7: return MTL::SamplerAddressModeRepeat; + default: panic("Unknown sampler address mode %u", addrMode); + } + + return MTL::SamplerAddressModeClampToEdge; +} + +} // namespace PICA diff --git a/include/renderer_mtl/renderer_mtl.hpp b/include/renderer_mtl/renderer_mtl.hpp new file mode 100644 index 00000000..9ba0937a --- /dev/null +++ b/include/renderer_mtl/renderer_mtl.hpp @@ -0,0 +1,189 @@ +#include +#include + +#include "renderer.hpp" +#include "mtl_texture.hpp" +#include "mtl_render_target.hpp" +#include "mtl_blit_pipeline_cache.hpp" +#include "mtl_draw_pipeline_cache.hpp" +#include "mtl_depth_stencil_cache.hpp" +#include "mtl_vertex_buffer_cache.hpp" +// HACK: use the OpenGL cache +#include "../renderer_gl/surface_cache.hpp" + +class GPU; + +struct Color4 { + float r, g, b, a; +}; + +class RendererMTL final : public Renderer { + public: + RendererMTL(GPU& gpu, const std::array& internalRegs, const std::array& externalRegs); + ~RendererMTL() override; + + void reset() override; + void display() override; + void initGraphicsContext(SDL_Window* window) override; + void clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) override; + void displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) override; + void textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) override; + void drawVertices(PICA::PrimType primType, std::span vertices) override; + void screenshot(const std::string& name) override; + void deinitGraphicsContext() override; + +#ifdef PANDA3DS_FRONTEND_QT + virtual void initGraphicsContext([[maybe_unused]] GL::Context* context) override {} +#endif + + private: + CA::MetalLayer* metalLayer; + + MTL::Device* device; + MTL::CommandQueue* commandQueue; + + // Libraries + MTL::Library* library; + + // Caches + SurfaceCache colorRenderTargetCache; + SurfaceCache depthStencilRenderTargetCache; + SurfaceCache textureCache; + Metal::BlitPipelineCache blitPipelineCache; + Metal::DrawPipelineCache drawPipelineCache; + Metal::DepthStencilCache depthStencilCache; + Metal::VertexBufferCache vertexBufferCache; + + // Objects + MTL::SamplerState* nearestSampler; + MTL::SamplerState* linearSampler; + MTL::Texture* lutTexture; + MTL::DepthStencilState* defaultDepthStencilState; + + // Pipelines + MTL::RenderPipelineState* displayPipeline; + MTL::RenderPipelineState* copyToLutTexturePipeline; + + // Clears + std::map colorClearOps; + std::map depthClearOps; + std::map stencilClearOps; + + // Active state + MTL::CommandBuffer* commandBuffer = nullptr; + MTL::RenderCommandEncoder* renderCommandEncoder = nullptr; + MTL::Texture* lastColorTexture = nullptr; + MTL::Texture* lastDepthTexture = nullptr; + + // Debug + std::string nextRenderPassName; + + void createCommandBufferIfNeeded() { + if (!commandBuffer) { + commandBuffer = commandQueue->commandBuffer(); + } + } + + void endRenderPass() { + if (renderCommandEncoder) { + renderCommandEncoder->endEncoding(); + renderCommandEncoder = nullptr; + } + } + + void beginRenderPassIfNeeded(MTL::RenderPassDescriptor* renderPassDescriptor, bool doesClears, MTL::Texture* colorTexture, MTL::Texture* depthTexture = nullptr) { + createCommandBufferIfNeeded(); + + if (doesClears || !renderCommandEncoder || colorTexture != lastColorTexture || (depthTexture != lastDepthTexture && !(lastDepthTexture && !depthTexture))) { + endRenderPass(); + + renderCommandEncoder = commandBuffer->renderCommandEncoder(renderPassDescriptor); + renderCommandEncoder->setLabel(toNSString(nextRenderPassName)); + + lastColorTexture = colorTexture; + lastDepthTexture = depthTexture; + } + + renderPassDescriptor->release(); + } + + void commitCommandBuffer() { + if (renderCommandEncoder) { + renderCommandEncoder->endEncoding(); + renderCommandEncoder->release(); + renderCommandEncoder = nullptr; + } + if (commandBuffer) { + commandBuffer->commit(); + commandBuffer->release(); + commandBuffer = nullptr; + } + } + + template + inline void clearAttachment(MTL::RenderPassDescriptor* renderPassDescriptor, MTL::Texture* texture, ClearDataT clearData, GetAttachmentT getAttachment, SetClearDataT setClearData) { + bool beginRenderPass = (renderPassDescriptor == nullptr); + if (!renderPassDescriptor) { + renderPassDescriptor = MTL::RenderPassDescriptor::alloc()->init(); + } + + AttachmentT* attachment = getAttachment(renderPassDescriptor); + attachment->setTexture(texture); + setClearData(attachment, clearData); + attachment->setLoadAction(MTL::LoadActionClear); + attachment->setStoreAction(MTL::StoreActionStore); + + if (beginRenderPass) { + if (std::is_same::value) + beginRenderPassIfNeeded(renderPassDescriptor, true, texture); + else + beginRenderPassIfNeeded(renderPassDescriptor, true, nullptr, texture); + } + } + + template + inline bool clearAttachment(MTL::RenderPassDescriptor* renderPassDescriptor, MTL::Texture* texture, std::map& clearOps, GetAttachmentT getAttachment, SetClearDataT setClearData) { + auto it = clearOps.find(texture); + if (it != clearOps.end()) { + clearAttachment(renderPassDescriptor, texture, it->second, getAttachment, setClearData); + clearOps.erase(it); + return true; + } + + if (renderPassDescriptor) { + AttachmentT* attachment = getAttachment(renderPassDescriptor); + attachment->setTexture(texture); + attachment->setLoadAction(MTL::LoadActionLoad); + attachment->setStoreAction(MTL::StoreActionStore); + } + + return false; + } + + bool clearColor(MTL::RenderPassDescriptor* renderPassDescriptor, MTL::Texture* texture) { + return clearAttachment(renderPassDescriptor, texture, colorClearOps, [](MTL::RenderPassDescriptor* renderPassDescriptor) { return renderPassDescriptor->colorAttachments()->object(0); }, [](auto attachment, auto& color) { + attachment->setClearColor(MTL::ClearColor(color.r, color.g, color.b, color.a)); + }); + } + + bool clearDepth(MTL::RenderPassDescriptor* renderPassDescriptor, MTL::Texture* texture) { + return clearAttachment(renderPassDescriptor, texture, depthClearOps, [](MTL::RenderPassDescriptor* renderPassDescriptor) { return renderPassDescriptor->depthAttachment(); }, [](auto attachment, auto& depth) { + attachment->setClearDepth(depth); + }); + } + + bool clearStencil(MTL::RenderPassDescriptor* renderPassDescriptor, MTL::Texture* texture) { + return clearAttachment(renderPassDescriptor, texture, stencilClearOps, [](MTL::RenderPassDescriptor* renderPassDescriptor) { return renderPassDescriptor->stencilAttachment(); }, [](auto attachment, auto& stencil) { + attachment->setClearStencil(stencil); + }); + } + + std::optional getColorRenderTarget(u32 addr, PICA::ColorFmt format, u32 width, u32 height, bool createIfnotFound = true); + Metal::DepthStencilRenderTarget& getDepthRenderTarget(); + Metal::Texture& getTexture(Metal::Texture& tex); + void setupTextureEnvState(MTL::RenderCommandEncoder* encoder); + void bindTexturesToSlots(MTL::RenderCommandEncoder* encoder); + void updateLightingLUT(MTL::RenderCommandEncoder* encoder); + void updateFogLUT(MTL::RenderCommandEncoder* encoder); + void textureCopyImpl(Metal::ColorRenderTarget& srcFramebuffer, Metal::ColorRenderTarget& destFramebuffer, const Math::Rect& srcRect, const Math::Rect& destRect); +}; diff --git a/src/core/renderer_mtl/metal_cpp_impl.cpp b/src/core/renderer_mtl/metal_cpp_impl.cpp new file mode 100644 index 00000000..7fa7137b --- /dev/null +++ b/src/core/renderer_mtl/metal_cpp_impl.cpp @@ -0,0 +1,6 @@ +#define NS_PRIVATE_IMPLEMENTATION +#define CA_PRIVATE_IMPLEMENTATION +#define MTL_PRIVATE_IMPLEMENTATION +#include +#include +#include diff --git a/src/core/renderer_mtl/mtl_etc1.cpp b/src/core/renderer_mtl/mtl_etc1.cpp new file mode 100644 index 00000000..a414df3c --- /dev/null +++ b/src/core/renderer_mtl/mtl_etc1.cpp @@ -0,0 +1,124 @@ +#include +#include "colour.hpp" +#include "renderer_mtl/renderer_mtl.hpp" +#include "renderer_mtl/mtl_texture.hpp" + +using namespace Helpers; + +namespace Metal { + +static constexpr u32 signExtend3To32(u32 val) { + return (u32)(s32(val) << 29 >> 29); +} + +u32 Texture::getTexelETC(bool hasAlpha, u32 u, u32 v, u32 width, std::span data) { + // Pixel offset of the 8x8 tile based on u, v and the width of the texture + u32 offs = ((u & ~7) * 8) + ((v & ~7) * width); + if (!hasAlpha) + offs >>= 1; + + // In-tile offsets for u/v + u &= 7; + v &= 7; + + // ETC1(A4) also subdivide the 8x8 tile to 4 4x4 tiles + // Each tile is 8 bytes for ETC1, but since ETC1A4 has 4 alpha bits per pixel, that becomes 16 bytes + const u32 subTileSize = hasAlpha ? 16 : 8; + const u32 subTileIndex = (u / 4) + 2 * (v / 4); // Which of the 4 subtiles is this texel in? + + // In-subtile offsets for u/v + u &= 3; + v &= 3; + offs += subTileSize * subTileIndex; + + u32 alpha; + const u64* ptr = reinterpret_cast(data.data() + offs); // Cast to u64* + + if (hasAlpha) { + // First 64 bits of the 4x4 subtile are alpha data + const u64 alphaData = *ptr++; + alpha = Colour::convert4To8Bit((alphaData >> (4 * (u * 4 + v))) & 0xf); + } + else { + alpha = 0xff; // ETC1 without alpha uses ff for every pixel + } + + // Next 64 bits of the subtile are colour data + u64 colourData = *ptr; + return decodeETC(alpha, u, v, colourData); +} + +u32 Texture::decodeETC(u32 alpha, u32 u, u32 v, u64 colourData) { + static constexpr u32 modifiers[8][2] = { + { 2, 8 }, + { 5, 17 }, + { 9, 29 }, + { 13, 42 }, + { 18, 60 }, + { 24, 80 }, + { 33, 106 }, + { 47, 183 }, + }; + + // Parse colour data for 4x4 block + const u32 subindices = getBits<0, 16, u32>(colourData); + const u32 negationFlags = getBits<16, 16, u32>(colourData); + const bool flip = getBit<32>(colourData); + const bool diffMode = getBit<33>(colourData); + + // Note: index1 is indeed stored on the higher bits, with index2 in the lower bits + const u32 tableIndex1 = getBits<37, 3, u32>(colourData); + const u32 tableIndex2 = getBits<34, 3, u32>(colourData); + const u32 texelIndex = u * 4 + v; // Index of the texel in the block + + if (flip) + std::swap(u, v); + + s32 r, g, b; + if (diffMode) { + r = getBits<59, 5, s32>(colourData); + g = getBits<51, 5, s32>(colourData); + b = getBits<43, 5, s32>(colourData); + + if (u >= 2) { + r += signExtend3To32(getBits<56, 3, u32>(colourData)); + g += signExtend3To32(getBits<48, 3, u32>(colourData)); + b += signExtend3To32(getBits<40, 3, u32>(colourData)); + } + + // Expand from 5 to 8 bits per channel + r = Colour::convert5To8Bit(r); + g = Colour::convert5To8Bit(g); + b = Colour::convert5To8Bit(b); + } else { + if (u < 2) { + r = getBits<60, 4, s32>(colourData); + g = getBits<52, 4, s32>(colourData); + b = getBits<44, 4, s32>(colourData); + } else { + r = getBits<56, 4, s32>(colourData); + g = getBits<48, 4, s32>(colourData); + b = getBits<40, 4, s32>(colourData); + } + + // Expand from 4 to 8 bits per channel + r = Colour::convert4To8Bit(r); + g = Colour::convert4To8Bit(g); + b = Colour::convert4To8Bit(b); + } + + const u32 index = (u < 2) ? tableIndex1 : tableIndex2; + s32 modifier = modifiers[index][(subindices >> texelIndex) & 1]; + + if (((negationFlags >> texelIndex) & 1) != 0) { + modifier = -modifier; + } + + r = std::clamp(r + modifier, 0, 255); + g = std::clamp(g + modifier, 0, 255); + b = std::clamp(b + modifier, 0, 255); + + return (alpha << 24) | (u32(b) << 16) | (u32(g) << 8) | u32(r); +} + +} // namespace Metal diff --git a/src/core/renderer_mtl/mtl_texture.cpp b/src/core/renderer_mtl/mtl_texture.cpp new file mode 100644 index 00000000..b61c5502 --- /dev/null +++ b/src/core/renderer_mtl/mtl_texture.cpp @@ -0,0 +1,312 @@ +#include "renderer_mtl/mtl_texture.hpp" +#include "renderer_mtl/objc_helper.hpp" +#include "colour.hpp" +#include + +using namespace Helpers; + +namespace Metal { + +void Texture::allocate() { + formatInfo = PICA::getPixelFormatInfo(format); + + MTL::TextureDescriptor* descriptor = MTL::TextureDescriptor::alloc()->init(); + descriptor->setTextureType(MTL::TextureType2D); + descriptor->setPixelFormat(formatInfo.pixelFormat); + descriptor->setWidth(size.u()); + descriptor->setHeight(size.v()); + descriptor->setUsage(MTL::TextureUsageShaderRead); + descriptor->setStorageMode(MTL::StorageModeShared); // TODO: use private + staging buffers? + texture = device->newTexture(descriptor); + texture->setLabel(toNSString("Texture " + std::string(PICA::textureFormatToString(format)) + " " + std::to_string(size.u()) + "x" + std::to_string(size.v()))); + descriptor->release(); + + setNewConfig(config); +} + +// Set the texture's configuration, which includes min/mag filters, wrapping S/T modes, and so on +void Texture::setNewConfig(u32 cfg) { + config = cfg; + + if (sampler) { + sampler->release(); + } + + const auto magFilter = (cfg & 0x2) != 0 ? MTL::SamplerMinMagFilterLinear : MTL::SamplerMinMagFilterNearest; + const auto minFilter = (cfg & 0x4) != 0 ? MTL::SamplerMinMagFilterLinear : MTL::SamplerMinMagFilterNearest; + const auto wrapT = PICA::toMTLSamplerAddressMode(getBits<8, 3>(cfg)); + const auto wrapS = PICA::toMTLSamplerAddressMode(getBits<12, 3>(cfg)); + + MTL::SamplerDescriptor* samplerDescriptor = MTL::SamplerDescriptor::alloc()->init(); + samplerDescriptor->setMinFilter(minFilter); + samplerDescriptor->setMagFilter(magFilter); + samplerDescriptor->setSAddressMode(wrapS); + samplerDescriptor->setTAddressMode(wrapT); + + samplerDescriptor->setLabel(toNSString("Sampler")); + sampler = device->newSamplerState(samplerDescriptor); + samplerDescriptor->release(); +} + +void Texture::free() { + valid = false; + + if (texture) { + texture->release(); + } + if (sampler) { + sampler->release(); + } +} + +u64 Texture::sizeInBytes() { + u64 pixelCount = u64(size.x()) * u64(size.y()); + + switch (format) { + case PICA::TextureFmt::RGBA8: // 4 bytes per pixel + return pixelCount * 4; + + case PICA::TextureFmt::RGB8: // 3 bytes per pixel + return pixelCount * 3; + + case PICA::TextureFmt::RGBA5551: // 2 bytes per pixel + case PICA::TextureFmt::RGB565: + case PICA::TextureFmt::RGBA4: + case PICA::TextureFmt::RG8: + case PICA::TextureFmt::IA8: + return pixelCount * 2; + + case PICA::TextureFmt::A8: // 1 byte per pixel + case PICA::TextureFmt::I8: + case PICA::TextureFmt::IA4: + return pixelCount; + + case PICA::TextureFmt::I4: // 4 bits per pixel + case PICA::TextureFmt::A4: + return pixelCount / 2; + + case PICA::TextureFmt::ETC1: // Compressed formats + case PICA::TextureFmt::ETC1A4: { + // Number of 4x4 tiles + const u64 tileCount = pixelCount / 16; + // Tiles are 8 bytes each on ETC1 and 16 bytes each on ETC1A4 + const u64 tileSize = format == PICA::TextureFmt::ETC1 ? 8 : 16; + return tileCount * tileSize; + } + + default: + Helpers::panic("[PICA] Attempted to get size of invalid texture type"); + } +} + +// u and v are the UVs of the relevant texel +// Texture data is stored interleaved in Morton order, ie in a Z - order curve as shown here +// https://en.wikipedia.org/wiki/Z-order_curve +// Textures are split into 8x8 tiles.This function returns the in - tile offset depending on the u & v of the texel +// The in - tile offset is the sum of 2 offsets, one depending on the value of u % 8 and the other on the value of y % 8 +// As documented in this picture https ://en.wikipedia.org/wiki/File:Moser%E2%80%93de_Bruijn_addition.svg +u32 Texture::mortonInterleave(u32 u, u32 v) { + static constexpr u32 xOffsets[] = { 0, 1, 4, 5, 16, 17, 20, 21 }; + static constexpr u32 yOffsets[] = { 0, 2, 8, 10, 32, 34, 40, 42 }; + + return xOffsets[u & 7] + yOffsets[v & 7]; +} + +// Get the byte offset of texel (u, v) in the texture +u32 Texture::getSwizzledOffset(u32 u, u32 v, u32 width, u32 bytesPerPixel) { + u32 offset = ((u & ~7) * 8) + ((v & ~7) * width); // Offset of the 8x8 tile the texel belongs to + offset += mortonInterleave(u, v); // Add the in-tile offset of the texel + + return offset * bytesPerPixel; +} + +// Same as the above code except we need to divide by 2 because 4 bits is smaller than a byte +u32 Texture::getSwizzledOffset_4bpp(u32 u, u32 v, u32 width) { + u32 offset = ((u & ~7) * 8) + ((v & ~7) * width); // Offset of the 8x8 tile the texel belongs to + offset += mortonInterleave(u, v); // Add the in-tile offset of the texel + + return offset / 2; +} + +u8 Texture::decodeTexelU8(u32 u, u32 v, PICA::TextureFmt fmt, std::span data) { + switch (fmt) { + case PICA::TextureFmt::A4: { + const u32 offset = getSwizzledOffset_4bpp(u, v, size.u()); + + // For odd U coordinates, grab the top 4 bits, and the low 4 bits for even coordinates + u8 alpha = data[offset] >> ((u % 2) ? 4 : 0); + alpha = Colour::convert4To8Bit(getBits<0, 4>(alpha)); + + // A8 + return alpha; + } + + case PICA::TextureFmt::A8: { + u32 offset = getSwizzledOffset(u, v, size.u(), 1); + const u8 alpha = data[offset]; + + // A8 + return alpha; + } + + default: + Helpers::panic("[Texture::DecodeTexel] Unimplemented format = %d", static_cast(fmt)); + } +} + +u16 Texture::decodeTexelU16(u32 u, u32 v, PICA::TextureFmt fmt, std::span data) { + switch (fmt) { + case PICA::TextureFmt::RG8: { + u32 offset = getSwizzledOffset(u, v, size.u(), 2); + constexpr u8 b = 0; + const u8 g = data[offset]; + const u8 r = data[offset + 1]; + + // RG8 + return (g << 8) | r; + } + + case PICA::TextureFmt::RGBA4: { + u32 offset = getSwizzledOffset(u, v, size.u(), 2); + u16 texel = u16(data[offset]) | (u16(data[offset + 1]) << 8); + + u8 alpha = getBits<0, 4, u8>(texel); + u8 b = getBits<4, 4, u8>(texel); + u8 g = getBits<8, 4, u8>(texel); + u8 r = getBits<12, 4, u8>(texel); + + // ABGR4 + return (r << 12) | (g << 8) | (b << 4) | alpha; + } + + case PICA::TextureFmt::RGBA5551: { + const u32 offset = getSwizzledOffset(u, v, size.u(), 2); + const u16 texel = u16(data[offset]) | (u16(data[offset + 1]) << 8); + + u8 alpha = getBit<0>(texel) ? 0xff : 0; + u8 b = getBits<1, 5, u8>(texel); + u8 g = getBits<6, 5, u8>(texel); + u8 r = getBits<11, 5, u8>(texel); + + // BGR5A1 + return (alpha << 15) | (r << 10) | (g << 5) | b; + } + + case PICA::TextureFmt::RGB565: { + const u32 offset = getSwizzledOffset(u, v, size.u(), 2); + const u16 texel = u16(data[offset]) | (u16(data[offset + 1]) << 8); + + const u8 b = getBits<0, 5, u8>(texel); + const u8 g = getBits<5, 6, u8>(texel); + const u8 r = getBits<11, 5, u8>(texel); + + // B5G6R5 + return (r << 11) | (g << 5) | b; + } + + case PICA::TextureFmt::IA4: { + const u32 offset = getSwizzledOffset(u, v, size.u(), 1); + const u8 texel = data[offset]; + const u8 alpha = texel & 0xf; + const u8 intensity = texel >> 4; + + // ABGR4 + return (intensity << 12) | (intensity << 8) | (intensity << 4) | alpha; + } + + case PICA::TextureFmt::I4: { + u32 offset = getSwizzledOffset_4bpp(u, v, size.u()); + + // For odd U coordinates, grab the top 4 bits, and the low 4 bits for even coordinates + u8 intensity = data[offset] >> ((u % 2) ? 4 : 0); + intensity = getBits<0, 4>(intensity); + + // ABGR4 + return (intensity << 12) | (intensity << 8) | (intensity << 4) | 0xff; + } + + default: + Helpers::panic("[Texture::DecodeTexel] Unimplemented format = %d", static_cast(fmt)); + } +} + +u32 Texture::decodeTexelU32(u32 u, u32 v, PICA::TextureFmt fmt, std::span data) { + switch (fmt) { + case PICA::TextureFmt::RGB8: { + const u32 offset = getSwizzledOffset(u, v, size.u(), 3); + const u8 b = data[offset]; + const u8 g = data[offset + 1]; + const u8 r = data[offset + 2]; + + // RGBA8 + return (0xff << 24) | (b << 16) | (g << 8) | r; + } + + case PICA::TextureFmt::RGBA8: { + const u32 offset = getSwizzledOffset(u, v, size.u(), 4); + const u8 alpha = data[offset]; + const u8 b = data[offset + 1]; + const u8 g = data[offset + 2]; + const u8 r = data[offset + 3]; + + // RGBA8 + return (alpha << 24) | (b << 16) | (g << 8) | r; + } + + case PICA::TextureFmt::I8: { + u32 offset = getSwizzledOffset(u, v, size.u(), 1); + const u8 intensity = data[offset]; + + // RGBA8 + return (0xff << 24) | (intensity << 16) | (intensity << 8) | intensity; + } + + case PICA::TextureFmt::IA8: { + u32 offset = getSwizzledOffset(u, v, size.u(), 2); + + // Same as I8 except each pixel gets its own alpha value too + const u8 alpha = data[offset]; + const u8 intensity = data[offset + 1]; + + // RGBA8 + return (alpha << 24) | (intensity << 16) | (intensity << 8) | intensity; + } + + case PICA::TextureFmt::ETC1: return getTexelETC(false, u, v, size.u(), data); + case PICA::TextureFmt::ETC1A4: return getTexelETC(true, u, v, size.u(), data); + + default: + Helpers::panic("[Texture::DecodeTexel] Unimplemented format = %d", static_cast(fmt)); + } +} + +void Texture::decodeTexture(std::span data) { + std::vector decoded; + decoded.reserve(u64(size.u()) * u64(size.v()) * formatInfo.bytesPerTexel); + + // Decode texels line by line + for (u32 v = 0; v < size.v(); v++) { + for (u32 u = 0; u < size.u(); u++) { + if (formatInfo.bytesPerTexel == 1) { + u8 texel = decodeTexelU8(u, v, format, data); + decoded.push_back(texel); + } else if (formatInfo.bytesPerTexel == 2) { + u16 texel = decodeTexelU16(u, v, format, data); + decoded.push_back((texel & 0x00ff) >> 0); + decoded.push_back((texel & 0xff00) >> 8); + } else if (formatInfo.bytesPerTexel == 4) { + u32 texel = decodeTexelU32(u, v, format, data); + decoded.push_back((texel & 0x000000ff) >> 0); + decoded.push_back((texel & 0x0000ff00) >> 8); + decoded.push_back((texel & 0x00ff0000) >> 16); + decoded.push_back((texel & 0xff000000) >> 24); + } else { + Helpers::panic("[Texture::decodeTexture] Unimplemented bytesPerTexel (%u)", formatInfo.bytesPerTexel); + } + } + } + + texture->replaceRegion(MTL::Region(0, 0, size.u(), size.v()), 0, 0, decoded.data(), formatInfo.bytesPerTexel * size.u(), 0); +} + +} // namespace Metal diff --git a/src/core/renderer_mtl/objc_helper.mm b/src/core/renderer_mtl/objc_helper.mm new file mode 100644 index 00000000..eeea56a0 --- /dev/null +++ b/src/core/renderer_mtl/objc_helper.mm @@ -0,0 +1,12 @@ +#include "renderer_mtl/objc_helper.hpp" + +// TODO: change the include +#import + +namespace Metal { + +dispatch_data_t createDispatchData(const void* data, size_t size) { + return dispatch_data_create(data, size, dispatch_get_global_queue(0, 0), ^{}); +} + +} // namespace Metal diff --git a/src/core/renderer_mtl/renderer_mtl.cpp b/src/core/renderer_mtl/renderer_mtl.cpp new file mode 100644 index 00000000..10bca5dd --- /dev/null +++ b/src/core/renderer_mtl/renderer_mtl.cpp @@ -0,0 +1,774 @@ +#include "PICA/gpu.hpp" +#include "renderer_mtl/renderer_mtl.hpp" +#include "renderer_mtl/objc_helper.hpp" + +#include +#include + +#include "SDL_metal.h" + +using namespace PICA; + +CMRC_DECLARE(RendererMTL); + +const u16 LIGHT_LUT_TEXTURE_WIDTH = 256; + +// HACK: redefinition... +PICA::ColorFmt ToColorFormat(u32 format) { + switch (format) { + case 2: return PICA::ColorFmt::RGB565; + case 3: return PICA::ColorFmt::RGBA5551; + default: return static_cast(format); + } +} + +MTL::Library* loadLibrary(MTL::Device* device, const cmrc::file& shaderSource) { + //MTL::CompileOptions* compileOptions = MTL::CompileOptions::alloc()->init(); + NS::Error* error = nullptr; + MTL::Library* library = device->newLibrary(Metal::createDispatchData(shaderSource.begin(), shaderSource.size()), &error); + //MTL::Library* library = device->newLibrary(NS::String::string(source.c_str(), NS::ASCIIStringEncoding), compileOptions, &error); + if (error) { + Helpers::panic("Error loading shaders: %s", error->description()->cString(NS::ASCIIStringEncoding)); + } + + return library; +} + +RendererMTL::RendererMTL(GPU& gpu, const std::array& internalRegs, const std::array& externalRegs) + : Renderer(gpu, internalRegs, externalRegs) {} +RendererMTL::~RendererMTL() {} + +void RendererMTL::reset() { + vertexBufferCache.reset(); + depthStencilCache.reset(); + drawPipelineCache.reset(); + blitPipelineCache.reset(); + textureCache.reset(); + depthStencilRenderTargetCache.reset(); + colorRenderTargetCache.reset(); +} + +void RendererMTL::display() { + CA::MetalDrawable* drawable = metalLayer->nextDrawable(); + if (!drawable) { + return; + } + + using namespace PICA::ExternalRegs; + + // Top screen + const u32 topActiveFb = externalRegs[Framebuffer0Select] & 1; + const u32 topScreenAddr = externalRegs[topActiveFb == 0 ? Framebuffer0AFirstAddr : Framebuffer0ASecondAddr]; + auto topScreen = colorRenderTargetCache.findFromAddress(topScreenAddr); + + if (topScreen) { + clearColor(nullptr, topScreen->get().texture); + } + + // Bottom screen + const u32 bottomActiveFb = externalRegs[Framebuffer1Select] & 1; + const u32 bottomScreenAddr = externalRegs[bottomActiveFb == 0 ? Framebuffer1AFirstAddr : Framebuffer1ASecondAddr]; + auto bottomScreen = colorRenderTargetCache.findFromAddress(bottomScreenAddr); + + if (bottomScreen) { + clearColor(nullptr, bottomScreen->get().texture); + } + + // -------- Draw -------- + commandBuffer->pushDebugGroup(toNSString("Display")); + + MTL::RenderPassDescriptor* renderPassDescriptor = MTL::RenderPassDescriptor::alloc()->init(); + MTL::RenderPassColorAttachmentDescriptor* colorAttachment = renderPassDescriptor->colorAttachments()->object(0); + colorAttachment->setTexture(drawable->texture()); + colorAttachment->setLoadAction(MTL::LoadActionClear); + colorAttachment->setClearColor(MTL::ClearColor{0.0f, 0.0f, 0.0f, 1.0f}); + colorAttachment->setStoreAction(MTL::StoreActionStore); + + nextRenderPassName = "Display"; + beginRenderPassIfNeeded(renderPassDescriptor, false, drawable->texture()); + renderCommandEncoder->setRenderPipelineState(displayPipeline); + renderCommandEncoder->setFragmentSamplerState(nearestSampler, 0); + + // Top screen + if (topScreen) { + renderCommandEncoder->setViewport(MTL::Viewport{0, 0, 400, 240, 0.0f, 1.0f}); + renderCommandEncoder->setFragmentTexture(topScreen->get().texture, 0); + renderCommandEncoder->drawPrimitives(MTL::PrimitiveTypeTriangleStrip, NS::UInteger(0), NS::UInteger(4)); + } + + // Bottom screen + if (bottomScreen) { + renderCommandEncoder->setViewport(MTL::Viewport{40, 240, 320, 240, 0.0f, 1.0f}); + renderCommandEncoder->setFragmentTexture(bottomScreen->get().texture, 0); + renderCommandEncoder->drawPrimitives(MTL::PrimitiveTypeTriangleStrip, NS::UInteger(0), NS::UInteger(4)); + } + + endRenderPass(); + + commandBuffer->presentDrawable(drawable); + + commandBuffer->popDebugGroup(); + + commitCommandBuffer(); + + // Inform the vertex buffer cache that the frame ended + vertexBufferCache.endFrame(); + + // Release + drawable->release(); +} + +void RendererMTL::initGraphicsContext(SDL_Window* window) { + // TODO: what should be the type of the view? + void* view = SDL_Metal_CreateView(window); + metalLayer = (CA::MetalLayer*)SDL_Metal_GetLayer(view); + device = MTL::CreateSystemDefaultDevice(); + metalLayer->setDevice(device); + commandQueue = device->newCommandQueue(); + + // -------- Objects -------- + + // Textures + MTL::TextureDescriptor* textureDescriptor = MTL::TextureDescriptor::alloc()->init(); + textureDescriptor->setTextureType(MTL::TextureType2D); + textureDescriptor->setPixelFormat(MTL::PixelFormatRGBA32Float); + textureDescriptor->setWidth(LIGHT_LUT_TEXTURE_WIDTH); + textureDescriptor->setHeight(Lights::LUT_Count + 1); + textureDescriptor->setUsage(MTL::TextureUsageShaderRead | MTL::TextureUsageShaderWrite); + textureDescriptor->setStorageMode(MTL::StorageModePrivate); + + lutTexture = device->newTexture(textureDescriptor); + lutTexture->setLabel(toNSString("LUT texture")); + textureDescriptor->release(); + + // Samplers + MTL::SamplerDescriptor* samplerDescriptor = MTL::SamplerDescriptor::alloc()->init(); + samplerDescriptor->setLabel(toNSString("Sampler (nearest)")); + nearestSampler = device->newSamplerState(samplerDescriptor); + + samplerDescriptor->setMinFilter(MTL::SamplerMinMagFilterLinear); + samplerDescriptor->setMagFilter(MTL::SamplerMinMagFilterLinear); + samplerDescriptor->setLabel(toNSString("Sampler (linear)")); + linearSampler = device->newSamplerState(samplerDescriptor); + + samplerDescriptor->release(); + + // -------- Pipelines -------- + + // Load shaders + auto mtlResources = cmrc::RendererMTL::get_filesystem(); + library = loadLibrary(device, mtlResources.open("metal_shaders.metallib")); + MTL::Library* copyToLutTextureLibrary = loadLibrary(device, mtlResources.open("metal_copy_to_lut_texture.metallib")); + + // Display + MTL::Function* vertexDisplayFunction = library->newFunction(NS::String::string("vertexDisplay", NS::ASCIIStringEncoding)); + MTL::Function* fragmentDisplayFunction = library->newFunction(NS::String::string("fragmentDisplay", NS::ASCIIStringEncoding)); + + MTL::RenderPipelineDescriptor* displayPipelineDescriptor = MTL::RenderPipelineDescriptor::alloc()->init(); + displayPipelineDescriptor->setVertexFunction(vertexDisplayFunction); + displayPipelineDescriptor->setFragmentFunction(fragmentDisplayFunction); + auto* displayColorAttachment = displayPipelineDescriptor->colorAttachments()->object(0); + displayColorAttachment->setPixelFormat(MTL::PixelFormat::PixelFormatBGRA8Unorm); + + NS::Error* error = nullptr; + displayPipelineDescriptor->setLabel(toNSString("Display pipeline")); + displayPipeline = device->newRenderPipelineState(displayPipelineDescriptor, &error); + if (error) { + Helpers::panic("Error creating display pipeline state: %s", error->description()->cString(NS::ASCIIStringEncoding)); + } + displayPipelineDescriptor->release(); + vertexDisplayFunction->release(); + fragmentDisplayFunction->release(); + + // Blit + MTL::Function* vertexBlitFunction = library->newFunction(NS::String::string("vertexBlit", NS::ASCIIStringEncoding)); + MTL::Function* fragmentBlitFunction = library->newFunction(NS::String::string("fragmentBlit", NS::ASCIIStringEncoding)); + + blitPipelineCache.set(device, vertexBlitFunction, fragmentBlitFunction); + + // Draw + MTL::Function* vertexDrawFunction = library->newFunction(NS::String::string("vertexDraw", NS::ASCIIStringEncoding)); + + // -------- Vertex descriptor -------- + MTL::VertexDescriptor* vertexDescriptor = MTL::VertexDescriptor::alloc()->init(); + + // Position + MTL::VertexAttributeDescriptor* positionAttribute = vertexDescriptor->attributes()->object(0); + positionAttribute->setFormat(MTL::VertexFormatFloat4); + positionAttribute->setOffset(offsetof(Vertex, s.positions)); + positionAttribute->setBufferIndex(VERTEX_BUFFER_BINDING_INDEX); + + // Quaternion + MTL::VertexAttributeDescriptor* quaternionAttribute = vertexDescriptor->attributes()->object(1); + quaternionAttribute->setFormat(MTL::VertexFormatFloat4); + quaternionAttribute->setOffset(offsetof(Vertex, s.quaternion)); + quaternionAttribute->setBufferIndex(VERTEX_BUFFER_BINDING_INDEX); + + // Color + MTL::VertexAttributeDescriptor* colorAttribute = vertexDescriptor->attributes()->object(2); + colorAttribute->setFormat(MTL::VertexFormatFloat4); + colorAttribute->setOffset(offsetof(Vertex, s.colour)); + colorAttribute->setBufferIndex(VERTEX_BUFFER_BINDING_INDEX); + + // Texture coordinate 0 + MTL::VertexAttributeDescriptor* texCoord0Attribute = vertexDescriptor->attributes()->object(3); + texCoord0Attribute->setFormat(MTL::VertexFormatFloat2); + texCoord0Attribute->setOffset(offsetof(Vertex, s.texcoord0)); + texCoord0Attribute->setBufferIndex(VERTEX_BUFFER_BINDING_INDEX); + + // Texture coordinate 1 + MTL::VertexAttributeDescriptor* texCoord1Attribute = vertexDescriptor->attributes()->object(4); + texCoord1Attribute->setFormat(MTL::VertexFormatFloat2); + texCoord1Attribute->setOffset(offsetof(Vertex, s.texcoord1)); + texCoord1Attribute->setBufferIndex(VERTEX_BUFFER_BINDING_INDEX); + + // Texture coordinate 0 W + MTL::VertexAttributeDescriptor* texCoord0WAttribute = vertexDescriptor->attributes()->object(5); + texCoord0WAttribute->setFormat(MTL::VertexFormatFloat); + texCoord0WAttribute->setOffset(offsetof(Vertex, s.texcoord0_w)); + texCoord0WAttribute->setBufferIndex(VERTEX_BUFFER_BINDING_INDEX); + + // View + MTL::VertexAttributeDescriptor* viewAttribute = vertexDescriptor->attributes()->object(6); + viewAttribute->setFormat(MTL::VertexFormatFloat3); + viewAttribute->setOffset(offsetof(Vertex, s.view)); + viewAttribute->setBufferIndex(VERTEX_BUFFER_BINDING_INDEX); + + // Texture coordinate 2 + MTL::VertexAttributeDescriptor* texCoord2Attribute = vertexDescriptor->attributes()->object(7); + texCoord2Attribute->setFormat(MTL::VertexFormatFloat2); + texCoord2Attribute->setOffset(offsetof(Vertex, s.texcoord2)); + texCoord2Attribute->setBufferIndex(VERTEX_BUFFER_BINDING_INDEX); + + MTL::VertexBufferLayoutDescriptor* vertexBufferLayout = vertexDescriptor->layouts()->object(VERTEX_BUFFER_BINDING_INDEX); + vertexBufferLayout->setStride(sizeof(Vertex)); + vertexBufferLayout->setStepFunction(MTL::VertexStepFunctionPerVertex); + vertexBufferLayout->setStepRate(1); + + drawPipelineCache.set(device, library, vertexDrawFunction, vertexDescriptor); + + // Copy to LUT texture + MTL::FunctionConstantValues* constants = MTL::FunctionConstantValues::alloc()->init(); + constants->setConstantValue(&LIGHT_LUT_TEXTURE_WIDTH, MTL::DataTypeUShort, NS::UInteger(0)); + + error = nullptr; + MTL::Function* vertexCopyToLutTextureFunction = copyToLutTextureLibrary->newFunction(NS::String::string("vertexCopyToLutTexture", NS::ASCIIStringEncoding), constants, &error); + if (error) { + Helpers::panic("Error creating copy_to_lut_texture vertex function: %s", error->description()->cString(NS::ASCIIStringEncoding)); + } + constants->release(); + + MTL::RenderPipelineDescriptor* copyToLutTexturePipelineDescriptor = MTL::RenderPipelineDescriptor::alloc()->init(); + copyToLutTexturePipelineDescriptor->setVertexFunction(vertexCopyToLutTextureFunction); + // Disable rasterization + copyToLutTexturePipelineDescriptor->setRasterizationEnabled(false); + + error = nullptr; + copyToLutTexturePipelineDescriptor->setLabel(toNSString("Copy to LUT texture pipeline")); + copyToLutTexturePipeline = device->newRenderPipelineState(copyToLutTexturePipelineDescriptor, &error); + if (error) { + Helpers::panic("Error creating copy_to_lut_texture pipeline state: %s", error->description()->cString(NS::ASCIIStringEncoding)); + } + copyToLutTexturePipelineDescriptor->release(); + vertexCopyToLutTextureFunction->release(); + + // Depth stencil cache + depthStencilCache.set(device); + + // Vertex buffer cache + vertexBufferCache.set(device); + + // -------- Depth stencil state -------- + MTL::DepthStencilDescriptor* depthStencilDescriptor = MTL::DepthStencilDescriptor::alloc()->init(); + depthStencilDescriptor->setLabel(toNSString("Default depth stencil state")); + defaultDepthStencilState = device->newDepthStencilState(depthStencilDescriptor); + depthStencilDescriptor->release(); + + // Release + copyToLutTextureLibrary->release(); +} + +void RendererMTL::clearBuffer(u32 startAddress, u32 endAddress, u32 value, u32 control) { + const auto color = colorRenderTargetCache.findFromAddress(startAddress); + if (color) { + const float r = Helpers::getBits<24, 8>(value) / 255.0f; + const float g = Helpers::getBits<16, 8>(value) / 255.0f; + const float b = Helpers::getBits<8, 8>(value) / 255.0f; + const float a = (value & 0xff) / 255.0f; + + colorClearOps[color->get().texture] = {r, g, b, a}; + + return; + } + + const auto depth = depthStencilRenderTargetCache.findFromAddress(startAddress); + if (depth) { + float depthVal; + const auto format = depth->get().format; + if (format == DepthFmt::Depth16) { + depthVal = (value & 0xffff) / 65535.0f; + } else { + depthVal = (value & 0xffffff) / 16777215.0f; + } + + depthClearOps[depth->get().texture] = depthVal; + + if (format == DepthFmt::Depth24Stencil8) { + const u8 stencilVal = value >> 24; + stencilClearOps[depth->get().texture] = stencilVal; + } + + return; + } + + Helpers::warn("[RendererMTL::ClearBuffer] No buffer found!\n"); +} + +void RendererMTL::displayTransfer(u32 inputAddr, u32 outputAddr, u32 inputSize, u32 outputSize, u32 flags) { + const u32 inputWidth = inputSize & 0xffff; + const u32 inputHeight = inputSize >> 16; + const auto inputFormat = ToColorFormat(Helpers::getBits<8, 3>(flags)); + const auto outputFormat = ToColorFormat(Helpers::getBits<12, 3>(flags)); + const bool verticalFlip = flags & 1; + const PICA::Scaling scaling = static_cast(Helpers::getBits<24, 2>(flags)); + + u32 outputWidth = outputSize & 0xffff; + u32 outputHeight = outputSize >> 16; + + auto srcFramebuffer = getColorRenderTarget(inputAddr, inputFormat, inputWidth, outputHeight); + nextRenderPassName = "Clear before display transfer"; + clearColor(nullptr, srcFramebuffer->texture); + Math::Rect srcRect = srcFramebuffer->getSubRect(inputAddr, outputWidth, outputHeight); + + if (verticalFlip) { + std::swap(srcRect.bottom, srcRect.top); + } + + // Apply scaling for the destination rectangle. + if (scaling == PICA::Scaling::X || scaling == PICA::Scaling::XY) { + outputWidth >>= 1; + } + + if (scaling == PICA::Scaling::XY) { + outputHeight >>= 1; + } + + auto destFramebuffer = getColorRenderTarget(outputAddr, outputFormat, outputWidth, outputHeight); + // TODO: clear if not blitting to the whole framebuffer + Math::Rect destRect = destFramebuffer->getSubRect(outputAddr, outputWidth, outputHeight); + + if (inputWidth != outputWidth) { + // Helpers::warn("Strided display transfer is not handled correctly!\n"); + } + + textureCopyImpl(*srcFramebuffer, *destFramebuffer, srcRect, destRect); +} + +void RendererMTL::textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 inputSize, u32 outputSize, u32 flags) { + // Texture copy size is aligned to 16 byte units + const u32 copySize = totalBytes & ~0xf; + if (copySize == 0) { + Helpers::warn("TextureCopy total bytes less than 16!\n"); + return; + } + + // The width and gap are provided in 16-byte units. + const u32 inputWidth = (inputSize & 0xffff) << 4; + const u32 inputGap = (inputSize >> 16) << 4; + const u32 outputWidth = (outputSize & 0xffff) << 4; + const u32 outputGap = (outputSize >> 16) << 4; + + if (inputGap != 0 || outputGap != 0) { + // Helpers::warn("Strided texture copy\n"); + } + + if (inputWidth != outputWidth) { + Helpers::warn("Input width does not match output width, cannot accelerate texture copy!"); + return; + } + + // Texture copy is a raw data copy in PICA, which means no format or tiling information is provided to the engine. + // Depending if the target surface is linear or tiled, games set inputWidth to either the width of the texture or + // the width multiplied by eight (because tiles are stored linearly in memory). + // To properly accelerate this we must examine each surface individually. For now we assume the most common case + // of tiled surface with RGBA8 format. If our assumption does not hold true, we abort the texture copy as inserting + // that surface is not correct. + + // We assume the source surface is tiled and RGBA8. inputWidth is in bytes so divide it + // by eight * sizePerPixel(RGBA8) to convert it to a useable width. + const u32 bpp = sizePerPixel(PICA::ColorFmt::RGBA8); + const u32 copyStride = (inputWidth + inputGap) / (8 * bpp); + const u32 copyWidth = inputWidth / (8 * bpp); + + // inputHeight/outputHeight are typically set to zero so they cannot be used to get the height of the copy region + // in contrast to display transfer. Compute height manually by dividing the copy size with the copy width. The result + // is the number of vertical tiles so multiply that by eight to get the actual copy height. + u32 copyHeight; + if (inputWidth != 0) [[likely]] { + copyHeight = (copySize / inputWidth) * 8; + } else { + copyHeight = 0; + } + + // Find the source surface. + auto srcFramebuffer = getColorRenderTarget(inputAddr, PICA::ColorFmt::RGBA8, copyStride, copyHeight, false); + if (!srcFramebuffer) { + Helpers::warn("RendererGL::TextureCopy failed to locate src framebuffer!\n"); + return; + } + nextRenderPassName = "Clear before texture copy"; + clearColor(nullptr, srcFramebuffer->texture); + + Math::Rect srcRect = srcFramebuffer->getSubRect(inputAddr, copyWidth, copyHeight); + + // Assume the destination surface has the same format. Unless the surfaces have the same block width, + // texture copy does not make sense. + auto destFramebuffer = getColorRenderTarget(outputAddr, srcFramebuffer->format, copyWidth, copyHeight); + // TODO: clear if not blitting to the whole framebuffer + Math::Rect destRect = destFramebuffer->getSubRect(outputAddr, copyWidth, copyHeight); + + textureCopyImpl(*srcFramebuffer, *destFramebuffer, srcRect, destRect); +} + +void RendererMTL::drawVertices(PICA::PrimType primType, std::span vertices) { + // Color + auto colorRenderTarget = getColorRenderTarget(colourBufferLoc, colourBufferFormat, fbSize[0], fbSize[1]); + + // Depth stencil + const u32 depthControl = regs[PICA::InternalRegs::DepthAndColorMask]; + const bool depthStencilWrite = regs[PICA::InternalRegs::DepthBufferWrite]; + const bool depthEnable = depthControl & 0x1; + const bool depthWriteEnable = Helpers::getBit<12>(depthControl); + const u8 depthFunc = Helpers::getBits<4, 3>(depthControl); + const u8 colorMask = Helpers::getBits<8, 4>(depthControl); + + Metal::DepthStencilHash depthStencilHash{false, 1}; + depthStencilHash.stencilConfig = regs[PICA::InternalRegs::StencilTest]; + depthStencilHash.stencilOpConfig = regs[PICA::InternalRegs::StencilOp]; + const bool stencilEnable = Helpers::getBit<0>(depthStencilHash.stencilConfig); + + std::optional depthStencilRenderTarget = std::nullopt; + if (depthEnable) { + depthStencilHash.depthStencilWrite = depthWriteEnable && depthStencilWrite; + depthStencilHash.depthFunc = depthFunc; + depthStencilRenderTarget = getDepthRenderTarget(); + } else { + if (depthWriteEnable) { + depthStencilHash.depthStencilWrite = true; + depthStencilRenderTarget = getDepthRenderTarget(); + } else if (stencilEnable) { + depthStencilRenderTarget = getDepthRenderTarget(); + } + } + + // Depth uniforms + struct { + float depthScale; + float depthOffset; + bool depthMapEnable; + } depthUniforms; + depthUniforms.depthScale = Floats::f24::fromRaw(regs[PICA::InternalRegs::DepthScale] & 0xffffff).toFloat32(); + depthUniforms.depthOffset = Floats::f24::fromRaw(regs[PICA::InternalRegs::DepthOffset] & 0xffffff).toFloat32(); + depthUniforms.depthMapEnable = regs[PICA::InternalRegs::DepthmapEnable] & 1; + + // -------- Pipeline -------- + Metal::DrawPipelineHash pipelineHash{colorRenderTarget->format, DepthFmt::Unknown1}; + if (depthStencilRenderTarget) { + pipelineHash.depthFmt = depthStencilRenderTarget->format; + } + pipelineHash.fragHash.lightingEnabled = regs[0x008F] & 1; + pipelineHash.fragHash.lightingNumLights = regs[0x01C2] & 0x7; + pipelineHash.fragHash.lightingConfig1 = regs[0x01C4u]; + pipelineHash.fragHash.alphaControl = regs[0x104]; + + // Blending and logic op + pipelineHash.blendEnabled = (regs[PICA::InternalRegs::ColourOperation] & (1 << 8)) != 0; + pipelineHash.colorWriteMask = colorMask; + + u8 logicOp = 3; // Copy, which doesn't do anything + if (pipelineHash.blendEnabled) { + pipelineHash.blendControl = regs[PICA::InternalRegs::BlendFunc]; + } else { + logicOp = Helpers::getBits<0, 4>(regs[PICA::InternalRegs::LogicOp]); + } + + MTL::RenderPipelineState* pipeline = drawPipelineCache.get(pipelineHash); + + // Depth stencil state + MTL::DepthStencilState* depthStencilState = depthStencilCache.get(depthStencilHash); + + // -------- Render -------- + MTL::RenderPassDescriptor* renderPassDescriptor = MTL::RenderPassDescriptor::alloc()->init(); + bool doesClear = clearColor(renderPassDescriptor, colorRenderTarget->texture); + if (depthStencilRenderTarget) { + if (clearDepth(renderPassDescriptor, depthStencilRenderTarget->texture)) + doesClear = true; + if (depthStencilRenderTarget->format == DepthFmt::Depth24Stencil8) { + if (clearStencil(renderPassDescriptor, depthStencilRenderTarget->texture)) + doesClear = true; + } + } + + nextRenderPassName = "Draw vertices"; + beginRenderPassIfNeeded(renderPassDescriptor, doesClear, colorRenderTarget->texture, (depthStencilRenderTarget ? depthStencilRenderTarget->texture : nullptr)); + + // Update the LUT texture if necessary + if (gpu.lightingLUTDirty) { + updateLightingLUT(renderCommandEncoder); + } + if (gpu.fogLUTDirty) { + updateFogLUT(renderCommandEncoder); + } + + renderCommandEncoder->setRenderPipelineState(pipeline); + renderCommandEncoder->setDepthStencilState(depthStencilState); + // If size is < 4KB, use inline vertex data, otherwise use a buffer + if (vertices.size_bytes() < 4 * 1024) { + renderCommandEncoder->setVertexBytes(vertices.data(), vertices.size_bytes(), VERTEX_BUFFER_BINDING_INDEX); + } else { + Metal::BufferHandle buffer = vertexBufferCache.get(vertices.data(), vertices.size_bytes()); + renderCommandEncoder->setVertexBuffer(buffer.buffer, buffer.offset, VERTEX_BUFFER_BINDING_INDEX); + } + + // Viewport + const u32 viewportX = regs[PICA::InternalRegs::ViewportXY] & 0x3ff; + const u32 viewportY = (regs[PICA::InternalRegs::ViewportXY] >> 16) & 0x3ff; + const u32 viewportWidth = Floats::f24::fromRaw(regs[PICA::InternalRegs::ViewportWidth] & 0xffffff).toFloat32() * 2.0f; + const u32 viewportHeight = Floats::f24::fromRaw(regs[PICA::InternalRegs::ViewportHeight] & 0xffffff).toFloat32() * 2.0f; + const auto rect = colorRenderTarget->getSubRect(colourBufferLoc, fbSize[0], fbSize[1]); + MTL::Viewport viewport{double(rect.left + viewportX), double(rect.bottom + viewportY), double(viewportWidth), double(viewportHeight), 0.0, 1.0}; + renderCommandEncoder->setViewport(viewport); + + // Blend color + if (pipelineHash.blendEnabled) { + u32 constantColor = regs[PICA::InternalRegs::BlendColour]; + const u8 r = constantColor & 0xff; + const u8 g = Helpers::getBits<8, 8>(constantColor); + const u8 b = Helpers::getBits<16, 8>(constantColor); + const u8 a = Helpers::getBits<24, 8>(constantColor); + + renderCommandEncoder->setBlendColor(r / 255.0f, g / 255.0f, b / 255.0f, a / 255.0f); + } + + // Stencil reference + if (stencilEnable) { + const s8 reference = s8(Helpers::getBits<16, 8>(depthStencilHash.stencilConfig)); // Signed reference value + renderCommandEncoder->setStencilReferenceValue(reference); + } + + // Bind resources + setupTextureEnvState(renderCommandEncoder); + bindTexturesToSlots(renderCommandEncoder); + renderCommandEncoder->setVertexBytes(®s[0x48], (0x200 - 0x48) * sizeof(regs[0]), 0); + renderCommandEncoder->setFragmentBytes(®s[0x48], (0x200 - 0x48) * sizeof(regs[0]), 0); + renderCommandEncoder->setVertexBytes(&depthUniforms, sizeof(depthUniforms), 2); + renderCommandEncoder->setFragmentBytes(&logicOp, sizeof(logicOp), 2); + + renderCommandEncoder->drawPrimitives(toMTLPrimitiveType(primType), NS::UInteger(0), NS::UInteger(vertices.size())); +} + +void RendererMTL::screenshot(const std::string& name) { + // TODO: implement + Helpers::warn("RendererMTL::screenshot not implemented"); +} + +void RendererMTL::deinitGraphicsContext() { + reset(); + + // Release + copyToLutTexturePipeline->release(); + displayPipeline->release(); + defaultDepthStencilState->release(); + lutTexture->release(); + linearSampler->release(); + nearestSampler->release(); + library->release(); + commandQueue->release(); + device->release(); +} + +std::optional RendererMTL::getColorRenderTarget( + u32 addr, PICA::ColorFmt format, u32 width, u32 height, bool createIfnotFound +) { + // Try to find an already existing buffer that contains the provided address + // This is a more relaxed check compared to getColourFBO as display transfer/texcopy may refer to + // subrect of a surface and in case of texcopy we don't know the format of the surface. + auto buffer = colorRenderTargetCache.findFromAddress(addr); + if (buffer.has_value()) { + return buffer.value().get(); + } + + if (!createIfnotFound) { + return std::nullopt; + } + + // Otherwise create and cache a new buffer. + Metal::ColorRenderTarget sampleBuffer(device, addr, format, width, height); + + return colorRenderTargetCache.add(sampleBuffer); +} + +Metal::DepthStencilRenderTarget& RendererMTL::getDepthRenderTarget() { + Metal::DepthStencilRenderTarget sampleBuffer(device, depthBufferLoc, depthBufferFormat, fbSize[0], fbSize[1]); + auto buffer = depthStencilRenderTargetCache.find(sampleBuffer); + + if (buffer.has_value()) { + return buffer.value().get(); + } else { + return depthStencilRenderTargetCache.add(sampleBuffer); + } +} + +Metal::Texture& RendererMTL::getTexture(Metal::Texture& tex) { + auto buffer = textureCache.find(tex); + + if (buffer.has_value()) { + return buffer.value().get(); + } else { + const auto textureData = std::span{gpu.getPointerPhys(tex.location), tex.sizeInBytes()}; // Get pointer to the texture data in 3DS memory + Metal::Texture& newTex = textureCache.add(tex); + newTex.decodeTexture(textureData); + + return newTex; + } +} + +void RendererMTL::setupTextureEnvState(MTL::RenderCommandEncoder* encoder) { + static constexpr std::array ioBases = { + PICA::InternalRegs::TexEnv0Source, PICA::InternalRegs::TexEnv1Source, PICA::InternalRegs::TexEnv2Source, + PICA::InternalRegs::TexEnv3Source, PICA::InternalRegs::TexEnv4Source, PICA::InternalRegs::TexEnv5Source, + }; + + struct { + u32 textureEnvSourceRegs[6]; + u32 textureEnvOperandRegs[6]; + u32 textureEnvCombinerRegs[6]; + u32 textureEnvScaleRegs[6]; + } envState; + u32 textureEnvColourRegs[6]; + + for (int i = 0; i < 6; i++) { + const u32 ioBase = ioBases[i]; + + envState.textureEnvSourceRegs[i] = regs[ioBase]; + envState.textureEnvOperandRegs[i] = regs[ioBase + 1]; + envState.textureEnvCombinerRegs[i] = regs[ioBase + 2]; + textureEnvColourRegs[i] = regs[ioBase + 3]; + envState.textureEnvScaleRegs[i] = regs[ioBase + 4]; + } + + encoder->setVertexBytes(&textureEnvColourRegs, sizeof(textureEnvColourRegs), 1); + encoder->setFragmentBytes(&envState, sizeof(envState), 1); +} + +void RendererMTL::bindTexturesToSlots(MTL::RenderCommandEncoder* encoder) { + static constexpr std::array ioBases = { + PICA::InternalRegs::Tex0BorderColor, + PICA::InternalRegs::Tex1BorderColor, + PICA::InternalRegs::Tex2BorderColor, + }; + + for (int i = 0; i < 3; i++) { + if ((regs[PICA::InternalRegs::TexUnitCfg] & (1 << i)) == 0) { + continue; + } + + const size_t ioBase = ioBases[i]; + + const u32 dim = regs[ioBase + 1]; + const u32 config = regs[ioBase + 2]; + const u32 height = dim & 0x7ff; + const u32 width = Helpers::getBits<16, 11>(dim); + const u32 addr = (regs[ioBase + 4] & 0x0FFFFFFF) << 3; + u32 format = regs[ioBase + (i == 0 ? 13 : 5)] & 0xF; + + if (addr != 0) [[likely]] { + Metal::Texture targetTex(device, addr, static_cast(format), width, height, config); + auto tex = getTexture(targetTex); + encoder->setFragmentTexture(tex.texture, i); + encoder->setFragmentSamplerState(tex.sampler ? tex.sampler : nearestSampler, i); + } else { + // TODO: bind a dummy texture? + } + } + + // LUT texture + encoder->setFragmentTexture(lutTexture, 3); + encoder->setFragmentSamplerState(linearSampler, 3); +} + +void RendererMTL::updateLightingLUT(MTL::RenderCommandEncoder* encoder) { + gpu.lightingLUTDirty = false; + std::array lightingLut = {0.0f}; + + for (int i = 0; i < gpu.lightingLUT.size(); i += 2) { + uint64_t value = gpu.lightingLUT[i >> 1] & 0xFFF; + lightingLut[i] = (float)(value << 4) / 65535.0f; + } + + //for (int i = 0; i < Lights::LUT_Count; i++) { + // lutTexture->replaceRegion(MTL::Region(0, 0, LIGHT_LUT_TEXTURE_WIDTH, 1), 0, i, u16_lightinglut.data() + LIGHT_LUT_TEXTURE_WIDTH * i, 0, 0); + //} + + renderCommandEncoder->setRenderPipelineState(copyToLutTexturePipeline); + renderCommandEncoder->setDepthStencilState(defaultDepthStencilState); + renderCommandEncoder->setVertexTexture(lutTexture, 0); + Metal::BufferHandle buffer = vertexBufferCache.get(lightingLut.data(), sizeof(lightingLut)); + renderCommandEncoder->setVertexBuffer(buffer.buffer, buffer.offset, 0); + u32 arrayOffset = 0; + renderCommandEncoder->setVertexBytes(&arrayOffset, sizeof(u32), 1); + + renderCommandEncoder->drawPrimitives(MTL::PrimitiveTypeTriangleStrip, NS::UInteger(0), GPU::LightingLutSize); +} + +void RendererMTL::updateFogLUT(MTL::RenderCommandEncoder* encoder) { + gpu.fogLUTDirty = false; + std::array fogLut = {0.0f}; + + for (int i = 0; i < fogLut.size(); i += 2) { + const uint32_t value = gpu.fogLUT[i >> 1]; + int32_t diff = value & 0x1fff; + diff = (diff << 19) >> 19; // Sign extend the 13-bit value to 32 bits + const float fogDifference = float(diff) / 2048.0f; + const float fogValue = float((value >> 13) & 0x7ff) / 2048.0f; + + fogLut[i] = fogValue; + fogLut[i + 1] = fogDifference; + } + + renderCommandEncoder->setRenderPipelineState(copyToLutTexturePipeline); + renderCommandEncoder->setDepthStencilState(defaultDepthStencilState); + renderCommandEncoder->setVertexTexture(lutTexture, 0); + //Metal::BufferHandle buffer = vertexBufferCache.get(fogLut.data(), sizeof(fogLut)); + //renderCommandEncoder->setVertexBuffer(buffer.buffer, buffer.offset, 0); + renderCommandEncoder->setVertexBytes(fogLut.data(), sizeof(fogLut), 0); + u32 arrayOffset = (u32)Lights::LUT_Count; + renderCommandEncoder->setVertexBytes(&arrayOffset, sizeof(u32), 1); + + renderCommandEncoder->drawPrimitives(MTL::PrimitiveTypeTriangleStrip, NS::UInteger(0), NS::UInteger(128)); +} + +void RendererMTL::textureCopyImpl(Metal::ColorRenderTarget& srcFramebuffer, Metal::ColorRenderTarget& destFramebuffer, const Math::Rect& srcRect, const Math::Rect& destRect) { + nextRenderPassName = "Texture copy"; + MTL::RenderPassDescriptor* renderPassDescriptor = MTL::RenderPassDescriptor::alloc()->init(); + // TODO: clearColor sets the load action to load if it didn't find any clear, but that is unnecessary if we are doing a copy to the whole texture + bool doesClear = clearColor(renderPassDescriptor, destFramebuffer.texture); + beginRenderPassIfNeeded(renderPassDescriptor, doesClear, destFramebuffer.texture); + + // Pipeline + Metal::BlitPipelineHash hash{destFramebuffer.format, DepthFmt::Unknown1}; + auto blitPipeline = blitPipelineCache.get(hash); + + renderCommandEncoder->setRenderPipelineState(blitPipeline); + + // Viewport + renderCommandEncoder->setViewport(MTL::Viewport{double(destRect.left), double(destRect.bottom), double(destRect.right - destRect.left), double(destRect.top - destRect.bottom), 0.0, 1.0}); + float srcRectNDC[4] = {srcRect.left / (float)srcFramebuffer.size.u(), srcRect.bottom / (float)srcFramebuffer.size.v(), (srcRect.right - srcRect.left) / (float)srcFramebuffer.size.u(), (srcRect.top - srcRect.bottom) / (float)srcFramebuffer.size.v()}; + + // Bind resources + renderCommandEncoder->setVertexBytes(&srcRectNDC, sizeof(srcRectNDC), 0); + renderCommandEncoder->setFragmentTexture(srcFramebuffer.texture, 0); + renderCommandEncoder->setFragmentSamplerState(nearestSampler, 0); + + renderCommandEncoder->drawPrimitives(MTL::PrimitiveTypeTriangleStrip, NS::UInteger(0), NS::UInteger(4)); +} diff --git a/src/host_shaders/metal_copy_to_lut_texture.metal b/src/host_shaders/metal_copy_to_lut_texture.metal new file mode 100644 index 00000000..40a7f50d --- /dev/null +++ b/src/host_shaders/metal_copy_to_lut_texture.metal @@ -0,0 +1,9 @@ +#include +using namespace metal; + +constant ushort lutTextureWidth [[function_constant(0)]]; + +// The copy is done in a vertex shader instead of a compute kernel, since dispatching compute would require ending the render pass +vertex void vertexCopyToLutTexture(uint vid [[vertex_id]], texture2d out [[texture(0)]], constant float2* data [[buffer(0)]], constant uint& arrayOffset [[buffer(1)]]) { + out.write(float4(data[vid], 0.0, 0.0), uint2(vid % lutTextureWidth, arrayOffset + vid / lutTextureWidth)); +} diff --git a/src/host_shaders/metal_shaders.metal b/src/host_shaders/metal_shaders.metal new file mode 100644 index 00000000..95f417c7 --- /dev/null +++ b/src/host_shaders/metal_shaders.metal @@ -0,0 +1,782 @@ +#include +using namespace metal; + +struct BasicVertexOut { + float4 position [[position]]; + float2 uv; +}; + +constant float4 displayPositions[4] = { + float4(-1.0, -1.0, 0.0, 1.0), + float4( 1.0, -1.0, 0.0, 1.0), + float4(-1.0, 1.0, 0.0, 1.0), + float4( 1.0, 1.0, 0.0, 1.0) +}; + +constant float2 displayTexCoord[4] = { + float2(0.0, 1.0), + float2(0.0, 0.0), + float2(1.0, 1.0), + float2(1.0, 0.0) +}; + +vertex BasicVertexOut vertexDisplay(uint vid [[vertex_id]]) { + BasicVertexOut out; + out.position = displayPositions[vid]; + out.uv = displayTexCoord[vid]; + + return out; +} + +fragment float4 fragmentDisplay(BasicVertexOut in [[stage_in]], texture2d tex [[texture(0)]], sampler samplr [[sampler(0)]]) { + return tex.sample(samplr, in.uv); +} + +struct NDCViewport { + float2 offset; + float2 scale; +}; + +vertex BasicVertexOut vertexBlit(uint vid [[vertex_id]], constant NDCViewport& viewport [[buffer(0)]]) { + BasicVertexOut out; + out.uv = float2((vid << 1) & 2, vid & 2); + out.position = float4(out.uv * 2.0 - 1.0, 0.0, 1.0); + out.position.y = -out.position.y; + out.uv = out.uv * viewport.scale + viewport.offset; + + return out; +} + +fragment float4 fragmentBlit(BasicVertexOut in [[stage_in]], texture2d tex [[texture(0)]], sampler samplr [[sampler(0)]]) { + return tex.sample(samplr, in.uv); +} + +struct PicaRegs { + uint regs[0x200 - 0x48]; + + uint read(uint reg) constant { + return regs[reg - 0x48]; + } +}; + +struct VertTEV { + uint textureEnvColor[6]; +}; + +float4 abgr8888ToFloat4(uint abgr) { + const float scale = 1.0 / 255.0; + + return scale * float4(float(abgr & 0xffu), float((abgr >> 8) & 0xffu), float((abgr >> 16) & 0xffu), float(abgr >> 24)); +} + +struct DrawVertexIn { + float4 position [[attribute(0)]]; + float4 quaternion [[attribute(1)]]; + float4 color [[attribute(2)]]; + float2 texCoord0 [[attribute(3)]]; + float2 texCoord1 [[attribute(4)]]; + float texCoord0W [[attribute(5)]]; + float3 view [[attribute(6)]]; + float2 texCoord2 [[attribute(7)]]; +}; + +// Metal cannot return arrays from vertex functions, this is an ugly workaround +struct EnvColor { + float4 c0; + float4 c1; + float4 c2; + float4 c3; + float4 c4; + float4 c5; + + thread float4& operator[](int i) { + switch (i) { + case 0: return c0; + case 1: return c1; + case 2: return c2; + case 3: return c3; + case 4: return c4; + case 5: return c5; + default: return c0; + } + } +}; + +float3 rotateFloat3ByQuaternion(float3 v, float4 q) { + float3 u = q.xyz; + float s = q.w; + + return 2.0 * dot(u, v) * u + (s * s - dot(u, u)) * v + 2.0 * s * cross(u, v); +} + +// Convert an arbitrary-width floating point literal to an f32 +float decodeFP(uint hex, uint E, uint M) { + uint width = M + E + 1u; + uint bias = 128u - (1u << (E - 1u)); + uint exponent = (hex >> M) & ((1u << E) - 1u); + uint mantissa = hex & ((1u << M) - 1u); + uint sign = (hex >> (E + M)) << 31u; + + if ((hex & ((1u << (width - 1u)) - 1u)) != 0u) { + if (exponent == (1u << E) - 1u) + exponent = 255u; + else + exponent += bias; + hex = sign | (mantissa << (23u - M)) | (exponent << 23u); + } else { + hex = sign; + } + + return as_type(hex); +} + +struct DepthUniforms { + float depthScale; + float depthOffset; + bool depthMapEnable; +}; + +struct DrawVertexOut { + float4 position [[position]]; + float4 quaternion; + float4 color; + float3 texCoord0; + float2 texCoord1; + float2 texCoord2; + float3 view; + float3 normal; + float3 tangent; + float3 bitangent; + EnvColor textureEnvColor [[flat]]; + float4 textureEnvBufferColor [[flat]]; +}; + +struct DrawVertexOutWithClip { + DrawVertexOut out; + float clipDistance [[clip_distance]] [2]; +}; + +// TODO: check this +float transformZ(float z, float w, constant DepthUniforms& depthUniforms) { + z = z / w * depthUniforms.depthScale + depthUniforms.depthOffset; + if (!depthUniforms.depthMapEnable) { + z *= w; + } + + return z * w; +} + +vertex DrawVertexOutWithClip vertexDraw(DrawVertexIn in [[stage_in]], constant PicaRegs& picaRegs [[buffer(0)]], constant VertTEV& tev [[buffer(1)]], constant DepthUniforms& depthUniforms [[buffer(2)]]) { + DrawVertexOut out; + + // Position + out.position = in.position; + // Flip the y position + out.position.y = -out.position.y; + + // Apply depth uniforms + out.position.z = transformZ(out.position.z, out.position.w, depthUniforms); + + // Color + out.color = min(abs(in.color), 1.0); + + // Texture coordinates + out.texCoord0 = float3(in.texCoord0, in.texCoord0W); + out.texCoord0.y = 1.0 - out.texCoord0.y; + out.texCoord1 = in.texCoord1; + out.texCoord1.y = 1.0 - out.texCoord1.y; + out.texCoord2 = in.texCoord2; + out.texCoord2.y = 1.0 - out.texCoord2.y; + + // View + out.view = in.view; + + // TBN + out.normal = normalize(rotateFloat3ByQuaternion(float3(0.0, 0.0, 1.0), in.quaternion)); + out.tangent = normalize(rotateFloat3ByQuaternion(float3(1.0, 0.0, 0.0), in.quaternion)); + out.bitangent = normalize(rotateFloat3ByQuaternion(float3(0.0, 1.0, 0.0), in.quaternion)); + out.quaternion = in.quaternion; + + // Environment + for (int i = 0; i < 6; i++) { + out.textureEnvColor[i] = abgr8888ToFloat4(tev.textureEnvColor[i]); + } + + out.textureEnvBufferColor = abgr8888ToFloat4(picaRegs.read(0xFDu)); + + DrawVertexOutWithClip outWithClip; + outWithClip.out = out; + + // Parse clipping plane registers + float4 clipData = float4( + decodeFP(picaRegs.read(0x48u) & 0xffffffu, 7u, 16u), decodeFP(picaRegs.read(0x49u) & 0xffffffu, 7u, 16u), + decodeFP(picaRegs.read(0x4Au) & 0xffffffu, 7u, 16u), decodeFP(picaRegs.read(0x4Bu) & 0xffffffu, 7u, 16u) + ); + + // There's also another, always-on clipping plane based on vertex z + // TODO: transform + outWithClip.clipDistance[0] = -in.position.z; + outWithClip.clipDistance[1] = dot(clipData, in.position); + + return outWithClip; +} + +constant bool lightingEnabled [[function_constant(0)]]; +constant uint8_t lightingNumLights [[function_constant(1)]]; +constant uint32_t lightingConfig1 [[function_constant(2)]]; +constant uint16_t alphaControl [[function_constant(3)]]; + +struct Globals { + bool error_unimpl; + + float4 tevSources[16]; + float4 tevNextPreviousBuffer; + bool tevUnimplementedSourceFlag = false; + + uint GPUREG_LIGHTING_LUTINPUT_SCALE; + uint GPUREG_LIGHTING_LUTINPUT_ABS; + uint GPUREG_LIGHTING_LUTINPUT_SELECT; + uint GPUREG_LIGHTi_CONFIG; + + // HACK + //bool lightingEnabled; + //uint8_t lightingNumLights; + //uint32_t lightingConfig1; + //uint16_t alphaControl; + + float3 normal; +}; + +// See docs/lighting.md +constant uint samplerEnabledBitfields[2] = {0x7170e645u, 0x7f013fefu}; + +bool isSamplerEnabled(uint environment_id, uint lut_id) { + uint index = 7 * environment_id + lut_id; + uint arrayIndex = (index >> 5); + return (samplerEnabledBitfields[arrayIndex] & (1u << (index & 31u))) != 0u; +} + +struct FragTEV { + uint textureEnvSource[6]; + uint textureEnvOperand[6]; + uint textureEnvCombiner[6]; + uint textureEnvScale[6]; + + float4 fetchSource(thread Globals& globals, uint src_id) constant { + if (src_id >= 6u && src_id < 13u) { + globals.tevUnimplementedSourceFlag = true; + } + + return globals.tevSources[src_id]; + } + + float4 getColorAndAlphaSource(thread Globals& globals, int tev_id, int src_id) constant { + float4 result; + + float4 colorSource = fetchSource(globals, (textureEnvSource[tev_id] >> (src_id * 4)) & 15u); + float4 alphaSource = fetchSource(globals, (textureEnvSource[tev_id] >> (src_id * 4 + 16)) & 15u); + + uint colorOperand = (textureEnvOperand[tev_id] >> (src_id * 4)) & 15u; + uint alphaOperand = (textureEnvOperand[tev_id] >> (12 + src_id * 4)) & 7u; + + // TODO: figure out what the undocumented values do + switch (colorOperand) { + case 0u: result.rgb = colorSource.rgb; break; // Source color + case 1u: result.rgb = 1.0 - colorSource.rgb; break; // One minus source color + case 2u: result.rgb = float3(colorSource.a); break; // Source alpha + case 3u: result.rgb = float3(1.0 - colorSource.a); break; // One minus source alpha + case 4u: result.rgb = float3(colorSource.r); break; // Source red + case 5u: result.rgb = float3(1.0 - colorSource.r); break; // One minus source red + case 8u: result.rgb = float3(colorSource.g); break; // Source green + case 9u: result.rgb = float3(1.0 - colorSource.g); break; // One minus source green + case 12u: result.rgb = float3(colorSource.b); break; // Source blue + case 13u: result.rgb = float3(1.0 - colorSource.b); break; // One minus source blue + default: break; + } + + // TODO: figure out what the undocumented values do + switch (alphaOperand) { + case 0u: result.a = alphaSource.a; break; // Source alpha + case 1u: result.a = 1.0 - alphaSource.a; break; // One minus source alpha + case 2u: result.a = alphaSource.r; break; // Source red + case 3u: result.a = 1.0 - alphaSource.r; break; // One minus source red + case 4u: result.a = alphaSource.g; break; // Source green + case 5u: result.a = 1.0 - alphaSource.g; break; // One minus source green + case 6u: result.a = alphaSource.b; break; // Source blue + case 7u: result.a = 1.0 - alphaSource.b; break; // One minus source blue + default: break; + } + + return result; + } + + float4 calculateCombiner(thread Globals& globals, int tev_id) constant { + float4 source0 = getColorAndAlphaSource(globals, tev_id, 0); + float4 source1 = getColorAndAlphaSource(globals, tev_id, 1); + float4 source2 = getColorAndAlphaSource(globals, tev_id, 2); + + uint colorCombine = textureEnvCombiner[tev_id] & 15u; + uint alphaCombine = (textureEnvCombiner[tev_id] >> 16) & 15u; + + float4 result = float4(1.0); + + // TODO: figure out what the undocumented values do + switch (colorCombine) { + case 0u: result.rgb = source0.rgb; break; // Replace + case 1u: result.rgb = source0.rgb * source1.rgb; break; // Modulate + case 2u: result.rgb = min(float3(1.0), source0.rgb + source1.rgb); break; // Add + case 3u: result.rgb = clamp(source0.rgb + source1.rgb - 0.5, 0.0, 1.0); break; // Add signed + case 4u: result.rgb = mix(source1.rgb, source0.rgb, source2.rgb); break; // Interpolate + case 5u: result.rgb = max(source0.rgb - source1.rgb, 0.0); break; // Subtract + case 6u: result.rgb = float3(4.0 * dot(source0.rgb - 0.5, source1.rgb - 0.5)); break; // Dot3 RGB + case 7u: result = float4(4.0 * dot(source0.rgb - 0.5, source1.rgb - 0.5)); break; // Dot3 RGBA + case 8u: result.rgb = min(source0.rgb * source1.rgb + source2.rgb, 1.0); break; // Multiply then add + case 9u: result.rgb = min((source0.rgb + source1.rgb), 1.0) * source2.rgb; break; // Add then multiply + default: break; + } + + if (colorCombine != 7u) { // The color combiner also writes the alpha channel in the "Dot3 RGBA" mode. + // TODO: figure out what the undocumented values do + // TODO: test if the alpha combiner supports all the same modes as the color combiner. + switch (alphaCombine) { + case 0u: result.a = source0.a; break; // Replace + case 1u: result.a = source0.a * source1.a; break; // Modulate + case 2u: result.a = min(1.0, source0.a + source1.a); break; // Add + case 3u: result.a = clamp(source0.a + source1.a - 0.5, 0.0, 1.0); break; // Add signed + case 4u: result.a = mix(source1.a, source0.a, source2.a); break; // Interpolate + case 5u: result.a = max(0.0, source0.a - source1.a); break; // Subtract + case 8u: result.a = min(source0.a * source1.a + source2.a, 1.0); break; // Multiply then add + case 9u: result.a = min(source0.a + source1.a, 1.0) * source2.a; break; // Add then multiply + default: break; + } + } + + result.rgb *= float(1 << (textureEnvScale[tev_id] & 3u)); + result.a *= float(1 << ((textureEnvScale[tev_id] >> 16) & 3u)); + + return result; + } +}; + +enum class LogicOp : uint8_t { + Clear = 0, + And = 1, + AndReverse = 2, + Copy = 3, + Set = 4, + CopyInverted = 5, + NoOp = 6, + Invert = 7, + Nand = 8, + Or = 9, + Nor = 10, + Xor = 11, + Equiv = 12, + AndInverted = 13, + OrReverse = 14, + OrInverted = 15 +}; + +uint4 performLogicOpU(LogicOp logicOp, uint4 s, uint4 d) { + switch (logicOp) { + case LogicOp::Clear: return as_type(float4(0.0)); + case LogicOp::And: return s & d; + case LogicOp::AndReverse: return s & ~d; + case LogicOp::Copy: return s; + case LogicOp::Set: return as_type(float4(1.0)); + case LogicOp::CopyInverted: return ~s; + case LogicOp::NoOp: return d; + case LogicOp::Invert: return ~d; + case LogicOp::Nand: return ~(s & d); + case LogicOp::Or: return s | d; + case LogicOp::Nor: return ~(s | d); + case LogicOp::Xor: return s ^ d; + case LogicOp::Equiv: return ~(s ^ d); + case LogicOp::AndInverted: return ~s & d; + case LogicOp::OrReverse: return s | ~d; + case LogicOp::OrInverted: return ~s | d; + } +} + +#define D0_LUT 0u +#define D1_LUT 1u +#define SP_LUT 2u +#define FR_LUT 3u +#define RB_LUT 4u +#define RG_LUT 5u +#define RR_LUT 6u + +#define FOG_INDEX 24 + +float lutLookup(texture2d texLut, uint lut, uint index) { + return texLut.read(uint2(index, lut)).r; +} + +float lightLutLookup(thread Globals& globals, thread DrawVertexOut& in, constant PicaRegs& picaRegs, texture2d texLut, uint environment_id, uint lut_id, uint light_id, float3 light_vector, float3 half_vector) { + uint lut_index; + int bit_in_config1; + if (lut_id == SP_LUT) { + // These are the spotlight attenuation LUTs + bit_in_config1 = 8 + int(light_id & 7u); + lut_index = 8u + light_id; + } else if (lut_id <= 6) { + bit_in_config1 = 16 + int(lut_id); + lut_index = lut_id; + } else { + globals.error_unimpl = true; + } + + bool current_sampler_enabled = isSamplerEnabled(environment_id, lut_id); // 7 luts per environment + + if (!current_sampler_enabled || (extract_bits(lightingConfig1, bit_in_config1, 1) != 0u)) { + return 1.0; + } + + uint scale_id = extract_bits(globals.GPUREG_LIGHTING_LUTINPUT_SCALE, int(lut_id) << 2, 3); + float scale = float(1u << scale_id); + if (scale_id >= 6u) scale /= 256.0; + + float delta = 1.0; + uint input_id = extract_bits(globals.GPUREG_LIGHTING_LUTINPUT_SELECT, int(lut_id) << 2, 3); + switch (input_id) { + case 0u: { + delta = dot(globals.normal, normalize(half_vector)); + break; + } + case 1u: { + delta = dot(normalize(in.view), normalize(half_vector)); + break; + } + case 2u: { + delta = dot(globals.normal, normalize(in.view)); + break; + } + case 3u: { + delta = dot(light_vector, globals.normal); + break; + } + case 4u: { + int GPUREG_LIGHTi_SPOTDIR_LOW = int(picaRegs.read(0x0146u + (light_id << 4u))); + int GPUREG_LIGHTi_SPOTDIR_HIGH = int(picaRegs.read(0x0147u + (light_id << 4u))); + + // Sign extend them. Normally bitfieldExtract would do that but it's missing on some versions + // of GLSL so we do it manually + int se_x = extract_bits(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13); + int se_y = extract_bits(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13); + int se_z = extract_bits(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13); + + if ((se_x & 0x1000) == 0x1000) se_x |= 0xffffe000; + if ((se_y & 0x1000) == 0x1000) se_y |= 0xffffe000; + if ((se_z & 0x1000) == 0x1000) se_z |= 0xffffe000; + + // These are fixed point 1.1.11 values, so we need to convert them to float + float x = float(se_x) / 2047.0; + float y = float(se_y) / 2047.0; + float z = float(se_z) / 2047.0; + float3 spotlight_vector = float3(x, y, z); + delta = dot(light_vector, spotlight_vector); // spotlight direction is negated so we don't negate light_vector + break; + } + case 5u: { + delta = 1.0; // TODO: cos (aka CP); + globals.error_unimpl = true; + break; + } + default: { + delta = 1.0; + globals.error_unimpl = true; + break; + } + } + + // 0 = enabled + if (extract_bits(globals.GPUREG_LIGHTING_LUTINPUT_ABS, 1 + (int(lut_id) << 2), 1) == 0u) { + // Two sided diffuse + if (extract_bits(globals.GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) { + delta = max(delta, 0.0); + } else { + delta = abs(delta); + } + int index = int(clamp(floor(delta * 255.0), 0.f, 255.f)); + return lutLookup(texLut, lut_index, index) * scale; + } else { + // Range is [-1, 1] so we need to map it to [0, 1] + int index = int(clamp(floor(delta * 128.0), -128.f, 127.f)); + if (index < 0) index += 256; + return lutLookup(texLut, lut_index, index) * scale; + } +} + +float3 regToColor(uint reg) { + // Normalization scale to convert from [0...255] to [0.0...1.0] + const float scale = 1.0 / 255.0; + + return scale * float3(float(extract_bits(reg, 20, 8)), float(extract_bits(reg, 10, 8)), float(extract_bits(reg, 00, 8))); +} + +// Implements the following algorthm: https://mathb.in/26766 +void calcLighting(thread Globals& globals, thread DrawVertexOut& in, constant PicaRegs& picaRegs, texture2d texLut, sampler linearSampler, thread float4& primaryColor, thread float4& secondaryColor) { + // Quaternions describe a transformation from surface-local space to eye space. + // In surface-local space, by definition (and up to permutation) the normal vector is (0,0,1), + // the tangent vector is (1,0,0), and the bitangent vector is (0,1,0). + //float3 normal = normalize(in.normal); + //float3 tangent = normalize(in.tangent); + //float3 bitangent = normalize(in.bitangent); + //float3 view = normalize(in.view); + + uint GPUREG_LIGHTING_LIGHT_PERMUTATION = picaRegs.read(0x01D9u); + + primaryColor = float4(0.0, 0.0, 0.0, 1.0); + secondaryColor = float4(0.0, 0.0, 0.0, 1.0); + + uint GPUREG_LIGHTING_CONFIG0 = picaRegs.read(0x01C3u); + globals.GPUREG_LIGHTING_LUTINPUT_SCALE = picaRegs.read(0x01D2u); + globals.GPUREG_LIGHTING_LUTINPUT_ABS = picaRegs.read(0x01D0u); + globals.GPUREG_LIGHTING_LUTINPUT_SELECT = picaRegs.read(0x01D1u); + + uint bumpMode = extract_bits(GPUREG_LIGHTING_CONFIG0, 28, 2); + + // Bump mode is ignored for now because it breaks some games ie. Toad Treasure Tracker + switch (bumpMode) { + default: { + globals.normal = rotateFloat3ByQuaternion(float3(0.0, 0.0, 1.0), in.quaternion); + break; + } + } + + float4 diffuseSum = float4(0.0, 0.0, 0.0, 1.0); + float4 specularSum = float4(0.0, 0.0, 0.0, 1.0); + + uint environmentId = extract_bits(GPUREG_LIGHTING_CONFIG0, 4, 4); + bool clampHighlights = extract_bits(GPUREG_LIGHTING_CONFIG0, 27, 1) == 1u; + + uint lightId; + float3 lightVector = float3(0.0); + float3 halfVector = float3(0.0); + + for (uint i = 0u; i < lightingNumLights + 1; i++) { + lightId = extract_bits(GPUREG_LIGHTING_LIGHT_PERMUTATION, int(i) << 2, 3); + + uint GPUREG_LIGHTi_SPECULAR0 = picaRegs.read(0x0140u + (lightId << 4u)); + uint GPUREG_LIGHTi_SPECULAR1 = picaRegs.read(0x0141u + (lightId << 4u)); + uint GPUREG_LIGHTi_DIFFUSE = picaRegs.read(0x0142u + (lightId << 4u)); + uint GPUREG_LIGHTi_AMBIENT = picaRegs.read(0x0143u + (lightId << 4u)); + uint GPUREG_LIGHTi_VECTOR_LOW = picaRegs.read(0x0144u + (lightId << 4u)); + uint GPUREG_LIGHTi_VECTOR_HIGH = picaRegs.read(0x0145u + (lightId << 4u)); + globals.GPUREG_LIGHTi_CONFIG = picaRegs.read(0x0149u + (lightId << 4u)); + + float lightDistance; + float3 lightPosition = normalize(float3( + decodeFP(extract_bits(GPUREG_LIGHTi_VECTOR_LOW, 0, 16), 5u, 10u), decodeFP(extract_bits(GPUREG_LIGHTi_VECTOR_LOW, 16, 16), 5u, 10u), + decodeFP(extract_bits(GPUREG_LIGHTi_VECTOR_HIGH, 0, 16), 5u, 10u) + )); + + // Positional Light + if (extract_bits(globals.GPUREG_LIGHTi_CONFIG, 0, 1) == 0u) { + // error_unimpl = true; + lightVector = lightPosition + in.view; + } + + // Directional light + else { + lightVector = lightPosition; + } + + lightDistance = length(lightVector); + lightVector = normalize(lightVector); + halfVector = lightVector + normalize(in.view); + + float NdotL = dot(globals.normal, lightVector); // N dot Li + + // Two sided diffuse + if (extract_bits(globals.GPUREG_LIGHTi_CONFIG, 1, 1) == 0u) + NdotL = max(0.0, NdotL); + else + NdotL = abs(NdotL); + + float geometricFactor; + bool useGeo0 = extract_bits(globals.GPUREG_LIGHTi_CONFIG, 2, 1) == 1u; + bool useGeo1 = extract_bits(globals.GPUREG_LIGHTi_CONFIG, 3, 1) == 1u; + if (useGeo0 || useGeo1) { + geometricFactor = dot(halfVector, halfVector); + geometricFactor = geometricFactor == 0.0 ? 0.0 : min(NdotL / geometricFactor, 1.0); + } + + float distanceAttenuation = 1.0; + if (extract_bits(lightingConfig1, 24 + int(lightId), 1) == 0u) { + uint GPUREG_LIGHTi_ATTENUATION_BIAS = extract_bits(picaRegs.read(0x014Au + (lightId << 4u)), 0, 20); + uint GPUREG_LIGHTi_ATTENUATION_SCALE = extract_bits(picaRegs.read(0x014Bu + (lightId << 4u)), 0, 20); + + float distanceAttenuationBias = decodeFP(GPUREG_LIGHTi_ATTENUATION_BIAS, 7u, 12u); + float distanceAttenuationScale = decodeFP(GPUREG_LIGHTi_ATTENUATION_SCALE, 7u, 12u); + + float delta = lightDistance * distanceAttenuationScale + distanceAttenuationBias; + delta = clamp(delta, 0.0, 1.0); + int index = int(clamp(floor(delta * 255.0), 0.0, 255.0)); + distanceAttenuation = lutLookup(texLut, 16u + lightId, index); + } + + float spotlightAttenuation = lightLutLookup(globals, in, picaRegs, texLut, environmentId, SP_LUT, lightId, lightVector, halfVector); + float specular0Distribution = lightLutLookup(globals, in, picaRegs, texLut, environmentId, D0_LUT, lightId, lightVector, halfVector); + float specular1Distribution = lightLutLookup(globals, in, picaRegs, texLut, environmentId, D1_LUT, lightId, lightVector, halfVector); + float3 reflectedColor; + reflectedColor.r = lightLutLookup(globals, in, picaRegs, texLut, environmentId, RR_LUT, lightId, lightVector, halfVector); + + if (isSamplerEnabled(environmentId, RG_LUT)) { + reflectedColor.g = lightLutLookup(globals, in, picaRegs, texLut, environmentId, RG_LUT, lightId, lightVector, halfVector); + } else { + reflectedColor.g = reflectedColor.r; + } + + if (isSamplerEnabled(environmentId, RB_LUT)) { + reflectedColor.b = lightLutLookup(globals, in, picaRegs, texLut, environmentId, RB_LUT, lightId, lightVector, halfVector); + } else { + reflectedColor.b = reflectedColor.r; + } + + float3 specular0 = regToColor(GPUREG_LIGHTi_SPECULAR0) * specular0Distribution; + float3 specular1 = regToColor(GPUREG_LIGHTi_SPECULAR1) * specular1Distribution * reflectedColor; + + specular0 *= useGeo0 ? geometricFactor : 1.0; + specular1 *= useGeo1 ? geometricFactor : 1.0; + + float clampFactor = 1.0; + if (clampHighlights && NdotL == 0.0) { + clampFactor = 0.0; + } + + float lightFactor = distanceAttenuation * spotlightAttenuation; + diffuseSum.rgb += lightFactor * (regToColor(GPUREG_LIGHTi_AMBIENT) + regToColor(GPUREG_LIGHTi_DIFFUSE) * NdotL); + specularSum.rgb += lightFactor * clampFactor * (specular0 + specular1); + } + uint fresnelOutput1 = extract_bits(GPUREG_LIGHTING_CONFIG0, 2, 1); + uint fresnelOutput2 = extract_bits(GPUREG_LIGHTING_CONFIG0, 3, 1); + + float fresnelFactor; + + if (fresnelOutput1 == 1u || fresnelOutput2 == 1u) { + fresnelFactor = lightLutLookup(globals, in, picaRegs, texLut, environmentId, FR_LUT, lightId, lightVector, halfVector); + } + + if (fresnelOutput1 == 1u) { + diffuseSum.a = fresnelFactor; + } + + if (fresnelOutput2 == 1u) { + specularSum.a = fresnelFactor; + } + + uint GPUREG_LIGHTING_AMBIENT = picaRegs.read(0x01C0u); + float4 globalAmbient = float4(regToColor(GPUREG_LIGHTING_AMBIENT), 1.0); + primaryColor = clamp(globalAmbient + diffuseSum, 0.0, 1.0); + secondaryColor = clamp(specularSum, 0.0, 1.0); +} + +float4 performLogicOp(LogicOp logicOp, float4 s, float4 d) { + return as_type(performLogicOpU(logicOp, as_type(s), as_type(d))); +} + +fragment float4 fragmentDraw(DrawVertexOut in [[stage_in]], float4 prevColor [[color(0)]], constant PicaRegs& picaRegs [[buffer(0)]], constant FragTEV& tev [[buffer(1)]], constant LogicOp& logicOp [[buffer(2)]], + texture2d tex0 [[texture(0)]], texture2d tex1 [[texture(1)]], texture2d tex2 [[texture(2)]], texture2d texLut [[texture(3)]], + sampler samplr0 [[sampler(0)]], sampler samplr1 [[sampler(1)]], sampler samplr2 [[sampler(2)]], sampler linearSampler [[sampler(3)]]) { + Globals globals; + + // HACK + //globals.lightingEnabled = picaRegs.read(0x008Fu) != 0u; + //globals.lightingNumLights = picaRegs.read(0x01C2u); + //globals.lightingConfig1 = picaRegs.read(0x01C4u); + //globals.alphaControl = picaRegs.read(0x104); + + globals.tevSources[0] = in.color; + if (lightingEnabled) { + calcLighting(globals, in, picaRegs, texLut, linearSampler, globals.tevSources[1], globals.tevSources[2]); + } else { + globals.tevSources[1] = float4(0.0); + globals.tevSources[2] = float4(0.0); + } + + uint textureConfig = picaRegs.read(0x80u); + float2 texCoord2 = (textureConfig & (1u << 13)) != 0u ? in.texCoord1 : in.texCoord2; + + if ((textureConfig & 1u) != 0u) globals.tevSources[3] = tex0.sample(samplr0, in.texCoord0.xy); + if ((textureConfig & 2u) != 0u) globals.tevSources[4] = tex1.sample(samplr1, in.texCoord1); + if ((textureConfig & 4u) != 0u) globals.tevSources[5] = tex2.sample(samplr2, texCoord2); + globals.tevSources[13] = float4(0.0); // Previous buffer + globals.tevSources[15] = in.color; // Previous combiner + + globals.tevNextPreviousBuffer = in.textureEnvBufferColor; + uint textureEnvUpdateBuffer = picaRegs.read(0xE0u); + + for (int i = 0; i < 6; i++) { + globals.tevSources[14] = in.textureEnvColor[i]; // Constant color + globals.tevSources[15] = tev.calculateCombiner(globals, i); + globals.tevSources[13] = globals.tevNextPreviousBuffer; + + if (i < 4) { + if ((textureEnvUpdateBuffer & (0x100u << i)) != 0u) { + globals.tevNextPreviousBuffer.rgb = globals.tevSources[15].rgb; + } + + if ((textureEnvUpdateBuffer & (0x1000u << i)) != 0u) { + globals.tevNextPreviousBuffer.a = globals.tevSources[15].a; + } + } + } + + float4 color = globals.tevSources[15]; + + // Fog + bool enable_fog = (textureEnvUpdateBuffer & 7u) == 5u; + + if (enable_fog) { + bool flip_depth = (textureEnvUpdateBuffer & (1u << 16)) != 0u; + float fog_index = flip_depth ? 1.0 - in.position.z : in.position.z; + fog_index *= 128.0; + float clamped_index = clamp(floor(fog_index), 0.0, 127.0); + float delta = fog_index - clamped_index; + float2 value = texLut.read(uint2(clamped_index, FOG_INDEX)).rg; + float fog_factor = clamp(value.r + value.g * delta, 0.0, 1.0); + + uint GPUREG_FOG_COLOR = picaRegs.read(0x00E1u); + + // Annoyingly color is not encoded in the same way as light color + float r = (GPUREG_FOG_COLOR & 0xFFu) / 255.0; + float g = ((GPUREG_FOG_COLOR >> 8) & 0xFFu) / 255.0; + float b = ((GPUREG_FOG_COLOR >> 16) & 0xFFu) / 255.0; + float3 fog_color = float3(r, g, b); + + color.rgb = mix(fog_color, color.rgb, fog_factor); + } + + // Perform alpha test + if ((alphaControl & 1u) != 0u) { // Check if alpha test is on + uint func = (alphaControl >> 4u) & 7u; + float reference = float((alphaControl >> 8u) & 0xffu) / 255.0; + float alpha = color.a; + + switch (func) { + case 0u: discard_fragment(); // Never pass alpha test + case 1u: break; // Always pass alpha test + case 2u: // Pass if equal + if (alpha != reference) discard_fragment(); + break; + case 3u: // Pass if not equal + if (alpha == reference) discard_fragment(); + break; + case 4u: // Pass if less than + if (alpha >= reference) discard_fragment(); + break; + case 5u: // Pass if less than or equal + if (alpha > reference) discard_fragment(); + break; + case 6u: // Pass if greater than + if (alpha <= reference) discard_fragment(); + break; + case 7u: // Pass if greater than or equal + if (alpha < reference) discard_fragment(); + break; + } + } + + return performLogicOp(logicOp, color, prevColor); +} From 58e1a536996348d1ec4c8ab5a65b396fe28ccb3b Mon Sep 17 00:00:00 2001 From: Samuliak Date: Fri, 16 Aug 2024 11:06:23 +0200 Subject: [PATCH 180/251] metal: create renderer --- include/panda_qt/main_window.hpp | 1 + include/renderer.hpp | 3 ++- include/renderer_gl/surface_cache.hpp | 4 ++-- src/core/PICA/gpu.cpp | 11 ++++++++++- src/panda_qt/main_window.cpp | 5 ++++- src/panda_sdl/frontend_sdl.cpp | 14 ++++++++++++-- src/renderer.cpp | 4 +++- 7 files changed, 34 insertions(+), 8 deletions(-) diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index 3ff16a1d..fc756b9f 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -129,6 +129,7 @@ class MainWindow : public QMainWindow { // Tracks whether we are using an OpenGL-backed renderer or a Vulkan-backed renderer bool usingGL = false; bool usingVk = false; + bool usingMtl = false; // Variables to keep track of whether the user is controlling the 3DS analog stick with their keyboard // This is done so when a gamepad is connected, we won't automatically override the 3DS analog stick settings with the gamepad's state diff --git a/include/renderer.hpp b/include/renderer.hpp index 569a730b..4eacf0b1 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -17,7 +17,8 @@ enum class RendererType : s8 { Null = 0, OpenGL = 1, Vulkan = 2, - Software = 3, + Metal = 3, + Software = 4, }; struct EmulatorConfig; diff --git a/include/renderer_gl/surface_cache.hpp b/include/renderer_gl/surface_cache.hpp index 5323741f..7346fd11 100644 --- a/include/renderer_gl/surface_cache.hpp +++ b/include/renderer_gl/surface_cache.hpp @@ -19,8 +19,8 @@ template class SurfaceCache { // Vanilla std::optional can't hold actual references using OptionalRef = std::optional>; - static_assert(std::is_same() || std::is_same() || - std::is_same(), "Invalid surface type"); + //static_assert(std::is_same() || std::is_same() || + // std::is_same(), "Invalid surface type"); size_t size; size_t evictionIndex; diff --git a/src/core/PICA/gpu.cpp b/src/core/PICA/gpu.cpp index fe336edc..95001b33 100644 --- a/src/core/PICA/gpu.cpp +++ b/src/core/PICA/gpu.cpp @@ -15,6 +15,9 @@ #ifdef PANDA3DS_ENABLE_VULKAN #include "renderer_vk/renderer_vk.hpp" #endif +#ifdef PANDA3DS_ENABLE_METAL +#include "renderer_mtl/renderer_mtl.hpp" +#endif constexpr u32 topScreenWidth = 240; constexpr u32 topScreenHeight = 400; @@ -52,6 +55,12 @@ GPU::GPU(Memory& mem, EmulatorConfig& config) : mem(mem), config(config) { renderer.reset(new RendererVK(*this, regs, externalRegs)); break; } +#endif +#ifdef PANDA3DS_ENABLE_METAL + case RendererType::Metal: { + renderer.reset(new RendererMTL(*this, regs, externalRegs)); + break; + } #endif default: { Helpers::panic("Rendering backend not supported: %s", Renderer::typeToString(config.rendererType)); @@ -365,7 +374,7 @@ PICA::Vertex GPU::getImmediateModeVertex() { // Run VS and return vertex data. TODO: Don't hardcode offsets for each attribute shaderUnit.vs.run(); - + // Map shader outputs to fixed function properties const u32 totalShaderOutputs = regs[PICA::InternalRegs::ShaderOutputCount] & 7; for (int i = 0; i < totalShaderOutputs; i++) { diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index f1949da7..4c187bc2 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -103,6 +103,7 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) const RendererType rendererType = emu->getConfig().rendererType; usingGL = (rendererType == RendererType::OpenGL || rendererType == RendererType::Software || rendererType == RendererType::Null); usingVk = (rendererType == RendererType::Vulkan); + usingMtl = (rendererType == RendererType::Metal); if (usingGL) { // Make GL context current for this thread, enable VSync @@ -113,6 +114,8 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) emu->initGraphicsContext(glContext); } else if (usingVk) { Helpers::panic("Vulkan on Qt is currently WIP, try the SDL frontend instead!"); + } else if (usingMtl) { + Helpers::panic("Metal on Qt currently doesn't work, try the SDL frontend instead!"); } else { Helpers::panic("Unsupported graphics backend for Qt frontend!"); } @@ -628,4 +631,4 @@ void MainWindow::setupControllerSensors(SDL_GameController* controller) { if (haveGyro) { SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); } -} \ No newline at end of file +} diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 8f9f4240..057a4858 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -67,6 +67,16 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp } #endif +#ifdef PANDA3DS_ENABLE_METAL + if (config.rendererType == RendererType::Metal) { + window = SDL_CreateWindow("Alber", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 400, 480, SDL_WINDOW_METAL | SDL_WINDOW_RESIZABLE); + + if (window == nullptr) { + Helpers::warn("Window creation failed: %s", SDL_GetError()); + } + } +#endif + emu.initGraphicsContext(window); } @@ -286,7 +296,7 @@ void FrontendSDL::run() { } break; } - + case SDL_CONTROLLERSENSORUPDATE: { if (event.csensor.sensor == SDL_SENSOR_GYRO) { auto rotation = Gyro::SDL::convertRotation({ @@ -370,4 +380,4 @@ void FrontendSDL::setupControllerSensors(SDL_GameController* controller) { if (haveGyro) { SDL_GameControllerSetSensorEnabled(controller, SDL_SENSOR_GYRO, SDL_TRUE); } -} \ No newline at end of file +} diff --git a/src/renderer.cpp b/src/renderer.cpp index 76c3e7a0..6a18df85 100644 --- a/src/renderer.cpp +++ b/src/renderer.cpp @@ -17,6 +17,7 @@ std::optional Renderer::typeFromString(std::string inString) { {"null", RendererType::Null}, {"nil", RendererType::Null}, {"none", RendererType::Null}, {"gl", RendererType::OpenGL}, {"ogl", RendererType::OpenGL}, {"opengl", RendererType::OpenGL}, {"vk", RendererType::Vulkan}, {"vulkan", RendererType::Vulkan}, {"vulcan", RendererType::Vulkan}, + {"mtl", RendererType::Metal}, {"metal", RendererType::Metal}, {"sw", RendererType::Software}, {"soft", RendererType::Software}, {"software", RendererType::Software}, {"softrast", RendererType::Software}, }; @@ -33,7 +34,8 @@ const char* Renderer::typeToString(RendererType rendererType) { case RendererType::Null: return "null"; case RendererType::OpenGL: return "opengl"; case RendererType::Vulkan: return "vulkan"; + case RendererType::Metal: return "metal"; case RendererType::Software: return "software"; default: return "Invalid"; } -} \ No newline at end of file +} From 45eda2f12048204315ce771ba84b95754bf6fdc6 Mon Sep 17 00:00:00 2001 From: Samuliak Date: Fri, 16 Aug 2024 12:25:46 +0200 Subject: [PATCH 181/251] bring back cmake changes --- CMakeLists.txt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 31fdd9f2..4fd12174 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,11 @@ if(BUILD_LIBRETRO_CORE) add_compile_definitions(__LIBRETRO__) endif() +if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND ENABLE_USER_BUILD) + # Disable stack buffer overflow checks in user builds + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /GS-") +endif() + add_library(AlberCore STATIC) include_directories(${PROJECT_SOURCE_DIR}/include/) @@ -256,6 +261,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp include/PICA/pica_frag_config.hpp include/PICA/pica_frag_uniforms.hpp include/PICA/shader_gen_types.hpp include/PICA/shader_decompiler.hpp + include/sdl_gyro.hpp ) cmrc_add_resource_library( @@ -570,7 +576,7 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE) ) else() set(FRONTEND_SOURCE_FILES src/panda_sdl/main.cpp src/panda_sdl/frontend_sdl.cpp src/panda_sdl/mappings.cpp) - set(FRONTEND_HEADER_FILES "") + set(FRONTEND_HEADER_FILES "include/panda_sdl/frontend_sdl.hpp") endif() target_link_libraries(Alber PRIVATE AlberCore) From dbdf21b1ab9d5636a55e266cac89b184e096227f Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:54:15 +0300 Subject: [PATCH 182/251] Improve accelerometer algorithm --- CMakeLists.txt | 2 +- include/sdl_gyro.hpp | 17 ----------------- include/sdl_sensors.hpp | 30 ++++++++++++++++++++++++++++++ src/panda_qt/main_window.cpp | 7 ++++--- src/panda_sdl/frontend_sdl.cpp | 7 ++++--- 5 files changed, 39 insertions(+), 24 deletions(-) delete mode 100644 include/sdl_gyro.hpp create mode 100644 include/sdl_sensors.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 2865a3f8..796217d1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -260,7 +260,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp include/PICA/pica_frag_config.hpp include/PICA/pica_frag_uniforms.hpp include/PICA/shader_gen_types.hpp include/PICA/shader_decompiler.hpp - include/sdl_gyro.hpp + include/sdl_sensors.hpp ) cmrc_add_resource_library( diff --git a/include/sdl_gyro.hpp b/include/sdl_gyro.hpp deleted file mode 100644 index e2df18df..00000000 --- a/include/sdl_gyro.hpp +++ /dev/null @@ -1,17 +0,0 @@ -#pragma once - -#include -#include - -#include "services/hid.hpp" - -namespace Gyro::SDL { - // Convert the rotation data we get from SDL sensor events to rotation data we can feed right to HID - // Returns [pitch, roll, yaw] - static glm::vec3 convertRotation(glm::vec3 rotation) { - // Convert the rotation from rad/s to deg/s and scale by the gyroscope coefficient in HID - constexpr float scale = 180.f / std::numbers::pi * HIDService::gyroscopeCoeff; - // The axes are also inverted, so invert scale before the multiplication. - return rotation * -scale; - } -} // namespace Gyro::SDL \ No newline at end of file diff --git a/include/sdl_sensors.hpp b/include/sdl_sensors.hpp new file mode 100644 index 00000000..cd452ce4 --- /dev/null +++ b/include/sdl_sensors.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include +#include + +#include "services/hid.hpp" + +namespace Sensors::SDL { + // Convert the rotation data we get from SDL sensor events to rotation data we can feed right to HID + // Returns [pitch, roll, yaw] + static glm::vec3 convertRotation(glm::vec3 rotation) { + // Convert the rotation from rad/s to deg/s and scale by the gyroscope coefficient in HID + constexpr float scale = 180.f / std::numbers::pi * HIDService::gyroscopeCoeff; + // The axes are also inverted, so invert scale before the multiplication. + return rotation * -scale; + } + + static glm::vec3 convertAcceleration(float* data) { + // Set our cap to ~9 m/s^2. The 3DS sensors cap at -930 and +930, so values above this value will get clamped to 930 + // At rest (3DS laid flat on table), hardware reads around ~0 for x and z axis, and around ~480 for y axis due to gravity. + // This code tries to mimic this approximately, with offsets based on measurements from my DualShock 4. + static constexpr float accelMax = 9.f; + + s16 x = std::clamp(s16(data[0] / accelMax * 930.f), -930, +930); + s16 y = std::clamp(s16(data[1] / (SDL_STANDARD_GRAVITY * accelMax) * 930.f - 350.f), -930, +930); + s16 z = std::clamp(s16((data[2] - 2.1f) / accelMax * 930.f), -930, +930); + + return glm::vec3(x, y, z); + } +} // namespace Gyro::SDL \ No newline at end of file diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index fab77d2e..6bdffb7e 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -9,7 +9,7 @@ #include "cheats.hpp" #include "input_mappings.hpp" -#include "sdl_gyro.hpp" +#include "sdl_sensors.hpp" #include "services/dsp.hpp" MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), keyboardMappings(InputMappings::defaultKeyboardMappings()) { @@ -606,7 +606,7 @@ void MainWindow::pollControllers() { case SDL_CONTROLLERSENSORUPDATE: { if (event.csensor.sensor == SDL_SENSOR_GYRO) { - auto rotation = Gyro::SDL::convertRotation({ + auto rotation = Sensors::SDL::convertRotation({ event.csensor.data[0], event.csensor.data[1], event.csensor.data[2], @@ -616,7 +616,8 @@ void MainWindow::pollControllers() { hid.setRoll(s16(rotation.y)); hid.setYaw(s16(rotation.z)); } else if (event.csensor.sensor == SDL_SENSOR_ACCEL) { - hid.setAccel(s16(event.csensor.data[0]), s16(-event.csensor.data[1]), s16(event.csensor.data[2])); + auto accel = Sensors::SDL::convertAcceleration(event.csensor.data); + hid.setAccel(accel.x, accel.y, accel.z); } break; } diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 80014884..90166899 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -2,7 +2,7 @@ #include -#include "sdl_gyro.hpp" +#include "sdl_sensors.hpp" FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMappings()) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) { @@ -289,7 +289,7 @@ void FrontendSDL::run() { case SDL_CONTROLLERSENSORUPDATE: { if (event.csensor.sensor == SDL_SENSOR_GYRO) { - auto rotation = Gyro::SDL::convertRotation({ + auto rotation = Sensors::SDL::convertRotation({ event.csensor.data[0], event.csensor.data[1], event.csensor.data[2], @@ -299,7 +299,8 @@ void FrontendSDL::run() { hid.setRoll(s16(rotation.y)); hid.setYaw(s16(rotation.z)); } else if (event.csensor.sensor == SDL_SENSOR_ACCEL) { - hid.setAccel(s16(event.csensor.data[0]), s16(-event.csensor.data[1]), s16(event.csensor.data[2])); + auto accel = Sensors::SDL::convertAcceleration(event.csensor.data); + hid.setAccel(accel.x, accel.y, accel.z); } break; } From ac3840ddb0e2a8cdae12bda4f078baba0bb77f57 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 16 Aug 2024 17:58:53 +0300 Subject: [PATCH 183/251] Docs: Add accelerometer sample data --- .../accelerometer_readings/readings_flat_1.png | Bin 0 -> 151093 bytes .../accelerometer_readings/readings_flat_2.png | Bin 0 -> 55117 bytes .../readings_shaking_1.png | Bin 0 -> 217640 bytes .../readings_shaking_2.png | Bin 0 -> 66836 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/3ds/accelerometer_readings/readings_flat_1.png create mode 100644 docs/3ds/accelerometer_readings/readings_flat_2.png create mode 100644 docs/3ds/accelerometer_readings/readings_shaking_1.png create mode 100644 docs/3ds/accelerometer_readings/readings_shaking_2.png diff --git a/docs/3ds/accelerometer_readings/readings_flat_1.png b/docs/3ds/accelerometer_readings/readings_flat_1.png new file mode 100644 index 0000000000000000000000000000000000000000..b7a425fc4cb953c1439f1146ef77b4f761990549 GIT binary patch literal 151093 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

~9+ix&i#vdHerc!s-w6 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%0DmMMl@ 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 literal 0 HcmV?d00001 diff --git a/docs/3ds/accelerometer_readings/readings_shaking_2.png b/docs/3ds/accelerometer_readings/readings_shaking_2.png new file mode 100644 index 0000000000000000000000000000000000000000..551e7d2eee37b57a6cf370dbdd80646a322784a9 GIT binary patch literal 66836 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 xBQ>t?AWY)(>n0OgLV3i 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 HN@xNAsKl?g literal 0 HcmV?d00001 From 12563956888b2c1bb0b6428438452242e07a2da8 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 16 Aug 2024 18:05:21 +0300 Subject: [PATCH 184/251] IWYU --- include/sdl_sensors.hpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/include/sdl_sensors.hpp b/include/sdl_sensors.hpp index cd452ce4..6de040ec 100644 --- a/include/sdl_sensors.hpp +++ b/include/sdl_sensors.hpp @@ -1,8 +1,10 @@ #pragma once +#include #include #include +#include "helpers.hpp" #include "services/hid.hpp" namespace Sensors::SDL { @@ -27,4 +29,4 @@ namespace Sensors::SDL { return glm::vec3(x, y, z); } -} // namespace Gyro::SDL \ No newline at end of file +} // namespace Sensors::SDL From 992c9fb98c5b72cff09e1566e4ab171ccfdfd029 Mon Sep 17 00:00:00 2001 From: Eric Warmenhoven Date: Sun, 11 Aug 2024 19:21:55 -0400 Subject: [PATCH 185/251] add libretro gitlab CI file --- .gitlab-ci.yml | 130 +++++++++++++++++++++++++++++++++++++++++++++++ CMakeLists.txt | 18 +++---- src/emulator.cpp | 2 +- 3 files changed, 140 insertions(+), 10 deletions(-) create mode 100644 .gitlab-ci.yml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 00000000..740ccf0c --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,130 @@ +# DESCRIPTION: GitLab CI/CD for libRetro (NOT FOR GitLab-proper) + +############################################################################## +################################# BOILERPLATE ################################ +############################################################################## + +# Core definitions +.core-defs: + variables: + GIT_SUBMODULE_STRATEGY: recursive + CORENAME: panda3ds + CORE_ARGS: -DBUILD_LIBRETRO_CORE=ON -DENABLE_USER_BUILD=ON -DENABLE_VULKAN=OFF -DENABLE_LUAJIT=OFF -DENABLE_DISCORD_RPC=OFF + +# Inclusion templates, required for the build to work + +include: + ################################## DESKTOPS ################################ + # Linux + - project: 'libretro-infrastructure/ci-templates' + file: '/linux-cmake.yml' + + # Windows + - project: 'libretro-infrastructure/ci-templates' + file: '/windows-cmake-mingw.yml' + + # MacOS + - project: 'libretro-infrastructure/ci-templates' + file: 'osx-cmake-x86.yml' + + # MacOS + - project: 'libretro-infrastructure/ci-templates' + file: 'osx-cmake-arm64.yml' + + ################################## CELLULAR ################################ + # Android + - project: 'libretro-infrastructure/ci-templates' + file: '/android-cmake.yml' + + # iOS + - project: 'libretro-infrastructure/ci-templates' + file: '/ios-cmake.yml' + +# Stages for building +stages: + - build-prepare + - build-static + - build-shared + +############################################################################## +#################################### STAGES ################################## +############################################################################## +# +################################### DESKTOPS ################################# +# Linux 64-bit +libretro-build-linux-x64: + image: $CI_SERVER_HOST:5050/libretro-infrastructure/libretro-build-amd64-ubuntu:latest + before_script: + - export NUMPROC=$(($(nproc)/5)) + - sudo apt-get update -qy + - sudo apt-get install -qy software-properties-common + - sudo add-apt-repository -y ppa:savoury1/build-tools + - sudo add-apt-repository -y ppa:savoury1/gcc-defaults-12 + - sudo apt-get update -qy + - sudo apt-get install -qy cmake gcc-12 g++-12 + variables: + CC: /usr/bin/gcc-12 + CXX: /usr/bin/g++-12 + extends: + - .libretro-linux-cmake-x86_64 + - .core-defs + +# Windows 64-bit +libretro-build-windows-x64: + extends: + - .libretro-windows-cmake-x86_64 + - .core-defs + +# MacOS 64-bit +libretro-build-osx-x64: + tags: + - mac-apple-silicon + variables: + CORE_ARGS: -DBUILD_LIBRETRO_CORE=ON -DENABLE_USER_BUILD=ON -DENABLE_VULKAN=OFF -DENABLE_LUAJIT=OFF -DENABLE_DISCORD_RPC=OFF -DCMAKE_OSX_ARCHITECTURES=x86_64 -DCRYPTOPP_AMD64=1 + extends: + - .libretro-osx-cmake-x86 + - .core-defs + +# MacOS arm 64-bit +libretro-build-osx-arm64: + tags: + - mac-apple-silicon + extends: + - .libretro-osx-cmake-arm64 + - .core-defs + +################################### CELLULAR ################################# +# Android ARMv7a +#android-armeabi-v7a: +# extends: +# - .libretro-android-cmake-armeabi-v7a +# - .core-defs + +# Android ARMv8a +# android-arm64-v8a: +# extends: +# - .libretro-android-cmake-arm64-v8a +# - .core-defs + +# Android 64-bit x86 +# android-x86_64: +# extends: +# - .libretro-android-cmake-x86_64 +# - .core-defs + +# Android 32-bit x86 +# android-x86: +# extends: +# - .libretro-android-cmake-x86 +# - .core-defs + +# iOS +# libretro-build-ios-arm64: +# extends: +# - .libretro-ios-cmake-arm64 +# - .core-defs +# variables: +# CORE_ARGS: -DBUILD_LIBRETRO_CORE=ON -DBUILD_PLAY=OFF -DENABLE_AMAZON_S3=off -DBUILD_TESTS=OFF -DCMAKE_TOOLCHAIN_FILE=deps/Dependencies/cmake-ios/ios.cmake -DTARGET_IOS=ON +# LIBNAME: ${CORENAME}_libretro_ios.dylib + +################################### CONSOLES ################################# diff --git a/CMakeLists.txt b/CMakeLists.txt index 796217d1..91824a63 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,7 +93,7 @@ set(SDL_STATIC ON CACHE BOOL "" FORCE) set(SDL_SHARED OFF CACHE BOOL "" FORCE) set(SDL_TEST OFF CACHE BOOL "" FORCE) -if (NOT ANDROID) +if (NOT ANDROID AND NOT BUILD_LIBRETRO_CORE) add_subdirectory(third_party/SDL2) target_link_libraries(AlberCore PUBLIC SDL2-static) endif() @@ -520,17 +520,17 @@ elseif(BUILD_HYDRA_CORE) target_link_libraries(Alber PUBLIC AlberCore) elseif(BUILD_LIBRETRO_CORE) include_directories(third_party/libretro/include) - add_library(Alber SHARED src/libretro_core.cpp) - target_link_libraries(Alber PUBLIC AlberCore) - - set_target_properties(Alber PROPERTIES - OUTPUT_NAME "panda3ds_libretro" - PREFIX "" - ) + add_library(panda3ds_libretro SHARED src/libretro_core.cpp) + target_link_libraries(panda3ds_libretro PUBLIC AlberCore) + set_target_properties(panda3ds_libretro PROPERTIES PREFIX "") endif() if(ENABLE_LTO OR ENABLE_USER_BUILD) - set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) + if (NOT BUILD_LIBRETRO_CORE) + set_target_properties(Alber PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) + else() + set_target_properties(panda3ds_libretro PROPERTIES INTERPROCEDURAL_OPTIMIZATION TRUE) + endif() endif() if(ENABLE_TESTS) diff --git a/src/emulator.cpp b/src/emulator.cpp index fdf56a00..45b63a12 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -1,6 +1,6 @@ #include "emulator.hpp" -#ifndef __ANDROID__ +#if !defined(__ANDROID__) && !defined(__LIBRETRO__) #include #endif From 4a90b1ede159e03ff449de140090d776bcf25455 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 18 Aug 2024 01:18:08 +0300 Subject: [PATCH 186/251] Compile SDL in libretro frontend --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 91824a63..f6bdd6ed 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,7 +93,7 @@ set(SDL_STATIC ON CACHE BOOL "" FORCE) set(SDL_SHARED OFF CACHE BOOL "" FORCE) set(SDL_TEST OFF CACHE BOOL "" FORCE) -if (NOT ANDROID AND NOT BUILD_LIBRETRO_CORE) +if (NOT ANDROID) add_subdirectory(third_party/SDL2) target_link_libraries(AlberCore PUBLIC SDL2-static) endif() From 24bb295d820de201c7369ef1009d988ddf0815f8 Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Sun, 18 Aug 2024 23:02:02 +0300 Subject: [PATCH 187/251] Libretro: Expose FCRAM pointer --- src/libretro_core.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index b099067f..304314ba 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -368,7 +368,7 @@ uint retro_api_version() { return RETRO_API_VERSION; } usize retro_get_memory_size(uint id) { if (id == RETRO_MEMORY_SYSTEM_RAM) { - return 0; + return Memory::FCRAM_SIZE; } return 0; @@ -376,7 +376,7 @@ usize retro_get_memory_size(uint id) { void* retro_get_memory_data(uint id) { if (id == RETRO_MEMORY_SYSTEM_RAM) { - return 0; + return emulator->getMemory().getFCRAM(); } return nullptr; From 4b11eb09846410201155c1ff57b9d5f9ed04b5e4 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 20 Aug 2024 03:09:40 +0300 Subject: [PATCH 188/251] OpenGL: Fix fragment shader compilation error --- src/host_shaders/opengl_fragment_shader.frag | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index b9f9fe4c..f439b5cd 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -523,10 +523,10 @@ void main() { uint GPUREG_FOG_COLOR = readPicaReg(0x00E1u); // Annoyingly color is not encoded in the same way as light color - float r = (GPUREG_FOG_COLOR & 0xFFu) / 255.0; - float g = ((GPUREG_FOG_COLOR >> 8) & 0xFFu) / 255.0; - float b = ((GPUREG_FOG_COLOR >> 16) & 0xFFu) / 255.0; - vec3 fog_color = vec3(r, g, b); + float r = float(GPUREG_FOG_COLOR & 0xFFu); + float g = float((GPUREG_FOG_COLOR >> 8) & 0xFFu); + float b = float((GPUREG_FOG_COLOR >> 16) & 0xFFu); + vec3 fog_color = (1.0 / 255.0) * vec3(r, g, b); fragColour.rgb = mix(fog_color, fragColour.rgb, fog_factor); } From 937348a36fcf642f4aa0644bda845e30007d2d1f Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 20 Aug 2024 03:17:53 +0300 Subject: [PATCH 189/251] OpenGL: Fix shift signedness in fog calculation --- src/host_shaders/opengl_fragment_shader.frag | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag index f439b5cd..9f07df0b 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -524,8 +524,8 @@ void main() { // Annoyingly color is not encoded in the same way as light color float r = float(GPUREG_FOG_COLOR & 0xFFu); - float g = float((GPUREG_FOG_COLOR >> 8) & 0xFFu); - float b = float((GPUREG_FOG_COLOR >> 16) & 0xFFu); + float g = float((GPUREG_FOG_COLOR >> 8u) & 0xFFu); + float b = float((GPUREG_FOG_COLOR >> 16u) & 0xFFu); vec3 fog_color = (1.0 / 255.0) * vec3(r, g, b); fragColour.rgb = mix(fog_color, fragColour.rgb, fog_factor); @@ -561,4 +561,4 @@ void main() { break; } } -} \ No newline at end of file +} From dfc7b59de243e77199eb1a99fe6c8b6da4cc5241 Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Tue, 20 Aug 2024 12:30:19 +0300 Subject: [PATCH 190/251] Get application version from git --- CMakeLists.txt | 25 +++++++++++++++++++++++++ include/version.hpp.in | 1 + src/hydra_core.cpp | 3 ++- src/libretro_core.cpp | 3 ++- src/panda_qt/about_window.cpp | 4 ++++ src/panda_qt/main_window.cpp | 3 ++- src/panda_sdl/frontend_sdl.cpp | 6 ++++-- 7 files changed, 40 insertions(+), 5 deletions(-) create mode 100644 include/version.hpp.in diff --git a/CMakeLists.txt b/CMakeLists.txt index f6bdd6ed..dd8867a7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,6 +18,7 @@ if(NOT CMAKE_BUILD_TYPE) endif() project(Alber) +set(PANDA3DS_VERSION "0.8") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) if(APPLE) @@ -60,6 +61,30 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND ENABLE_USER_BUILD) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /GS-") endif() +find_package(Git) +if(GIT_FOUND) + execute_process( + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} describe --tags --abbrev=0 + OUTPUT_VARIABLE PANDA3DS_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE + ) + execute_process( + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} describe --tags + OUTPUT_VARIABLE git_version_tag OUTPUT_STRIP_TRAILING_WHITESPACE + ) + if(NOT PANDA3DS_VERSION STREQUAL git_version_tag) + execute_process( + WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} describe --always --abbrev=7 + OUTPUT_VARIABLE git_version_rev OUTPUT_STRIP_TRAILING_WHITESPACE + ) + set(PANDA3DS_VERSION "${PANDA3DS_VERSION}.${git_version_rev}") + unset(git_version_rev) + endif() + string(REGEX REPLACE "^v" "" PANDA3DS_VERSION "${PANDA3DS_VERSION}") + unset(git_version_tag) +endif() +configure_file(${PROJECT_SOURCE_DIR}/include/version.hpp.in ${CMAKE_BINARY_DIR}/include/version.hpp) +include_directories(${CMAKE_BINARY_DIR}/include/) + add_library(AlberCore STATIC) include_directories(${PROJECT_SOURCE_DIR}/include/) diff --git a/include/version.hpp.in b/include/version.hpp.in new file mode 100644 index 00000000..37359828 --- /dev/null +++ b/include/version.hpp.in @@ -0,0 +1 @@ +#define PANDA3DS_VERSION "${PANDA3DS_VERSION}" diff --git a/src/hydra_core.cpp b/src/hydra_core.cpp index acbf30a8..04fcdbdb 100644 --- a/src/hydra_core.cpp +++ b/src/hydra_core.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -150,7 +151,7 @@ HC_API const char* getInfo(hydra::InfoType type) { case hydra::InfoType::SystemName: return "Nintendo 3DS"; case hydra::InfoType::Description: return "HLE 3DS emulator. There's a little Alber in your computer and he runs Nintendo 3DS games."; case hydra::InfoType::Author: return "wheremyfoodat (Peach)"; - case hydra::InfoType::Version: return "0.7"; + case hydra::InfoType::Version: return PANDA3DS_VERSION; case hydra::InfoType::License: return "GPLv3"; case hydra::InfoType::Website: return "https://panda3ds.com/"; case hydra::InfoType::Extensions: return "3ds,cci,cxi,app,3dsx,elf,axf"; diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index 304314ba..cd0e9747 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -4,6 +4,7 @@ #include +#include #include #include @@ -200,7 +201,7 @@ static void ConfigCheckVariables() { void retro_get_system_info(retro_system_info* info) { info->need_fullpath = true; info->valid_extensions = "3ds|3dsx|elf|axf|cci|cxi|app"; - info->library_version = "0.8"; + info->library_version = PANDA3DS_VERSION; info->library_name = "Panda3DS"; info->block_extract = true; } diff --git a/src/panda_qt/about_window.cpp b/src/panda_qt/about_window.cpp index 67767198..60a91272 100644 --- a/src/panda_qt/about_window.cpp +++ b/src/panda_qt/about_window.cpp @@ -1,4 +1,5 @@ #include "panda_qt/about_window.hpp" +#include "version.hpp" #include #include @@ -17,6 +18,8 @@ AboutWindow::AboutWindow(QWidget* parent) : QDialog(parent) { QStringLiteral(R"(

Panda3DS

+

v%VERSION_STRING%

+
)") + .replace(QStringLiteral("%VERSION_STRING%"), PANDA3DS_VERSION) .replace(QStringLiteral("%ABOUT_PANDA3DS%"), tr("Panda3DS is a free and open source Nintendo 3DS emulator, for Windows, MacOS and Linux")) .replace(QStringLiteral("%SUPPORT%"), tr("Visit panda3ds.com for help with Panda3DS and links to our official support sites.")) .replace( diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 6bdffb7e..4b1f399d 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -7,13 +7,14 @@ #include #include +#include "version.hpp" #include "cheats.hpp" #include "input_mappings.hpp" #include "sdl_sensors.hpp" #include "services/dsp.hpp" MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), keyboardMappings(InputMappings::defaultKeyboardMappings()) { - setWindowTitle("Alber"); + setWindowTitle("Alber - " PANDA3DS_VERSION); // Enable drop events for loading ROMs setAcceptDrops(true); resize(800, 240 * 4); diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index 90166899..a60bc484 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -1,4 +1,5 @@ #include "panda_sdl/frontend_sdl.hpp" +#include "version.hpp" #include @@ -32,6 +33,7 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp #ifdef PANDA3DS_ENABLE_OPENGL needOpenGL = needOpenGL || (config.rendererType == RendererType::OpenGL); #endif + char* windowTitle = "Alber - " PANDA3DS_VERSION; if (needOpenGL) { // Demand 3.3 core for software renderer, or 4.1 core for OpenGL renderer (max available on MacOS) @@ -39,7 +41,7 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, config.rendererType == RendererType::Software ? 3 : 4); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, config.rendererType == RendererType::Software ? 3 : 1); - window = SDL_CreateWindow("Alber", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 400, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); + window = SDL_CreateWindow(windowTitle, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 400, 480, SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE); if (window == nullptr) { Helpers::panic("Window creation failed: %s", SDL_GetError()); @@ -59,7 +61,7 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp #ifdef PANDA3DS_ENABLE_VULKAN if (config.rendererType == RendererType::Vulkan) { - window = SDL_CreateWindow("Alber", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 400, 480, SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE); + window = SDL_CreateWindow(windowTitle, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 400, 480, SDL_WINDOW_VULKAN | SDL_WINDOW_RESIZABLE); if (window == nullptr) { Helpers::warn("Window creation failed: %s", SDL_GetError()); From c694ce9a257b1469b51eda701286832efb703413 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:20:14 +0300 Subject: [PATCH 191/251] Improved git versioning --- CMakeLists.txt | 13 ++++++++++--- include/config.hpp | 3 +++ include/version.hpp.in | 1 - src/config.cpp | 5 +++++ src/panda_qt/main_window.cpp | 10 +++++++++- src/panda_sdl/frontend_sdl.cpp | 6 +++++- 6 files changed, 32 insertions(+), 6 deletions(-) delete mode 100644 include/version.hpp.in diff --git a/CMakeLists.txt b/CMakeLists.txt index dd8867a7..38bc6dff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -18,7 +18,6 @@ if(NOT CMAKE_BUILD_TYPE) endif() project(Alber) -set(PANDA3DS_VERSION "0.8") set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) if(APPLE) @@ -44,6 +43,7 @@ option(ENABLE_HTTP_SERVER "Enable HTTP server. Used for Discord bot support" OFF option(ENABLE_DISCORD_RPC "Compile with Discord RPC support (disabled by default)" ON) option(ENABLE_LUAJIT "Enable scripting with the Lua programming language" ON) option(ENABLE_QT_GUI "Enable the Qt GUI. If not selected then the emulator uses a minimal SDL-based UI instead" OFF) +option(ENABLE_GIT_VERSIONING "Enables querying git for the emulator version" ON) option(BUILD_HYDRA_CORE "Build a Hydra core" OFF) option(BUILD_LIBRETRO_CORE "Build a Libretro core" OFF) @@ -61,8 +61,15 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC" AND ENABLE_USER_BUILD) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /GS-") endif() +# Generate versioning files find_package(Git) -if(GIT_FOUND) +set(PANDA3DS_VERSION "0.8") + +if(NOT EXISTS ${CMAKE_BINARY_DIR}/include/version.hpp.in) + file(WRITE ${CMAKE_BINARY_DIR}/include/version.hpp.in "#define PANDA3DS_VERSION \"\${PANDA3DS_VERSION}\"") +endif() + +if(GIT_FOUND AND ENABLE_GIT_VERSIONING) execute_process( WORKING_DIRECTORY ${PROJECT_SOURCE_DIR} COMMAND ${GIT_EXECUTABLE} describe --tags --abbrev=0 OUTPUT_VARIABLE PANDA3DS_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE @@ -82,7 +89,7 @@ if(GIT_FOUND) string(REGEX REPLACE "^v" "" PANDA3DS_VERSION "${PANDA3DS_VERSION}") unset(git_version_tag) endif() -configure_file(${PROJECT_SOURCE_DIR}/include/version.hpp.in ${CMAKE_BINARY_DIR}/include/version.hpp) +configure_file(${CMAKE_BINARY_DIR}/include/version.hpp.in ${CMAKE_BINARY_DIR}/include/version.hpp) include_directories(${CMAKE_BINARY_DIR}/include/) add_library(AlberCore STATIC) diff --git a/include/config.hpp b/include/config.hpp index 52be1af7..8833e4b3 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -40,6 +40,9 @@ struct EmulatorConfig { bool audioEnabled = false; bool vsyncEnabled = true; + bool printAppVersion = true; + bool appVersionOnWindow = false; + bool chargerPlugged = true; // Default to 3% battery to make users suffer int batteryPercentage = 3; diff --git a/include/version.hpp.in b/include/version.hpp.in deleted file mode 100644 index 37359828..00000000 --- a/include/version.hpp.in +++ /dev/null @@ -1 +0,0 @@ -#define PANDA3DS_VERSION "${PANDA3DS_VERSION}" diff --git a/src/config.cpp b/src/config.cpp index dae5a0ab..e1bbf2e4 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -41,6 +41,9 @@ void EmulatorConfig::load() { discordRpcEnabled = toml::find_or(general, "EnableDiscordRPC", false); usePortableBuild = toml::find_or(general, "UsePortableBuild", false); defaultRomPath = toml::find_or(general, "DefaultRomPath", ""); + + printAppVersion = toml::find_or(general, "PrintAppVersion", true); + appVersionOnWindow = toml::find_or(general, "AppVersionOnWindow", false); } } @@ -127,6 +130,8 @@ void EmulatorConfig::save() { data["General"]["EnableDiscordRPC"] = discordRpcEnabled; data["General"]["UsePortableBuild"] = usePortableBuild; data["General"]["DefaultRomPath"] = defaultRomPath.string(); + data["General"]["PrintAppVersion"] = printAppVersion; + data["General"]["AppVersionOnWindow"] = appVersionOnWindow; data["GPU"]["EnableShaderJIT"] = shaderJitEnabled; data["GPU"]["Renderer"] = std::string(Renderer::typeToString(rendererType)); diff --git a/src/panda_qt/main_window.cpp b/src/panda_qt/main_window.cpp index 4b1f399d..45690da7 100644 --- a/src/panda_qt/main_window.cpp +++ b/src/panda_qt/main_window.cpp @@ -14,7 +14,7 @@ #include "services/dsp.hpp" MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent), keyboardMappings(InputMappings::defaultKeyboardMappings()) { - setWindowTitle("Alber - " PANDA3DS_VERSION); + setWindowTitle("Alber"); // Enable drop events for loading ROMs setAcceptDrops(true); resize(800, 240 * 4); @@ -99,6 +99,14 @@ MainWindow::MainWindow(QApplication* app, QWidget* parent) : QMainWindow(parent) } } + if (emu->getConfig().appVersionOnWindow) { + setWindowTitle("Alber v" PANDA3DS_VERSION); + } + + if (emu->getConfig().printAppVersion) { + printf("Welcome to Panda3DS v%s!\n", PANDA3DS_VERSION); + } + // The emulator graphics context for the thread should be initialized in the emulator thread due to how GL contexts work emuThread = std::thread([this]() { const RendererType rendererType = emu->getConfig().rendererType; diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index a60bc484..c892c3c5 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -33,7 +33,11 @@ FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMapp #ifdef PANDA3DS_ENABLE_OPENGL needOpenGL = needOpenGL || (config.rendererType == RendererType::OpenGL); #endif - char* windowTitle = "Alber - " PANDA3DS_VERSION; + + const char* windowTitle = config.appVersionOnWindow ? ("Alber v" PANDA3DS_VERSION) : "Alber"; + if (config.printAppVersion) { + printf("Welcome to Panda3DS v%s!\n", PANDA3DS_VERSION); + } if (needOpenGL) { // Demand 3.3 core for software renderer, or 4.1 core for OpenGL renderer (max available on MacOS) From 471bdd6ab955c71d705d7d1c4639611f9993e4cf Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Tue, 20 Aug 2024 16:02:06 +0300 Subject: [PATCH 192/251] GPU: Temporarily skip draws if they're too big instead of panicking --- src/core/PICA/gpu.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/core/PICA/gpu.cpp b/src/core/PICA/gpu.cpp index fe336edc..3c4d4c5b 100644 --- a/src/core/PICA/gpu.cpp +++ b/src/core/PICA/gpu.cpp @@ -157,7 +157,10 @@ void GPU::drawArrays() { // Configures the type of primitive and the number of vertex shader outputs const u32 primConfig = regs[PICA::InternalRegs::PrimitiveConfig]; const PICA::PrimType primType = static_cast(Helpers::getBits<8, 2>(primConfig)); - if (vertexCount > Renderer::vertexBufferSize) Helpers::panic("[PICA] vertexCount > vertexBufferSize"); + if (vertexCount > Renderer::vertexBufferSize) [[unlikely]] { + Helpers::warn("[PICA] vertexCount > vertexBufferSize"); + return; + } if ((primType == PICA::PrimType::TriangleList && vertexCount % 3) || (primType == PICA::PrimType::TriangleStrip && vertexCount < 3) || (primType == PICA::PrimType::TriangleFan && vertexCount < 3)) { From 2754df9b94fcc9daa89d633232aae3ec1957aafb Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Fri, 23 Aug 2024 02:30:25 +0000 Subject: [PATCH 193/251] Add renderdoc API support (#585) * Add renderdoc API support * FIx renderdoc include directory * Fix RenderDoc linking * Fix Renderdoc linking (again) * Maybe fix renderdoc --- CMakeLists.txt | 12 +- cmake/FindRenderDoc.cmake | 25 + include/config.hpp | 3 +- include/emulator.hpp | 3 + include/renderdoc.hpp | 38 ++ src/config.cpp | 2 + src/emulator.cpp | 12 + src/panda_sdl/frontend_sdl.cpp | 11 +- src/renderdoc.cpp | 119 +++++ third_party/renderdoc/renderdoc_app.h | 721 ++++++++++++++++++++++++++ 10 files changed, 942 insertions(+), 4 deletions(-) create mode 100644 cmake/FindRenderDoc.cmake create mode 100644 include/renderdoc.hpp create mode 100644 src/renderdoc.cpp create mode 100644 third_party/renderdoc/renderdoc_app.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 38bc6dff..71c86578 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,7 @@ endif() project(Alber) set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}) +list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") if(APPLE) enable_language(OBJC) @@ -46,6 +47,7 @@ option(ENABLE_QT_GUI "Enable the Qt GUI. If not selected then the emulator uses option(ENABLE_GIT_VERSIONING "Enables querying git for the emulator version" ON) option(BUILD_HYDRA_CORE "Build a Hydra core" OFF) option(BUILD_LIBRETRO_CORE "Build a Libretro core" OFF) +option(ENABLE_RENDERDOC_API "Build with support for Renderdoc's capture API for graphics debugging" ON) if(BUILD_HYDRA_CORE) set(CMAKE_POSITION_INDEPENDENT_CODE ON) @@ -134,6 +136,7 @@ add_subdirectory(third_party/toml11) include_directories(${SDL2_INCLUDE_DIR}) include_directories(third_party/toml11) include_directories(third_party/glm) +include_directories(third_party/renderdoc) add_subdirectory(third_party/cmrc) @@ -192,6 +195,11 @@ else() set(HOST_ARM64 FALSE) endif() +if(ENABLE_RENDERDOC_API) + find_package(RenderDoc 1.6.0 MODULE REQUIRED) + add_compile_definitions(PANDA3DS_ENABLE_RENDERDOC) +endif() + if(HOST_X64 OR HOST_ARM64) set(DYNARMIC_TESTS OFF) #set(DYNARMIC_NO_BUNDLED_FMT ON) @@ -214,7 +222,7 @@ set(SOURCE_FILES src/emulator.cpp src/io_file.cpp src/config.cpp src/core/CPU/cpu_dynarmic.cpp src/core/CPU/dynarmic_cycles.cpp src/core/memory.cpp src/renderer.cpp src/core/renderer_null/renderer_null.cpp src/http_server.cpp src/stb_image_write.c src/core/cheats.cpp src/core/action_replay.cpp - src/discord_rpc.cpp src/lua.cpp src/memory_mapped_file.cpp src/miniaudio.cpp + src/discord_rpc.cpp src/lua.cpp src/memory_mapped_file.cpp src/miniaudio.cpp src/renderdoc.cpp ) set(CRYPTO_SOURCE_FILES src/core/crypto/aes_engine.cpp) set(KERNEL_SOURCE_FILES src/core/kernel/kernel.cpp src/core/kernel/resource_limits.cpp @@ -292,7 +300,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp include/PICA/pica_frag_config.hpp include/PICA/pica_frag_uniforms.hpp include/PICA/shader_gen_types.hpp include/PICA/shader_decompiler.hpp - include/sdl_sensors.hpp + include/sdl_sensors.hpp include/renderdoc.hpp ) cmrc_add_resource_library( diff --git a/cmake/FindRenderDoc.cmake b/cmake/FindRenderDoc.cmake new file mode 100644 index 00000000..c00a0888 --- /dev/null +++ b/cmake/FindRenderDoc.cmake @@ -0,0 +1,25 @@ +# SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +# SPDX-License-Identifier: GPL-2.0-or-later + +set(RENDERDOC_INCLUDE_DIR third_party/renderdoc) + +if (RENDERDOC_INCLUDE_DIR AND EXISTS "${RENDERDOC_INCLUDE_DIR}/renderdoc_app.h") + file(STRINGS "${RENDERDOC_INCLUDE_DIR}/renderdoc_app.h" RENDERDOC_VERSION_LINE REGEX "typedef struct RENDERDOC_API") + string(REGEX REPLACE ".*typedef struct RENDERDOC_API_([0-9]+)_([0-9]+)_([0-9]+).*" "\\1.\\2.\\3" RENDERDOC_VERSION "${RENDERDOC_VERSION_LINE}") + unset(RENDERDOC_VERSION_LINE) +endif() + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(RenderDoc + REQUIRED_VARS RENDERDOC_INCLUDE_DIR + VERSION_VAR RENDERDOC_VERSION +) + +if (RenderDoc_FOUND AND NOT TARGET RenderDoc::API) + add_library(RenderDoc::API INTERFACE IMPORTED) + set_target_properties(RenderDoc::API PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${RENDERDOC_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(RENDERDOC_INCLUDE_DIR) \ No newline at end of file diff --git a/include/config.hpp b/include/config.hpp index 8833e4b3..459f0907 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -39,7 +39,8 @@ struct EmulatorConfig { bool audioEnabled = false; bool vsyncEnabled = true; - + + bool enableRenderdoc = false; bool printAppVersion = true; bool appVersionOnWindow = false; diff --git a/include/emulator.hpp b/include/emulator.hpp index de04648e..a668d6c1 100644 --- a/include/emulator.hpp +++ b/include/emulator.hpp @@ -135,4 +135,7 @@ class Emulator { std::filesystem::path getAppDataRoot(); std::span getSMDH(); + + private: + void loadRenderdoc(); }; diff --git a/include/renderdoc.hpp b/include/renderdoc.hpp new file mode 100644 index 00000000..94a0f494 --- /dev/null +++ b/include/renderdoc.hpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once +#include + +#include "helpers.hpp" + +#ifdef PANDA3DS_ENABLE_RENDERDOC +namespace Renderdoc { + // Loads renderdoc dynamic library module. + void loadRenderdoc(); + + // Begins a capture if a renderdoc instance is attached. + void startCapture(); + + // Ends current renderdoc capture. + void endCapture(); + + // Triggers capturing process. + void triggerCapture(); + + // Sets output directory for captures + void setOutputDir(const std::string& path, const std::string& prefix); + + // Returns whether we've compiled with Renderdoc support + static constexpr bool isSupported() { return true; } +} // namespace Renderdoc +#else +namespace Renderdoc { + static void loadRenderdoc() {} + static void startCapture() { Helpers::panic("Tried to start a Renderdoc capture while support for renderdoc is disabled") } + static void endCapture() { Helpers::panic("Tried to end a Renderdoc capture while support for renderdoc is disabled") } + static void triggerCapture() { Helpers::panic("Tried to trigger a Renderdoc capture while support for renderdoc is disabled") } + static void setOutputDir(const std::string& path, const std::string& prefix) {} + static constexpr bool isSupported() { return false; } +} // namespace Renderdoc +#endif \ No newline at end of file diff --git a/src/config.cpp b/src/config.cpp index e1bbf2e4..70f2189c 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -70,6 +70,7 @@ void EmulatorConfig::load() { forceShadergenForLights = toml::find_or(gpu, "ForceShadergenForLighting", true); lightShadergenThreshold = toml::find_or(gpu, "ShadergenLightThreshold", 1); + enableRenderdoc = toml::find_or(gpu, "EnableRenderdoc", false); } } @@ -140,6 +141,7 @@ void EmulatorConfig::save() { data["GPU"]["UseUbershaders"] = useUbershaders; data["GPU"]["ForceShadergenForLighting"] = forceShadergenForLights; data["GPU"]["ShadergenLightThreshold"] = lightShadergenThreshold; + data["GPU"]["EnableRenderdoc"] = enableRenderdoc; data["Audio"]["DSPEmulation"] = std::string(Audio::DSPCore::typeToString(dspType)); data["Audio"]["EnableAudio"] = audioEnabled; diff --git a/src/emulator.cpp b/src/emulator.cpp index 45b63a12..9b856425 100644 --- a/src/emulator.cpp +++ b/src/emulator.cpp @@ -6,6 +6,8 @@ #include +#include "renderdoc.hpp" + #ifdef _WIN32 #include @@ -32,6 +34,10 @@ Emulator::Emulator() audioDevice.init(dsp->getSamples()); setAudioEnabled(config.audioEnabled); + if (Renderdoc::isSupported() && config.enableRenderdoc) { + loadRenderdoc(); + } + #ifdef PANDA3DS_ENABLE_DISCORD_RPC if (config.discordRpcEnabled) { discordRpc.init(); @@ -431,3 +437,9 @@ void Emulator::setAudioEnabled(bool enable) { dsp->setAudioEnabled(enable); } + +void Emulator::loadRenderdoc() { + std::string capturePath = (std::filesystem::current_path() / "RenderdocCaptures").generic_string(); + Renderdoc::loadRenderdoc(); + Renderdoc::setOutputDir(capturePath, ""); +} \ No newline at end of file diff --git a/src/panda_sdl/frontend_sdl.cpp b/src/panda_sdl/frontend_sdl.cpp index c892c3c5..de496d56 100644 --- a/src/panda_sdl/frontend_sdl.cpp +++ b/src/panda_sdl/frontend_sdl.cpp @@ -1,9 +1,10 @@ #include "panda_sdl/frontend_sdl.hpp" -#include "version.hpp" #include +#include "renderdoc.hpp" #include "sdl_sensors.hpp" +#include "version.hpp" FrontendSDL::FrontendSDL() : keyboardMappings(InputMappings::defaultKeyboardMappings()) { if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_EVENTS) < 0) { @@ -140,6 +141,14 @@ void FrontendSDL::run() { emu.reset(Emulator::ReloadOption::Reload); break; } + + case SDLK_F11: { + if constexpr (Renderdoc::isSupported()) { + Renderdoc::triggerCapture(); + } + + break; + } } } break; diff --git a/src/renderdoc.cpp b/src/renderdoc.cpp new file mode 100644 index 00000000..1de9c451 --- /dev/null +++ b/src/renderdoc.cpp @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: Copyright 2024 shadPS4 Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#ifdef PANDA3DS_ENABLE_RENDERDOC +#include "renderdoc.hpp" + +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include +#include + +namespace Renderdoc { + enum class CaptureState { + Idle, + Triggered, + InProgress, + }; + + static CaptureState captureState{CaptureState::Idle}; + RENDERDOC_API_1_6_0* rdocAPI{}; + + void loadRenderdoc() { +#ifdef WIN32 + // Check if we are running in Renderdoc GUI + HMODULE mod = GetModuleHandleA("renderdoc.dll"); + if (!mod) { + // If enabled in config, try to load RDoc runtime in offline mode + HKEY h_reg_key; + LONG result = RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Classes\\RenderDoc.RDCCapture.1\\DefaultIcon\\", 0, KEY_READ, &h_reg_key); + if (result != ERROR_SUCCESS) { + return; + } + std::array keyString{}; + DWORD stringSize{keyString.size()}; + result = RegQueryValueExW(h_reg_key, L"", 0, NULL, (LPBYTE)keyString.data(), &stringSize); + if (result != ERROR_SUCCESS) { + return; + } + + std::filesystem::path path{keyString.cbegin(), keyString.cend()}; + path = path.parent_path().append("renderdoc.dll"); + const auto path_to_lib = path.generic_string(); + mod = LoadLibraryA(path_to_lib.c_str()); + } + + if (mod) { + const auto RENDERDOC_GetAPI = reinterpret_cast(GetProcAddress(mod, "RENDERDOC_GetAPI")); + const s32 ret = RENDERDOC_GetAPI(eRENDERDOC_API_Version_1_6_0, (void**)&rdocAPI); + + if (ret != 1) { + Helpers::panic("Invalid return value from RENDERDOC_GetAPI"); + } + } +#else +#ifdef ANDROID + static constexpr const char RENDERDOC_LIB[] = "libVkLayer_GLES_RenderDoc.so"; +#else + static constexpr const char RENDERDOC_LIB[] = "librenderdoc.so"; +#endif + if (void* mod = dlopen(RENDERDOC_LIB, RTLD_NOW | RTLD_NOLOAD)) { + const auto RENDERDOC_GetAPI = reinterpret_cast(dlsym(mod, "RENDERDOC_GetAPI")); + const s32 ret = RENDERDOC_GetAPI(eRENDERDOC_API_Version_1_6_0, (void**)&rdocAPI); + + if (ret != 1) { + Helpers::panic("Invalid return value from RENDERDOC_GetAPI"); + } + } +#endif + if (rdocAPI) { + // Disable default capture keys as they suppose to trigger present-to-present capturing + // and it is not what we want + rdocAPI->SetCaptureKeys(nullptr, 0); + + // Also remove rdoc crash handler + rdocAPI->UnloadCrashHandler(); + } + } + + void startCapture() { + if (!rdocAPI) { + return; + } + + if (captureState == CaptureState::Triggered) { + rdocAPI->StartFrameCapture(nullptr, nullptr); + captureState = CaptureState::InProgress; + } + } + + void endCapture() { + if (!rdocAPI) { + return; + } + + if (captureState == CaptureState::InProgress) { + rdocAPI->EndFrameCapture(nullptr, nullptr); + captureState = CaptureState::Idle; + } + } + + void triggerCapture() { + if (captureState == CaptureState::Idle) { + captureState = CaptureState::Triggered; + } + } + + void setOutputDir(const std::string& path, const std::string& prefix) { + if (rdocAPI) { + rdocAPI->SetCaptureFilePathTemplate((path + '\\' + prefix).c_str()); + } + } +} // namespace Renderdoc +#endif \ No newline at end of file diff --git a/third_party/renderdoc/renderdoc_app.h b/third_party/renderdoc/renderdoc_app.h new file mode 100644 index 00000000..e73f1c90 --- /dev/null +++ b/third_party/renderdoc/renderdoc_app.h @@ -0,0 +1,721 @@ +/****************************************************************************** + * The MIT License (MIT) + * + * Copyright (c) 2019-2024 Baldur Karlsson + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + ******************************************************************************/ + +#pragma once + +////////////////////////////////////////////////////////////////////////////////////////////////// +// +// Documentation for the API is available at https://renderdoc.org/docs/in_application_api.html +// + +#if !defined(RENDERDOC_NO_STDINT) +#include +#endif + +#if defined(WIN32) || defined(__WIN32__) || defined(_WIN32) || defined(_MSC_VER) +#define RENDERDOC_CC __cdecl +#elif defined(__linux__) || defined(__FreeBSD__) +#define RENDERDOC_CC +#elif defined(__APPLE__) +#define RENDERDOC_CC +#else +#error "Unknown platform" +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +////////////////////////////////////////////////////////////////////////////////////////////////// +// Constants not used directly in below API + +// This is a GUID/magic value used for when applications pass a path where shader debug +// information can be found to match up with a stripped shader. +// the define can be used like so: const GUID RENDERDOC_ShaderDebugMagicValue = +// RENDERDOC_ShaderDebugMagicValue_value +#define RENDERDOC_ShaderDebugMagicValue_struct \ + { 0xeab25520, 0x6670, 0x4865, 0x84, 0x29, 0x6c, 0x8, 0x51, 0x54, 0x00, 0xff } + +// as an alternative when you want a byte array (assuming x86 endianness): +#define RENDERDOC_ShaderDebugMagicValue_bytearray \ + { 0x20, 0x55, 0xb2, 0xea, 0x70, 0x66, 0x65, 0x48, 0x84, 0x29, 0x6c, 0x8, 0x51, 0x54, 0x00, 0xff } + +// truncated version when only a uint64_t is available (e.g. Vulkan tags): +#define RENDERDOC_ShaderDebugMagicValue_truncated 0x48656670eab25520ULL + +////////////////////////////////////////////////////////////////////////////////////////////////// +// RenderDoc capture options +// + +typedef enum RENDERDOC_CaptureOption { + // Allow the application to enable vsync + // + // Default - enabled + // + // 1 - The application can enable or disable vsync at will + // 0 - vsync is force disabled + eRENDERDOC_Option_AllowVSync = 0, + + // Allow the application to enable fullscreen + // + // Default - enabled + // + // 1 - The application can enable or disable fullscreen at will + // 0 - fullscreen is force disabled + eRENDERDOC_Option_AllowFullscreen = 1, + + // Record API debugging events and messages + // + // Default - disabled + // + // 1 - Enable built-in API debugging features and records the results into + // the capture, which is matched up with events on replay + // 0 - no API debugging is forcibly enabled + eRENDERDOC_Option_APIValidation = 2, + eRENDERDOC_Option_DebugDeviceMode = 2, // deprecated name of this enum + + // Capture CPU callstacks for API events + // + // Default - disabled + // + // 1 - Enables capturing of callstacks + // 0 - no callstacks are captured + eRENDERDOC_Option_CaptureCallstacks = 3, + + // When capturing CPU callstacks, only capture them from actions. + // This option does nothing without the above option being enabled + // + // Default - disabled + // + // 1 - Only captures callstacks for actions. + // Ignored if CaptureCallstacks is disabled + // 0 - Callstacks, if enabled, are captured for every event. + eRENDERDOC_Option_CaptureCallstacksOnlyDraws = 4, + eRENDERDOC_Option_CaptureCallstacksOnlyActions = 4, + + // Specify a delay in seconds to wait for a debugger to attach, after + // creating or injecting into a process, before continuing to allow it to run. + // + // 0 indicates no delay, and the process will run immediately after injection + // + // Default - 0 seconds + // + eRENDERDOC_Option_DelayForDebugger = 5, + + // Verify buffer access. This includes checking the memory returned by a Map() call to + // detect any out-of-bounds modification, as well as initialising buffers with undefined contents + // to a marker value to catch use of uninitialised memory. + // + // NOTE: This option is only valid for OpenGL and D3D11. Explicit APIs such as D3D12 and Vulkan do + // not do the same kind of interception & checking and undefined contents are really undefined. + // + // Default - disabled + // + // 1 - Verify buffer access + // 0 - No verification is performed, and overwriting bounds may cause crashes or corruption in + // RenderDoc. + eRENDERDOC_Option_VerifyBufferAccess = 6, + + // The old name for eRENDERDOC_Option_VerifyBufferAccess was eRENDERDOC_Option_VerifyMapWrites. + // This option now controls the filling of uninitialised buffers with 0xdddddddd which was + // previously always enabled + eRENDERDOC_Option_VerifyMapWrites = eRENDERDOC_Option_VerifyBufferAccess, + + // Hooks any system API calls that create child processes, and injects + // RenderDoc into them recursively with the same options. + // + // Default - disabled + // + // 1 - Hooks into spawned child processes + // 0 - Child processes are not hooked by RenderDoc + eRENDERDOC_Option_HookIntoChildren = 7, + + // By default RenderDoc only includes resources in the final capture necessary + // for that frame, this allows you to override that behaviour. + // + // Default - disabled + // + // 1 - all live resources at the time of capture are included in the capture + // and available for inspection + // 0 - only the resources referenced by the captured frame are included + eRENDERDOC_Option_RefAllResources = 8, + + // **NOTE**: As of RenderDoc v1.1 this option has been deprecated. Setting or + // getting it will be ignored, to allow compatibility with older versions. + // In v1.1 the option acts as if it's always enabled. + // + // By default RenderDoc skips saving initial states for resources where the + // previous contents don't appear to be used, assuming that writes before + // reads indicate previous contents aren't used. + // + // Default - disabled + // + // 1 - initial contents at the start of each captured frame are saved, even if + // they are later overwritten or cleared before being used. + // 0 - unless a read is detected, initial contents will not be saved and will + // appear as black or empty data. + eRENDERDOC_Option_SaveAllInitials = 9, + + // In APIs that allow for the recording of command lists to be replayed later, + // RenderDoc may choose to not capture command lists before a frame capture is + // triggered, to reduce overheads. This means any command lists recorded once + // and replayed many times will not be available and may cause a failure to + // capture. + // + // NOTE: This is only true for APIs where multithreading is difficult or + // discouraged. Newer APIs like Vulkan and D3D12 will ignore this option + // and always capture all command lists since the API is heavily oriented + // around it and the overheads have been reduced by API design. + // + // 1 - All command lists are captured from the start of the application + // 0 - Command lists are only captured if their recording begins during + // the period when a frame capture is in progress. + eRENDERDOC_Option_CaptureAllCmdLists = 10, + + // Mute API debugging output when the API validation mode option is enabled + // + // Default - enabled + // + // 1 - Mute any API debug messages from being displayed or passed through + // 0 - API debugging is displayed as normal + eRENDERDOC_Option_DebugOutputMute = 11, + + // Option to allow vendor extensions to be used even when they may be + // incompatible with RenderDoc and cause corrupted replays or crashes. + // + // Default - inactive + // + // No values are documented, this option should only be used when absolutely + // necessary as directed by a RenderDoc developer. + eRENDERDOC_Option_AllowUnsupportedVendorExtensions = 12, + + // Define a soft memory limit which some APIs may aim to keep overhead under where + // possible. Anything above this limit will where possible be saved directly to disk during + // capture. + // This will cause increased disk space use (which may cause a capture to fail if disk space is + // exhausted) as well as slower capture times. + // + // Not all memory allocations may be deferred like this so it is not a guarantee of a memory + // limit. + // + // Units are in MBs, suggested values would range from 200MB to 1000MB. + // + // Default - 0 Megabytes + eRENDERDOC_Option_SoftMemoryLimit = 13, +} RENDERDOC_CaptureOption; + +// Sets an option that controls how RenderDoc behaves on capture. +// +// Returns 1 if the option and value are valid +// Returns 0 if either is invalid and the option is unchanged +typedef int(RENDERDOC_CC *pRENDERDOC_SetCaptureOptionU32)(RENDERDOC_CaptureOption opt, uint32_t val); +typedef int(RENDERDOC_CC *pRENDERDOC_SetCaptureOptionF32)(RENDERDOC_CaptureOption opt, float val); + +// Gets the current value of an option as a uint32_t +// +// If the option is invalid, 0xffffffff is returned +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetCaptureOptionU32)(RENDERDOC_CaptureOption opt); + +// Gets the current value of an option as a float +// +// If the option is invalid, -FLT_MAX is returned +typedef float(RENDERDOC_CC *pRENDERDOC_GetCaptureOptionF32)(RENDERDOC_CaptureOption opt); + +typedef enum RENDERDOC_InputButton { + // '0' - '9' matches ASCII values + eRENDERDOC_Key_0 = 0x30, + eRENDERDOC_Key_1 = 0x31, + eRENDERDOC_Key_2 = 0x32, + eRENDERDOC_Key_3 = 0x33, + eRENDERDOC_Key_4 = 0x34, + eRENDERDOC_Key_5 = 0x35, + eRENDERDOC_Key_6 = 0x36, + eRENDERDOC_Key_7 = 0x37, + eRENDERDOC_Key_8 = 0x38, + eRENDERDOC_Key_9 = 0x39, + + // 'A' - 'Z' matches ASCII values + eRENDERDOC_Key_A = 0x41, + eRENDERDOC_Key_B = 0x42, + eRENDERDOC_Key_C = 0x43, + eRENDERDOC_Key_D = 0x44, + eRENDERDOC_Key_E = 0x45, + eRENDERDOC_Key_F = 0x46, + eRENDERDOC_Key_G = 0x47, + eRENDERDOC_Key_H = 0x48, + eRENDERDOC_Key_I = 0x49, + eRENDERDOC_Key_J = 0x4A, + eRENDERDOC_Key_K = 0x4B, + eRENDERDOC_Key_L = 0x4C, + eRENDERDOC_Key_M = 0x4D, + eRENDERDOC_Key_N = 0x4E, + eRENDERDOC_Key_O = 0x4F, + eRENDERDOC_Key_P = 0x50, + eRENDERDOC_Key_Q = 0x51, + eRENDERDOC_Key_R = 0x52, + eRENDERDOC_Key_S = 0x53, + eRENDERDOC_Key_T = 0x54, + eRENDERDOC_Key_U = 0x55, + eRENDERDOC_Key_V = 0x56, + eRENDERDOC_Key_W = 0x57, + eRENDERDOC_Key_X = 0x58, + eRENDERDOC_Key_Y = 0x59, + eRENDERDOC_Key_Z = 0x5A, + + // leave the rest of the ASCII range free + // in case we want to use it later + eRENDERDOC_Key_NonPrintable = 0x100, + + eRENDERDOC_Key_Divide, + eRENDERDOC_Key_Multiply, + eRENDERDOC_Key_Subtract, + eRENDERDOC_Key_Plus, + + eRENDERDOC_Key_F1, + eRENDERDOC_Key_F2, + eRENDERDOC_Key_F3, + eRENDERDOC_Key_F4, + eRENDERDOC_Key_F5, + eRENDERDOC_Key_F6, + eRENDERDOC_Key_F7, + eRENDERDOC_Key_F8, + eRENDERDOC_Key_F9, + eRENDERDOC_Key_F10, + eRENDERDOC_Key_F11, + eRENDERDOC_Key_F12, + + eRENDERDOC_Key_Home, + eRENDERDOC_Key_End, + eRENDERDOC_Key_Insert, + eRENDERDOC_Key_Delete, + eRENDERDOC_Key_PageUp, + eRENDERDOC_Key_PageDn, + + eRENDERDOC_Key_Backspace, + eRENDERDOC_Key_Tab, + eRENDERDOC_Key_PrtScrn, + eRENDERDOC_Key_Pause, + + eRENDERDOC_Key_Max, +} RENDERDOC_InputButton; + +// Sets which key or keys can be used to toggle focus between multiple windows +// +// If keys is NULL or num is 0, toggle keys will be disabled +typedef void(RENDERDOC_CC *pRENDERDOC_SetFocusToggleKeys)(RENDERDOC_InputButton *keys, int num); + +// Sets which key or keys can be used to capture the next frame +// +// If keys is NULL or num is 0, captures keys will be disabled +typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureKeys)(RENDERDOC_InputButton *keys, int num); + +typedef enum RENDERDOC_OverlayBits { + // This single bit controls whether the overlay is enabled or disabled globally + eRENDERDOC_Overlay_Enabled = 0x1, + + // Show the average framerate over several seconds as well as min/max + eRENDERDOC_Overlay_FrameRate = 0x2, + + // Show the current frame number + eRENDERDOC_Overlay_FrameNumber = 0x4, + + // Show a list of recent captures, and how many captures have been made + eRENDERDOC_Overlay_CaptureList = 0x8, + + // Default values for the overlay mask + eRENDERDOC_Overlay_Default = + (eRENDERDOC_Overlay_Enabled | eRENDERDOC_Overlay_FrameRate | eRENDERDOC_Overlay_FrameNumber | eRENDERDOC_Overlay_CaptureList), + + // Enable all bits + eRENDERDOC_Overlay_All = ~0U, + + // Disable all bits + eRENDERDOC_Overlay_None = 0, +} RENDERDOC_OverlayBits; + +// returns the overlay bits that have been set +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetOverlayBits)(); +// sets the overlay bits with an and & or mask +typedef void(RENDERDOC_CC *pRENDERDOC_MaskOverlayBits)(uint32_t And, uint32_t Or); + +// this function will attempt to remove RenderDoc's hooks in the application. +// +// Note: that this can only work correctly if done immediately after +// the module is loaded, before any API work happens. RenderDoc will remove its +// injected hooks and shut down. Behaviour is undefined if this is called +// after any API functions have been called, and there is still no guarantee of +// success. +typedef void(RENDERDOC_CC *pRENDERDOC_RemoveHooks)(); + +// DEPRECATED: compatibility for code compiled against pre-1.4.1 headers. +typedef pRENDERDOC_RemoveHooks pRENDERDOC_Shutdown; + +// This function will unload RenderDoc's crash handler. +// +// If you use your own crash handler and don't want RenderDoc's handler to +// intercede, you can call this function to unload it and any unhandled +// exceptions will pass to the next handler. +typedef void(RENDERDOC_CC *pRENDERDOC_UnloadCrashHandler)(); + +// Sets the capture file path template +// +// pathtemplate is a UTF-8 string that gives a template for how captures will be named +// and where they will be saved. +// +// Any extension is stripped off the path, and captures are saved in the directory +// specified, and named with the filename and the frame number appended. If the +// directory does not exist it will be created, including any parent directories. +// +// If pathtemplate is NULL, the template will remain unchanged +// +// Example: +// +// SetCaptureFilePathTemplate("my_captures/example"); +// +// Capture #1 -> my_captures/example_frame123.rdc +// Capture #2 -> my_captures/example_frame456.rdc +typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureFilePathTemplate)(const char *pathtemplate); + +// returns the current capture path template, see SetCaptureFileTemplate above, as a UTF-8 string +typedef const char *(RENDERDOC_CC *pRENDERDOC_GetCaptureFilePathTemplate)(); + +// DEPRECATED: compatibility for code compiled against pre-1.1.2 headers. +typedef pRENDERDOC_SetCaptureFilePathTemplate pRENDERDOC_SetLogFilePathTemplate; +typedef pRENDERDOC_GetCaptureFilePathTemplate pRENDERDOC_GetLogFilePathTemplate; + +// returns the number of captures that have been made +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetNumCaptures)(); + +// This function returns the details of a capture, by index. New captures are added +// to the end of the list. +// +// filename will be filled with the absolute path to the capture file, as a UTF-8 string +// pathlength will be written with the length in bytes of the filename string +// timestamp will be written with the time of the capture, in seconds since the Unix epoch +// +// Any of the parameters can be NULL and they'll be skipped. +// +// The function will return 1 if the capture index is valid, or 0 if the index is invalid +// If the index is invalid, the values will be unchanged +// +// Note: when captures are deleted in the UI they will remain in this list, so the +// capture path may not exist anymore. +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_GetCapture)(uint32_t idx, char *filename, uint32_t *pathlength, uint64_t *timestamp); + +// Sets the comments associated with a capture file. These comments are displayed in the +// UI program when opening. +// +// filePath should be a path to the capture file to add comments to. If set to NULL or "" +// the most recent capture file created made will be used instead. +// comments should be a NULL-terminated UTF-8 string to add as comments. +// +// Any existing comments will be overwritten. +typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureFileComments)(const char *filePath, const char *comments); + +// returns 1 if the RenderDoc UI is connected to this application, 0 otherwise +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_IsTargetControlConnected)(); + +// DEPRECATED: compatibility for code compiled against pre-1.1.1 headers. +// This was renamed to IsTargetControlConnected in API 1.1.1, the old typedef is kept here for +// backwards compatibility with old code, it is castable either way since it's ABI compatible +// as the same function pointer type. +typedef pRENDERDOC_IsTargetControlConnected pRENDERDOC_IsRemoteAccessConnected; + +// This function will launch the Replay UI associated with the RenderDoc library injected +// into the running application. +// +// if connectTargetControl is 1, the Replay UI will be launched with a command line parameter +// to connect to this application +// cmdline is the rest of the command line, as a UTF-8 string. E.g. a captures to open +// if cmdline is NULL, the command line will be empty. +// +// returns the PID of the replay UI if successful, 0 if not successful. +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_LaunchReplayUI)(uint32_t connectTargetControl, const char *cmdline); + +// RenderDoc can return a higher version than requested if it's backwards compatible, +// this function returns the actual version returned. If a parameter is NULL, it will be +// ignored and the others will be filled out. +typedef void(RENDERDOC_CC *pRENDERDOC_GetAPIVersion)(int *major, int *minor, int *patch); + +// Requests that the replay UI show itself (if hidden or not the current top window). This can be +// used in conjunction with IsTargetControlConnected and LaunchReplayUI to intelligently handle +// showing the UI after making a capture. +// +// This will return 1 if the request was successfully passed on, though it's not guaranteed that +// the UI will be on top in all cases depending on OS rules. It will return 0 if there is no current +// target control connection to make such a request, or if there was another error +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_ShowReplayUI)(); + +////////////////////////////////////////////////////////////////////////// +// Capturing functions +// + +// A device pointer is a pointer to the API's root handle. +// +// This would be an ID3D11Device, HGLRC/GLXContext, ID3D12Device, etc +typedef void *RENDERDOC_DevicePointer; + +// A window handle is the OS's native window handle +// +// This would be an HWND, GLXDrawable, etc +typedef void *RENDERDOC_WindowHandle; + +// A helper macro for Vulkan, where the device handle cannot be used directly. +// +// Passing the VkInstance to this macro will return the RENDERDOC_DevicePointer to use. +// +// Specifically, the value needed is the dispatch table pointer, which sits as the first +// pointer-sized object in the memory pointed to by the VkInstance. Thus we cast to a void** and +// indirect once. +#define RENDERDOC_DEVICEPOINTER_FROM_VKINSTANCE(inst) (*((void **)(inst))) + +// This sets the RenderDoc in-app overlay in the API/window pair as 'active' and it will +// respond to keypresses. Neither parameter can be NULL +typedef void(RENDERDOC_CC *pRENDERDOC_SetActiveWindow)(RENDERDOC_DevicePointer device, RENDERDOC_WindowHandle wndHandle); + +// capture the next frame on whichever window and API is currently considered active +typedef void(RENDERDOC_CC *pRENDERDOC_TriggerCapture)(); + +// capture the next N frames on whichever window and API is currently considered active +typedef void(RENDERDOC_CC *pRENDERDOC_TriggerMultiFrameCapture)(uint32_t numFrames); + +// When choosing either a device pointer or a window handle to capture, you can pass NULL. +// Passing NULL specifies a 'wildcard' match against anything. This allows you to specify +// any API rendering to a specific window, or a specific API instance rendering to any window, +// or in the simplest case of one window and one API, you can just pass NULL for both. +// +// In either case, if there are two or more possible matching (device,window) pairs it +// is undefined which one will be captured. +// +// Note: for headless rendering you can pass NULL for the window handle and either specify +// a device pointer or leave it NULL as above. + +// Immediately starts capturing API calls on the specified device pointer and window handle. +// +// If there is no matching thing to capture (e.g. no supported API has been initialised), +// this will do nothing. +// +// The results are undefined (including crashes) if two captures are started overlapping, +// even on separate devices and/oror windows. +typedef void(RENDERDOC_CC *pRENDERDOC_StartFrameCapture)(RENDERDOC_DevicePointer device, RENDERDOC_WindowHandle wndHandle); + +// Returns whether or not a frame capture is currently ongoing anywhere. +// +// This will return 1 if a capture is ongoing, and 0 if there is no capture running +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_IsFrameCapturing)(); + +// Ends capturing immediately. +// +// This will return 1 if the capture succeeded, and 0 if there was an error capturing. +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_EndFrameCapture)(RENDERDOC_DevicePointer device, RENDERDOC_WindowHandle wndHandle); + +// Ends capturing immediately and discard any data stored without saving to disk. +// +// This will return 1 if the capture was discarded, and 0 if there was an error or no capture +// was in progress +typedef uint32_t(RENDERDOC_CC *pRENDERDOC_DiscardFrameCapture)(RENDERDOC_DevicePointer device, RENDERDOC_WindowHandle wndHandle); + +// Only valid to be called between a call to StartFrameCapture and EndFrameCapture. Gives a custom +// title to the capture produced which will be displayed in the UI. +// +// If multiple captures are ongoing, this title will be applied to the first capture to end after +// this call. The second capture to end will have no title, unless this function is called again. +// +// Calling this function has no effect if no capture is currently running, and if it is called +// multiple times only the last title will be used. +typedef void(RENDERDOC_CC *pRENDERDOC_SetCaptureTitle)(const char *title); + +////////////////////////////////////////////////////////////////////////////////////////////////// +// RenderDoc API versions +// + +// RenderDoc uses semantic versioning (http://semver.org/). +// +// MAJOR version is incremented when incompatible API changes happen. +// MINOR version is incremented when functionality is added in a backwards-compatible manner. +// PATCH version is incremented when backwards-compatible bug fixes happen. +// +// Note that this means the API returned can be higher than the one you might have requested. +// e.g. if you are running against a newer RenderDoc that supports 1.0.1, it will be returned +// instead of 1.0.0. You can check this with the GetAPIVersion entry point +typedef enum RENDERDOC_Version { + eRENDERDOC_API_Version_1_0_0 = 10000, // RENDERDOC_API_1_0_0 = 1 00 00 + eRENDERDOC_API_Version_1_0_1 = 10001, // RENDERDOC_API_1_0_1 = 1 00 01 + eRENDERDOC_API_Version_1_0_2 = 10002, // RENDERDOC_API_1_0_2 = 1 00 02 + eRENDERDOC_API_Version_1_1_0 = 10100, // RENDERDOC_API_1_1_0 = 1 01 00 + eRENDERDOC_API_Version_1_1_1 = 10101, // RENDERDOC_API_1_1_1 = 1 01 01 + eRENDERDOC_API_Version_1_1_2 = 10102, // RENDERDOC_API_1_1_2 = 1 01 02 + eRENDERDOC_API_Version_1_2_0 = 10200, // RENDERDOC_API_1_2_0 = 1 02 00 + eRENDERDOC_API_Version_1_3_0 = 10300, // RENDERDOC_API_1_3_0 = 1 03 00 + eRENDERDOC_API_Version_1_4_0 = 10400, // RENDERDOC_API_1_4_0 = 1 04 00 + eRENDERDOC_API_Version_1_4_1 = 10401, // RENDERDOC_API_1_4_1 = 1 04 01 + eRENDERDOC_API_Version_1_4_2 = 10402, // RENDERDOC_API_1_4_2 = 1 04 02 + eRENDERDOC_API_Version_1_5_0 = 10500, // RENDERDOC_API_1_5_0 = 1 05 00 + eRENDERDOC_API_Version_1_6_0 = 10600, // RENDERDOC_API_1_6_0 = 1 06 00 +} RENDERDOC_Version; + +// API version changelog: +// +// 1.0.0 - initial release +// 1.0.1 - Bugfix: IsFrameCapturing() was returning false for captures that were triggered +// by keypress or TriggerCapture, instead of Start/EndFrameCapture. +// 1.0.2 - Refactor: Renamed eRENDERDOC_Option_DebugDeviceMode to eRENDERDOC_Option_APIValidation +// 1.1.0 - Add feature: TriggerMultiFrameCapture(). Backwards compatible with 1.0.x since the new +// function pointer is added to the end of the struct, the original layout is identical +// 1.1.1 - Refactor: Renamed remote access to target control (to better disambiguate from remote +// replay/remote server concept in replay UI) +// 1.1.2 - Refactor: Renamed "log file" in function names to just capture, to clarify that these +// are captures and not debug logging files. This is the first API version in the v1.0 +// branch. +// 1.2.0 - Added feature: SetCaptureFileComments() to add comments to a capture file that will be +// displayed in the UI program on load. +// 1.3.0 - Added feature: New capture option eRENDERDOC_Option_AllowUnsupportedVendorExtensions +// which allows users to opt-in to allowing unsupported vendor extensions to function. +// Should be used at the user's own risk. +// Refactor: Renamed eRENDERDOC_Option_VerifyMapWrites to +// eRENDERDOC_Option_VerifyBufferAccess, which now also controls initialisation to +// 0xdddddddd of uninitialised buffer contents. +// 1.4.0 - Added feature: DiscardFrameCapture() to discard a frame capture in progress and stop +// capturing without saving anything to disk. +// 1.4.1 - Refactor: Renamed Shutdown to RemoveHooks to better clarify what is happening +// 1.4.2 - Refactor: Renamed 'draws' to 'actions' in callstack capture option. +// 1.5.0 - Added feature: ShowReplayUI() to request that the replay UI show itself if connected +// 1.6.0 - Added feature: SetCaptureTitle() which can be used to set a title for a +// capture made with StartFrameCapture() or EndFrameCapture() + +typedef struct RENDERDOC_API_1_6_0 { + pRENDERDOC_GetAPIVersion GetAPIVersion; + + pRENDERDOC_SetCaptureOptionU32 SetCaptureOptionU32; + pRENDERDOC_SetCaptureOptionF32 SetCaptureOptionF32; + + pRENDERDOC_GetCaptureOptionU32 GetCaptureOptionU32; + pRENDERDOC_GetCaptureOptionF32 GetCaptureOptionF32; + + pRENDERDOC_SetFocusToggleKeys SetFocusToggleKeys; + pRENDERDOC_SetCaptureKeys SetCaptureKeys; + + pRENDERDOC_GetOverlayBits GetOverlayBits; + pRENDERDOC_MaskOverlayBits MaskOverlayBits; + + // Shutdown was renamed to RemoveHooks in 1.4.1. + // These unions allow old code to continue compiling without changes + union { + pRENDERDOC_Shutdown Shutdown; + pRENDERDOC_RemoveHooks RemoveHooks; + }; + pRENDERDOC_UnloadCrashHandler UnloadCrashHandler; + + // Get/SetLogFilePathTemplate was renamed to Get/SetCaptureFilePathTemplate in 1.1.2. + // These unions allow old code to continue compiling without changes + union { + // deprecated name + pRENDERDOC_SetLogFilePathTemplate SetLogFilePathTemplate; + // current name + pRENDERDOC_SetCaptureFilePathTemplate SetCaptureFilePathTemplate; + }; + union { + // deprecated name + pRENDERDOC_GetLogFilePathTemplate GetLogFilePathTemplate; + // current name + pRENDERDOC_GetCaptureFilePathTemplate GetCaptureFilePathTemplate; + }; + + pRENDERDOC_GetNumCaptures GetNumCaptures; + pRENDERDOC_GetCapture GetCapture; + + pRENDERDOC_TriggerCapture TriggerCapture; + + // IsRemoteAccessConnected was renamed to IsTargetControlConnected in 1.1.1. + // This union allows old code to continue compiling without changes + union { + // deprecated name + pRENDERDOC_IsRemoteAccessConnected IsRemoteAccessConnected; + // current name + pRENDERDOC_IsTargetControlConnected IsTargetControlConnected; + }; + pRENDERDOC_LaunchReplayUI LaunchReplayUI; + + pRENDERDOC_SetActiveWindow SetActiveWindow; + + pRENDERDOC_StartFrameCapture StartFrameCapture; + pRENDERDOC_IsFrameCapturing IsFrameCapturing; + pRENDERDOC_EndFrameCapture EndFrameCapture; + + // new function in 1.1.0 + pRENDERDOC_TriggerMultiFrameCapture TriggerMultiFrameCapture; + + // new function in 1.2.0 + pRENDERDOC_SetCaptureFileComments SetCaptureFileComments; + + // new function in 1.4.0 + pRENDERDOC_DiscardFrameCapture DiscardFrameCapture; + + // new function in 1.5.0 + pRENDERDOC_ShowReplayUI ShowReplayUI; + + // new function in 1.6.0 + pRENDERDOC_SetCaptureTitle SetCaptureTitle; +} RENDERDOC_API_1_6_0; + +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_0_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_0_1; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_0_2; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_1_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_1_1; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_1_2; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_2_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_3_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_4_0; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_4_1; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_4_2; +typedef RENDERDOC_API_1_6_0 RENDERDOC_API_1_5_0; + +////////////////////////////////////////////////////////////////////////////////////////////////// +// RenderDoc API entry point +// +// This entry point can be obtained via GetProcAddress/dlsym if RenderDoc is available. +// +// The name is the same as the typedef - "RENDERDOC_GetAPI" +// +// This function is not thread safe, and should not be called on multiple threads at once. +// Ideally, call this once as early as possible in your application's startup, before doing +// any API work, since some configuration functionality etc has to be done also before +// initialising any APIs. +// +// Parameters: +// version is a single value from the RENDERDOC_Version above. +// +// outAPIPointers will be filled out with a pointer to the corresponding struct of function +// pointers. +// +// Returns: +// 1 - if the outAPIPointers has been filled with a pointer to the API struct requested +// 0 - if the requested version is not supported or the arguments are invalid. +// +typedef int(RENDERDOC_CC *pRENDERDOC_GetAPI)(RENDERDOC_Version version, void **outAPIPointers); + +#ifdef __cplusplus +} // extern "C" +#endif \ No newline at end of file From 1674ad5a2c50c78e02fe366c7bd7c660cd3cef8e Mon Sep 17 00:00:00 2001 From: shinra-electric <50119606+shinra-electric@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:36:41 +0100 Subject: [PATCH 194/251] macOS CI fixes (#587) * Update checkout to v4 * Update upload-artifact to v4 * don't try to reinstall python * Update to v4 in Qt_Build.yml too * remove python re-install in Qt_Build.yml too --- .github/workflows/MacOS_Build.yml | 6 +++--- .github/workflows/Qt_Build.yml | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/MacOS_Build.yml b/.github/workflows/MacOS_Build.yml index 912c8568..76b75bd4 100644 --- a/.github/workflows/MacOS_Build.yml +++ b/.github/workflows/MacOS_Build.yml @@ -19,7 +19,7 @@ jobs: runs-on: macos-13 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Fetch submodules run: git submodule update --init --recursive @@ -40,7 +40,7 @@ jobs: run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - name: Install bundle dependencies - run: brew install --overwrite python@3.12 && brew install dylibbundler imagemagick + run: brew install dylibbundler imagemagick - name: Run bundle script run: ./.github/mac-bundle.sh @@ -52,7 +52,7 @@ jobs: run: zip -r Alber Alber.app - name: Upload MacOS App - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: MacOS Alber App Bundle path: 'Alber.zip' diff --git a/.github/workflows/Qt_Build.yml b/.github/workflows/Qt_Build.yml index 40141fb1..4485cc1c 100644 --- a/.github/workflows/Qt_Build.yml +++ b/.github/workflows/Qt_Build.yml @@ -54,7 +54,7 @@ jobs: runs-on: macos-13 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Fetch submodules run: git submodule update --init --recursive @@ -67,7 +67,7 @@ jobs: - name: Install bundle dependencies run: | - brew install --overwrite python@3.12 && brew install dylibbundler imagemagick + brew install dylibbundler imagemagick - name: Install qt run: brew install qt && which macdeployqt @@ -90,7 +90,7 @@ jobs: run: zip -r Alber Alber.app - name: Upload MacOS App - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: MacOS Alber App Bundle path: 'Alber.zip' From e421f02500c01ec40bad2658059442f8e10ea5bf Mon Sep 17 00:00:00 2001 From: offtkp Date: Tue, 27 Aug 2024 17:12:12 +0300 Subject: [PATCH 195/251] GLES <= 3.1 lacks fma, added a define --- src/core/PICA/shader_gen_glsl.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 60887d56..aa605dd2 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -44,6 +44,7 @@ std::string FragmentGenerator::getDefaultVertexShader() { if (api == API::GLES) { ret += R"( #define USING_GLES 1 + #define fma(a, b, c) ((a) * (b) + (c)) precision mediump int; precision mediump float; From 2cffafff86cbab5596bd3b53760ce0a281e15263 Mon Sep 17 00:00:00 2001 From: offtkp Date: Tue, 27 Aug 2024 17:16:11 +0300 Subject: [PATCH 196/251] Update gles.patch --- .github/gles.patch | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/.github/gles.patch b/.github/gles.patch index 5a922fcf..c5cdb7d4 100644 --- a/.github/gles.patch +++ b/.github/gles.patch @@ -21,7 +21,7 @@ index 990e2f80..2e7842ac 100644 void main() { diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag -index b9f9fe4c..f1cf286f 100644 +index 9f07df0b..2ab623af 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -1,4 +1,5 @@ @@ -31,6 +31,17 @@ index b9f9fe4c..f1cf286f 100644 in vec4 v_quaternion; in vec4 v_colour; +@@ -41,8 +42,8 @@ vec3 normal; + const uint samplerEnabledBitfields[2] = uint[2](0x7170e645u, 0x7f013fefu); + + bool isSamplerEnabled(uint environment_id, uint lut_id) { +- uint index = 7 * environment_id + lut_id; +- uint arrayIndex = (index >> 5); ++ uint index = 7u * environment_id + lut_id; ++ uint arrayIndex = (index >> 5u); + return (samplerEnabledBitfields[arrayIndex] & (1u << (index & 31u))) != 0u; + } + @@ -166,11 +167,17 @@ float lutLookup(uint lut, int index) { return texelFetch(u_tex_luts, ivec2(index, int(lut)), 0).r; } @@ -50,6 +61,15 @@ index b9f9fe4c..f1cf286f 100644 } // Convert an arbitrary-width floating point literal to an f32 +@@ -201,7 +208,7 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light + // These are the spotlight attenuation LUTs + bit_in_config1 = 8 + int(light_id & 7u); + lut_index = 8u + light_id; +- } else if (lut_id <= 6) { ++ } else if (lut_id <= 6u) { + bit_in_config1 = 16 + int(lut_id); + lut_index = lut_id; + } else { @@ -210,16 +217,16 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light bool current_sampler_enabled = isSamplerEnabled(environment_id, lut_id); // 7 luts per environment @@ -70,19 +90,16 @@ index b9f9fe4c..f1cf286f 100644 switch (input_id) { case 0u: { delta = dot(normal, normalize(half_vector)); -@@ -241,11 +248,11 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light - int GPUREG_LIGHTi_SPOTDIR_LOW = int(readPicaReg(0x0146u + (light_id << 4u))); - int GPUREG_LIGHTi_SPOTDIR_HIGH = int(readPicaReg(0x0147u + (light_id << 4u))); +@@ -243,9 +250,9 @@ float lightLutLookup(uint environment_id, uint lut_id, uint light_id, vec3 light -- // Sign extend them. Normally bitfieldExtract would do that but it's missing on some versions -+ // Sign extend them. Normally bitfieldExtractCompat would do that but it's missing on some versions + // Sign extend them. Normally bitfieldExtract would do that but it's missing on some versions // of GLSL so we do it manually - int se_x = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13); - int se_y = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13); - int se_z = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13); -+ int se_x = bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13); -+ int se_y = bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13); -+ int se_z = bitfieldExtractCompat(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13); ++ int se_x = bitfieldExtract(uint(GPUREG_LIGHTi_SPOTDIR_LOW), 0, 13); ++ int se_y = bitfieldExtract(uint(GPUREG_LIGHTi_SPOTDIR_LOW), 16, 13); ++ int se_z = bitfieldExtract(uint(GPUREG_LIGHTi_SPOTDIR_HIGH), 0, 13); if ((se_x & 0x1000) == 0x1000) se_x |= 0xffffe000; if ((se_y & 0x1000) == 0x1000) se_y |= 0xffffe000; @@ -225,10 +242,10 @@ index 057f9a88..dc735ced 100644 v_quaternion = a_quaternion; } diff --git a/third_party/opengl/opengl.hpp b/third_party/opengl/opengl.hpp -index 4a08650a..21af37e3 100644 +index 607815fa..cbfcc096 100644 --- a/third_party/opengl/opengl.hpp +++ b/third_party/opengl/opengl.hpp -@@ -583,22 +583,22 @@ namespace OpenGL { +@@ -602,22 +602,22 @@ namespace OpenGL { static void disableScissor() { glDisable(GL_SCISSOR_TEST); } static void enableBlend() { glEnable(GL_BLEND); } static void disableBlend() { glDisable(GL_BLEND); } From 201edfb02df05025c3443fe067c4777915d9df6c Mon Sep 17 00:00:00 2001 From: Paris Oplopoios Date: Tue, 27 Aug 2024 19:47:27 +0300 Subject: [PATCH 197/251] I hate the gles.patch (#590) --- .github/gles.patch | 8 ++++---- src/core/PICA/shader_gen_glsl.cpp | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/gles.patch b/.github/gles.patch index c5cdb7d4..50721dac 100644 --- a/.github/gles.patch +++ b/.github/gles.patch @@ -21,7 +21,7 @@ index 990e2f80..2e7842ac 100644 void main() { diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag -index 9f07df0b..2ab623af 100644 +index 9f07df0b..75b708f7 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -1,4 +1,5 @@ @@ -97,9 +97,9 @@ index 9f07df0b..2ab623af 100644 - int se_x = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13); - int se_y = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13); - int se_z = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13); -+ int se_x = bitfieldExtract(uint(GPUREG_LIGHTi_SPOTDIR_LOW), 0, 13); -+ int se_y = bitfieldExtract(uint(GPUREG_LIGHTi_SPOTDIR_LOW), 16, 13); -+ int se_z = bitfieldExtract(uint(GPUREG_LIGHTi_SPOTDIR_HIGH), 0, 13); ++ int se_x = bitfieldExtractCompat(uint(GPUREG_LIGHTi_SPOTDIR_LOW), 0, 13); ++ int se_y = bitfieldExtractCompat(uint(GPUREG_LIGHTi_SPOTDIR_LOW), 16, 13); ++ int se_z = bitfieldExtractCompat(uint(GPUREG_LIGHTi_SPOTDIR_HIGH), 0, 13); if ((se_x & 0x1000) == 0x1000) se_x |= 0xffffe000; if ((se_y & 0x1000) == 0x1000) se_y |= 0xffffe000; diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index aa605dd2..154e403c 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -44,7 +44,6 @@ std::string FragmentGenerator::getDefaultVertexShader() { if (api == API::GLES) { ret += R"( #define USING_GLES 1 - #define fma(a, b, c) ((a) * (b) + (c)) precision mediump int; precision mediump float; @@ -108,6 +107,7 @@ std::string FragmentGenerator::generate(const FragmentConfig& config) { if (api == API::GLES) { ret += R"( #define USING_GLES 1 + #define fma(a, b, c) ((a) * (b) + (c)) precision mediump int; precision mediump float; From 595e4e0341e86c5f49befd168d675e032bbcb7f1 Mon Sep 17 00:00:00 2001 From: Paris Oplopoios Date: Wed, 28 Aug 2024 03:02:54 +0300 Subject: [PATCH 198/251] More implicit conversion fixes, hopefully the last ones this time (#591) * No implicit uint conversion * Update gles.patch --- .github/gles.patch | 8 ++++---- src/core/PICA/shader_gen_glsl.cpp | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/gles.patch b/.github/gles.patch index 50721dac..548b243d 100644 --- a/.github/gles.patch +++ b/.github/gles.patch @@ -21,7 +21,7 @@ index 990e2f80..2e7842ac 100644 void main() { diff --git a/src/host_shaders/opengl_fragment_shader.frag b/src/host_shaders/opengl_fragment_shader.frag -index 9f07df0b..75b708f7 100644 +index 9f07df0b..96a35afa 100644 --- a/src/host_shaders/opengl_fragment_shader.frag +++ b/src/host_shaders/opengl_fragment_shader.frag @@ -1,4 +1,5 @@ @@ -97,9 +97,9 @@ index 9f07df0b..75b708f7 100644 - int se_x = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 0, 13); - int se_y = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_LOW, 16, 13); - int se_z = bitfieldExtract(GPUREG_LIGHTi_SPOTDIR_HIGH, 0, 13); -+ int se_x = bitfieldExtractCompat(uint(GPUREG_LIGHTi_SPOTDIR_LOW), 0, 13); -+ int se_y = bitfieldExtractCompat(uint(GPUREG_LIGHTi_SPOTDIR_LOW), 16, 13); -+ int se_z = bitfieldExtractCompat(uint(GPUREG_LIGHTi_SPOTDIR_HIGH), 0, 13); ++ int se_x = int(bitfieldExtractCompat(uint(GPUREG_LIGHTi_SPOTDIR_LOW), 0, 13)); ++ int se_y = int(bitfieldExtractCompat(uint(GPUREG_LIGHTi_SPOTDIR_LOW), 16, 13)); ++ int se_z = int(bitfieldExtractCompat(uint(GPUREG_LIGHTi_SPOTDIR_HIGH), 0, 13)); if ((se_x & 0x1000) == 0x1000) se_x |= 0xffffe000; if ((se_y & 0x1000) == 0x1000) se_y |= 0xffffe000; diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 154e403c..69f74930 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -503,7 +503,7 @@ void FragmentGenerator::compileLights(std::string& shader, const PICA::FragmentC "].distanceAttenuationScale + lightSources[" + std::to_string(lightID) + "].distanceAttenuationBias, 0.0, 1.0);\n"; shader += "distance_attenuation = lutLookup(" + std::to_string(16 + lightID) + - ", int(clamp(floor(distance_att_delta * 256.0), 0.0, 255.0)));\n"; + "u, int(clamp(floor(distance_att_delta * 256.0), 0.0, 255.0)));\n"; } compileLUTLookup(shader, config, i, spotlightLutIndex); @@ -638,7 +638,7 @@ void FragmentGenerator::compileLUTLookup(std::string& shader, const PICA::Fragme if (absEnabled) { bool twoSidedDiffuse = config.lighting.lights[lightIndex].twoSidedDiffuse; shader += twoSidedDiffuse ? "lut_lookup_delta = abs(lut_lookup_delta);\n" : "lut_lookup_delta = max(lut_lookup_delta, 0.0);\n"; - shader += "lut_lookup_result = lutLookup(" + std::to_string(lutIndex) + ", int(clamp(floor(lut_lookup_delta * 256.0), 0.0, 255.0)));\n"; + shader += "lut_lookup_result = lutLookup(" + std::to_string(lutIndex) + "u, int(clamp(floor(lut_lookup_delta * 256.0), 0.0, 255.0)));\n"; if (scale != 0) { shader += "lut_lookup_result *= " + std::to_string(scales[scale]) + ";\n"; } @@ -646,7 +646,7 @@ void FragmentGenerator::compileLUTLookup(std::string& shader, const PICA::Fragme // Range is [-1, 1] so we need to map it to [0, 1] shader += "lut_lookup_index = int(clamp(floor(lut_lookup_delta * 128.0), -128.f, 127.f));\n"; shader += "if (lut_lookup_index < 0) lut_lookup_index += 256;\n"; - shader += "lut_lookup_result = lutLookup(" + std::to_string(lutIndex) + ", lut_lookup_index);\n"; + shader += "lut_lookup_result = lutLookup(" + std::to_string(lutIndex) + "u, lut_lookup_index);\n"; if (scale != 0) { shader += "lut_lookup_result *= " + std::to_string(scales[scale]) + ";\n"; } From 4adc50039cc22de2ee730dc83074760d72a8f3ce Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Wed, 28 Aug 2024 15:01:55 +0300 Subject: [PATCH 199/251] Add build option for opengl profile (#592) * Add opengl_profile build option on android the option is set to OpenGLES by default * Replace android checks with using_gles --- CMakeLists.txt | 14 ++++++++++++++ src/core/renderer_gl/renderer_gl.cpp | 2 +- src/hydra_core.cpp | 2 +- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 71c86578..107593d0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -33,6 +33,12 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-interference-size") endif() +if(ANDROID) + set(DEFAULT_OPENGL_PROFILE OpenGLES) +else() + set(DEFAULT_OPENGL_PROFILE OpenGL) +endif() + option(DISABLE_PANIC_DEV "Make a build with fewer and less intrusive asserts" ON) option(GPU_DEBUG_INFO "Enable additional GPU debugging info" OFF) option(ENABLE_OPENGL "Enable OpenGL rendering backend" ON) @@ -49,6 +55,14 @@ option(BUILD_HYDRA_CORE "Build a Hydra core" OFF) option(BUILD_LIBRETRO_CORE "Build a Libretro core" OFF) option(ENABLE_RENDERDOC_API "Build with support for Renderdoc's capture API for graphics debugging" ON) +set(OPENGL_PROFILE ${DEFAULT_OPENGL_PROFILE} CACHE STRING "OpenGL profile to use if OpenGL is enabled. Valid values are 'OpenGL' and 'OpenGLES'.") +set_property(CACHE OPENGL_PROFILE PROPERTY STRINGS OpenGL OpenGLES) + +if(ENABLE_OPENGL AND (OPENGL_PROFILE STREQUAL "OpenGLES")) + message(STATUS "Building with OpenGLES support") + add_compile_definitions(USING_GLES) +endif() + if(BUILD_HYDRA_CORE) set(CMAKE_POSITION_INDEPENDENT_CODE ON) endif() diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index f8fc31e7..5146370a 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -49,7 +49,7 @@ void RendererGL::reset() { gl.useProgram(oldProgram); // Switch to old GL program } -#ifdef __ANDROID__ +#ifdef USING_GLES fragShaderGen.setTarget(PICA::ShaderGen::API::GLES, PICA::ShaderGen::Language::GLSL); #endif } diff --git a/src/hydra_core.cpp b/src/hydra_core.cpp index 04fcdbdb..078b8a6c 100644 --- a/src/hydra_core.cpp +++ b/src/hydra_core.cpp @@ -114,7 +114,7 @@ hydra::Size HydraCore::getNativeSize() { return {400, 480}; } void HydraCore::setOutputSize(hydra::Size size) {} void HydraCore::resetContext() { -#ifdef __ANDROID__ +#ifdef USING_GLES if (!gladLoadGLES2Loader(reinterpret_cast(getProcAddress))) { Helpers::panic("OpenGL ES init failed"); } From 8830747e90a119d50fb5c7063af1f9e08bfe4cdd Mon Sep 17 00:00:00 2001 From: Samuliak Date: Thu, 29 Aug 2024 19:43:36 +0200 Subject: [PATCH 200/251] clear render targets after creation --- .../renderer_mtl/mtl_vertex_buffer_cache.hpp | 2 +- src/core/renderer_mtl/renderer_mtl.cpp | 19 ++++++++++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/include/renderer_mtl/mtl_vertex_buffer_cache.hpp b/include/renderer_mtl/mtl_vertex_buffer_cache.hpp index 1760cdfa..8aa299da 100644 --- a/include/renderer_mtl/mtl_vertex_buffer_cache.hpp +++ b/include/renderer_mtl/mtl_vertex_buffer_cache.hpp @@ -12,7 +12,7 @@ struct BufferHandle { }; // 64MB buffer for caching vertex data -#define CACHE_BUFFER_SIZE 64 * 1024 * 1024 +#define CACHE_BUFFER_SIZE 128 * 1024 * 1024 class VertexBufferCache { public: diff --git a/src/core/renderer_mtl/renderer_mtl.cpp b/src/core/renderer_mtl/renderer_mtl.cpp index 10bca5dd..bdb5390d 100644 --- a/src/core/renderer_mtl/renderer_mtl.cpp +++ b/src/core/renderer_mtl/renderer_mtl.cpp @@ -413,7 +413,7 @@ void RendererMTL::textureCopy(u32 inputAddr, u32 outputAddr, u32 totalBytes, u32 // Find the source surface. auto srcFramebuffer = getColorRenderTarget(inputAddr, PICA::ColorFmt::RGBA8, copyStride, copyHeight, false); if (!srcFramebuffer) { - Helpers::warn("RendererGL::TextureCopy failed to locate src framebuffer!\n"); + Helpers::warn("RendererMTL::TextureCopy failed to locate src framebuffer!\n"); return; } nextRenderPassName = "Clear before texture copy"; @@ -605,7 +605,12 @@ std::optional RendererMTL::getColorRenderTarget( // Otherwise create and cache a new buffer. Metal::ColorRenderTarget sampleBuffer(device, addr, format, width, height); - return colorRenderTargetCache.add(sampleBuffer); + auto& colorBuffer = colorRenderTargetCache.add(sampleBuffer); + + // Clear the color buffer + colorClearOps[colorBuffer.texture] = {0, 0, 0, 0}; + + return colorBuffer; } Metal::DepthStencilRenderTarget& RendererMTL::getDepthRenderTarget() { @@ -615,7 +620,15 @@ Metal::DepthStencilRenderTarget& RendererMTL::getDepthRenderTarget() { if (buffer.has_value()) { return buffer.value().get(); } else { - return depthStencilRenderTargetCache.add(sampleBuffer); + auto& depthBuffer = depthStencilRenderTargetCache.add(sampleBuffer); + + // Clear the depth buffer + depthClearOps[depthBuffer.texture] = 0.0f; + if (depthBuffer.format == DepthFmt::Depth24Stencil8) { + stencilClearOps[depthBuffer.texture] = 0; + } + + return depthBuffer; } } From 9055076d0d81d7b83fc88d652020a21f9a8f440d Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Wed, 4 Sep 2024 22:31:19 +0300 Subject: [PATCH 201/251] Add fdk-aac & AAC request structure --- .gitmodules | 3 +++ CMakeLists.txt | 3 ++- include/audio/aac.hpp | 9 +++++++++ third_party/fdk-aac | 1 + 4 files changed, 15 insertions(+), 1 deletion(-) create mode 160000 third_party/fdk-aac diff --git a/.gitmodules b/.gitmodules index 656e1f41..981b4426 100644 --- a/.gitmodules +++ b/.gitmodules @@ -76,3 +76,6 @@ [submodule "third_party/metal-cpp"] path = third_party/metal-cpp url = https://github.com/Panda3DS-emu/metal-cpp +[submodule "third_party/fdk-aac"] + path = third_party/fdk-aac + url = https://github.com/mstorsjo/fdk-aac/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 107593d0..d1a67b79 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -225,6 +225,7 @@ else() endif() add_subdirectory(third_party/teakra EXCLUDE_FROM_ALL) +add_subdirectory(third_party/fdk-aac) set(CAPSTONE_ARCHITECTURE_DEFAULT OFF) set(CAPSTONE_ARM_SUPPORT ON) @@ -478,7 +479,7 @@ set(ALL_SOURCES ${SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERN ${AUDIO_SOURCE_FILES} ${HEADER_FILES} ${FRONTEND_HEADER_FILES}) target_sources(AlberCore PRIVATE ${ALL_SOURCES}) -target_link_libraries(AlberCore PRIVATE dynarmic cryptopp glad resources_console_fonts teakra) +target_link_libraries(AlberCore PRIVATE dynarmic cryptopp glad resources_console_fonts teakra fdk-aac) target_link_libraries(AlberCore PUBLIC glad capstone) if(ENABLE_DISCORD_RPC AND NOT ANDROID) diff --git a/include/audio/aac.hpp b/include/audio/aac.hpp index afd2dbba..e59a006c 100644 --- a/include/audio/aac.hpp +++ b/include/audio/aac.hpp @@ -54,6 +54,13 @@ namespace Audio::AAC { u32_le sampleCount; }; + struct DecodeRequest { + u32_le address; // Address of input AAC stream + u32_le size; // Size of input AAC stream + u32_le destAddrLeft; // Output address for left channel samples + u32_le destAddrRight; // Output address for right channel samples + }; + struct Message { u16_le mode = Mode::None; // Encode or decode AAC? u16_le command = Command::Init; @@ -62,7 +69,9 @@ namespace Audio::AAC { // Info on the AAC request union { std::array commandData{}; + DecodeResponse decodeResponse; + DecodeRequest decodeRequest; }; }; diff --git a/third_party/fdk-aac b/third_party/fdk-aac new file mode 160000 index 00000000..716f4394 --- /dev/null +++ b/third_party/fdk-aac @@ -0,0 +1 @@ +Subproject commit 716f4394641d53f0d79c9ddac3fa93b03a49f278 From 545bbd5c45977e5d71acf76f7d792c5c1fad5e53 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 5 Sep 2024 00:06:48 +0300 Subject: [PATCH 202/251] HLE DSP: Implement AAC audio decoder --- CMakeLists.txt | 4 +- include/audio/aac_decoder.hpp | 24 ++++++ include/audio/hle_core.hpp | 8 +- src/core/audio/aac_decoder.cpp | 139 +++++++++++++++++++++++++++++++++ src/core/audio/hle_core.cpp | 8 +- 5 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 include/audio/aac_decoder.hpp create mode 100644 src/core/audio/aac_decoder.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d1a67b79..8407eccd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -276,7 +276,7 @@ set(APPLET_SOURCE_FILES src/core/applets/applet.cpp src/core/applets/mii_selecto src/core/applets/error_applet.cpp ) set(AUDIO_SOURCE_FILES src/core/audio/dsp_core.cpp src/core/audio/null_core.cpp src/core/audio/teakra_core.cpp - src/core/audio/miniaudio_device.cpp src/core/audio/hle_core.cpp + src/core/audio/miniaudio_device.cpp src/core/audio/hle_core.cpp src/core/audio/aac_decoder.cpp ) set(RENDERER_SW_SOURCE_FILES src/core/renderer_sw/renderer_sw.cpp) @@ -315,7 +315,7 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp include/PICA/pica_frag_config.hpp include/PICA/pica_frag_uniforms.hpp include/PICA/shader_gen_types.hpp include/PICA/shader_decompiler.hpp - include/sdl_sensors.hpp include/renderdoc.hpp + include/sdl_sensors.hpp include/renderdoc.hpp include/audio/aac_decoder.hpp ) cmrc_add_resource_library( diff --git a/include/audio/aac_decoder.hpp b/include/audio/aac_decoder.hpp new file mode 100644 index 00000000..6538bce9 --- /dev/null +++ b/include/audio/aac_decoder.hpp @@ -0,0 +1,24 @@ +#pragma once +#include + +#include "audio/aac.hpp" +#include "helpers.hpp" + +struct AAC_DECODER_INSTANCE; + +namespace Audio::AAC { + class Decoder { + using DecoderHandle = AAC_DECODER_INSTANCE*; + using PaddrCallback = std::function; + + DecoderHandle decoderHandle = nullptr; + + bool isInitialized() { return decoderHandle != nullptr; } + void initialize(); + + public: + // Decode function. Takes in a reference to the AAC response & request, and a callback for paddr -> pointer conversions + void decode(AAC::Message& response, const AAC::Message& request, PaddrCallback paddrCallback); + ~Decoder(); + }; +} // namespace Audio::AAC \ No newline at end of file diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index c0e0896f..c36f0500 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -2,10 +2,12 @@ #include #include #include +#include #include #include #include "audio/aac.hpp" +#include "audio/aac_decoder.hpp" #include "audio/dsp_core.hpp" #include "audio/dsp_shared_mem.hpp" #include "memory.hpp" @@ -33,8 +35,8 @@ namespace Audio { SampleFormat format; SourceType sourceType; - bool fromQueue = false; // Is this buffer from the buffer queue or an embedded buffer? - bool hasPlayedOnce = false; // Has the buffer been played at least once before? + bool fromQueue = false; // Is this buffer from the buffer queue or an embedded buffer? + bool hasPlayedOnce = false; // Has the buffer been played at least once before? bool operator<(const Buffer& other) const { // Lower ID = Higher priority @@ -129,6 +131,8 @@ namespace Audio { std::array sources; // DSP voices Audio::HLE::DspMemory dspRam; + std::unique_ptr aacDecoder; + void resetAudioPipe(); bool loaded = false; // Have we loaded a component? diff --git a/src/core/audio/aac_decoder.cpp b/src/core/audio/aac_decoder.cpp new file mode 100644 index 00000000..281539d8 --- /dev/null +++ b/src/core/audio/aac_decoder.cpp @@ -0,0 +1,139 @@ +#include "audio/aac_decoder.hpp" + +#include + +#include +using namespace Audio; + +void AAC::Decoder::decode(AAC::Message& response, const AAC::Message& request, AAC::Decoder::PaddrCallback paddrCallback) { + // Copy the command and mode fields of the request to the response + response.command = request.command; + response.mode = request.mode; + response.decodeResponse.size = request.decodeRequest.size; + + // Write a dummy response at first. We'll be overwriting it later if decoding goes well + response.resultCode = AAC::ResultCode::Success; + response.decodeResponse.channelCount = 2; + response.decodeResponse.sampleCount = 1024; + response.decodeResponse.sampleRate = AAC::SampleRate::Rate48000; + + if (!isInitialized()) { + initialize(); + + // AAC decoder failed to initialize, return dummy data and return without decoding + if (!isInitialized()) { + Helpers::warn("Failed to initialize AAC decoder"); + return; + } + } + + u8* input = paddrCallback(request.decodeRequest.address); + const u8* inputEnd = paddrCallback(request.decodeRequest.address + request.decodeRequest.size); + u8* outputLeft = paddrCallback(request.decodeRequest.destAddrLeft); + u8* outputRight = nullptr; + + if (input == nullptr || inputEnd == nullptr || outputLeft == nullptr) { + Helpers::warn("Invalid pointers passed to AAC decoder"); + return; + } + + u32 bytesValid = request.decodeRequest.size; + u32 bufferSize = request.decodeRequest.size; + + // Each frame is 2048 samples with 2 channels + static constexpr usize frameSize = 2048 * 2; + std::array frame; + std::array, 2> audioStreams; + + bool queriedStreamInfo = false; + + while (bytesValid != 0) { + if (aacDecoder_Fill(decoderHandle, &input, &bufferSize, &bytesValid) != AAC_DEC_OK) { + Helpers::warn("Failed to fill AAC decoder with samples"); + return; + } + + auto decodeResult = aacDecoder_DecodeFrame(decoderHandle, frame.data(), frameSize, 0); + + if (decodeResult == AAC_DEC_TRANSPORT_SYNC_ERROR) { + // https://android.googlesource.com/platform/external/aac/+/2ddc922/libAACdec/include/aacdecoder_lib.h#362 + // According to the above, if we get a sync error, we're not meant to stop decoding, but rather just continue feeding data + } else if (decodeResult == AAC_DEC_OK) { + auto getSampleRate = [](u32 rate) { + switch (rate) { + case 8000: return AAC::SampleRate::Rate8000; + case 11025: return AAC::SampleRate::Rate11025; + case 12000: return AAC::SampleRate::Rate12000; + case 16000: return AAC::SampleRate::Rate16000; + case 22050: return AAC::SampleRate::Rate22050; + case 24000: return AAC::SampleRate::Rate24000; + case 32000: return AAC::SampleRate::Rate32000; + case 44100: return AAC::SampleRate::Rate44100; + case 48000: + default: return AAC::SampleRate::Rate48000; + } + }; + + auto info = aacDecoder_GetStreamInfo(decoderHandle); + response.decodeResponse.sampleCount = info->frameSize; + response.decodeResponse.channelCount = info->numChannels; + response.decodeResponse.sampleRate = getSampleRate(info->sampleRate); + + int channels = info->numChannels; + // Reserve space in our output stream vectors so push_back doesn't do allocations + for (int i = 0; i < channels; i++) { + audioStreams[i].reserve(audioStreams[i].size() + info->frameSize); + } + + // Fetch output pointer for right output channel if we've got > 1 channel + if (channels > 1 && outputRight == nullptr) { + outputRight = paddrCallback(request.decodeRequest.destAddrRight); + // If the right output channel doesn't point to a proper padddr, return + if (outputRight == nullptr) { + Helpers::warn("Right AAC output channel doesn't point to valid physical address"); + return; + } + } + + for (int sample = 0; sample < info->frameSize; sample++) { + for (int stream = 0; stream < channels; stream++) { + audioStreams[stream].push_back(frame[(sample * channels) + stream]); + } + } + } else { + Helpers::warn("Failed to decode AAC frame"); + return; + } + } + + for (int i = 0; i < 2; i++) { + auto& stream = audioStreams[i]; + u8* pointer = (i == 0) ? outputLeft : outputRight; + + if (!stream.empty() && pointer != nullptr) { + std::memcpy(pointer, stream.data(), stream.size() * sizeof(s16)); + } + } +} + +void AAC::Decoder::initialize() { + decoderHandle = aacDecoder_Open(TRANSPORT_TYPE::TT_MP4_ADTS, 1); + + if (decoderHandle == nullptr) [[unlikely]] { + return; + } + + // Cap output channel count to 2 + if (aacDecoder_SetParam(decoderHandle, AAC_PCM_MAX_OUTPUT_CHANNELS, 2) != AAC_DEC_OK) [[unlikely]] { + aacDecoder_Close(decoderHandle); + decoderHandle = nullptr; + return; + } +} + +AAC::Decoder::~Decoder() { + if (isInitialized()) { + aacDecoder_Close(decoderHandle); + decoderHandle = nullptr; + } +} \ No newline at end of file diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index 83271a43..b4f9ab02 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -6,6 +6,7 @@ #include #include +#include "audio/aac_decoder.hpp" #include "services/dsp.hpp" namespace Audio { @@ -23,6 +24,8 @@ namespace Audio { for (int i = 0; i < sources.size(); i++) { sources[i].index = i; } + + aacDecoder.reset(new Audio::AAC::Decoder()); } void HLE_DSP::resetAudioPipe() { @@ -584,7 +587,6 @@ namespace Audio { switch (request.command) { case AAC::Command::EncodeDecode: // Dummy response to stop games from hanging - // TODO: Fix this when implementing AAC response.resultCode = AAC::ResultCode::Success; response.decodeResponse.channelCount = 2; response.decodeResponse.sampleCount = 1024; @@ -593,6 +595,10 @@ namespace Audio { response.command = request.command; response.mode = request.mode; + + // We've already got an AAC decoder but it's currently disabled until mixing & output is properly implemented + // TODO: Uncomment this when the time comes + // aacDecoder->decode(response, request, [this](u32 paddr) { return getPointerPhys(paddr); }); break; case AAC::Command::Init: From 4b21d601e2b71a564cf09475bd8a57c8afa744d0 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 5 Sep 2024 00:35:31 +0300 Subject: [PATCH 203/251] Vendor fdk-aac --- .gitmodules | 2 +- third_party/fdk-aac | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 981b4426..97bc129c 100644 --- a/.gitmodules +++ b/.gitmodules @@ -78,4 +78,4 @@ url = https://github.com/Panda3DS-emu/metal-cpp [submodule "third_party/fdk-aac"] path = third_party/fdk-aac - url = https://github.com/mstorsjo/fdk-aac/ + url = https://github.com/Panda3DS-emu/fdk-aac/ diff --git a/third_party/fdk-aac b/third_party/fdk-aac index 716f4394..5559136b 160000 --- a/third_party/fdk-aac +++ b/third_party/fdk-aac @@ -1 +1 @@ -Subproject commit 716f4394641d53f0d79c9ddac3fa93b03a49f278 +Subproject commit 5559136bb53ce38f6f07dac4f47674dd4f032d03 From 656b7da5e43c2b2110657f9354339f7574beafec Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Thu, 5 Sep 2024 01:55:44 +0300 Subject: [PATCH 204/251] Properly pad AAC request struct --- include/audio/aac.hpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/include/audio/aac.hpp b/include/audio/aac.hpp index e59a006c..389ecc04 100644 --- a/include/audio/aac.hpp +++ b/include/audio/aac.hpp @@ -59,6 +59,8 @@ namespace Audio::AAC { u32_le size; // Size of input AAC stream u32_le destAddrLeft; // Output address for left channel samples u32_le destAddrRight; // Output address for right channel samples + u32_le unknown1; + u32_le unknown2; }; struct Message { From f1b7830952e98299a62d325333cbe83b7bf81e83 Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Fri, 6 Sep 2024 00:43:41 +0300 Subject: [PATCH 205/251] Libretro: Complete code formatting (#594) * Libretro: Optimize range settings, fix default values * Libretro: More code formatting * Libretro: Fix loading of archived roms --- src/libretro_core.cpp | 165 +++++++++++++++++++++++------------------- 1 file changed, 90 insertions(+), 75 deletions(-) diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index cd0e9747..3f92cddd 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -8,13 +8,13 @@ #include #include -static retro_environment_t envCallbacks; -static retro_video_refresh_t videoCallbacks; +static retro_environment_t envCallback; +static retro_video_refresh_t videoCallback; static retro_audio_sample_batch_t audioBatchCallback; static retro_input_poll_t inputPollCallback; static retro_input_state_t inputStateCallback; -static retro_hw_render_callback hw_render; +static retro_hw_render_callback hwRender; static std::filesystem::path savePath; static bool screenTouched; @@ -30,17 +30,17 @@ std::filesystem::path Emulator::getAppDataRoot() { return std::filesystem::path(savePath / "Emulator Files"); } -static void* GetGLProcAddress(const char* name) { - return (void*)hw_render.get_proc_address(name); +static void* getGLProcAddress(const char* name) { + return (void*)hwRender.get_proc_address(name); } -static void VideoResetContext() { +static void videoResetContext() { #ifdef USING_GLES - if (!gladLoadGLES2Loader(reinterpret_cast(GetGLProcAddress))) { + if (!gladLoadGLES2Loader(reinterpret_cast(getGLProcAddress))) { Helpers::panic("OpenGL ES init failed"); } #else - if (!gladLoadGLLoader(reinterpret_cast(GetGLProcAddress))) { + if (!gladLoadGLLoader(reinterpret_cast(getGLProcAddress))) { Helpers::panic("OpenGL init failed"); } #endif @@ -48,31 +48,31 @@ static void VideoResetContext() { emulator->initGraphicsContext(nullptr); } -static void VideoDestroyContext() { - emulator->deinitGraphicsContext(); +static void videoDestroyContext() { + emulator->deinitGraphicsContext(); } -static bool SetHWRender(retro_hw_context_type type) { - hw_render.context_type = type; - hw_render.context_reset = VideoResetContext; - hw_render.context_destroy = VideoDestroyContext; - hw_render.bottom_left_origin = true; +static bool setHWRender(retro_hw_context_type type) { + hwRender.context_type = type; + hwRender.context_reset = videoResetContext; + hwRender.context_destroy = videoDestroyContext; + hwRender.bottom_left_origin = true; switch (type) { case RETRO_HW_CONTEXT_OPENGL_CORE: - hw_render.version_major = 4; - hw_render.version_minor = 1; + hwRender.version_major = 4; + hwRender.version_minor = 1; - if (envCallbacks(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render)) { + if (envCallback(RETRO_ENVIRONMENT_SET_HW_RENDER, &hwRender)) { return true; } break; case RETRO_HW_CONTEXT_OPENGLES3: case RETRO_HW_CONTEXT_OPENGL: - hw_render.version_major = 3; - hw_render.version_minor = 1; + hwRender.version_major = 3; + hwRender.version_minor = 1; - if (envCallbacks(RETRO_ENVIRONMENT_SET_HW_RENDER, &hw_render)) { + if (envCallback(RETRO_ENVIRONMENT_SET_HW_RENDER, &hwRender)) { return true; } break; @@ -84,18 +84,18 @@ static bool SetHWRender(retro_hw_context_type type) { static void videoInit() { retro_hw_context_type preferred = RETRO_HW_CONTEXT_NONE; - envCallbacks(RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER, &preferred); + envCallback(RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER, &preferred); - if (preferred && SetHWRender(preferred)) return; - if (SetHWRender(RETRO_HW_CONTEXT_OPENGL_CORE)) return; - if (SetHWRender(RETRO_HW_CONTEXT_OPENGL)) return; - if (SetHWRender(RETRO_HW_CONTEXT_OPENGLES3)) return; + if (preferred && setHWRender(preferred)) return; + if (setHWRender(RETRO_HW_CONTEXT_OPENGL_CORE)) return; + if (setHWRender(RETRO_HW_CONTEXT_OPENGL)) return; + if (setHWRender(RETRO_HW_CONTEXT_OPENGLES3)) return; - hw_render.context_type = RETRO_HW_CONTEXT_NONE; + hwRender.context_type = RETRO_HW_CONTEXT_NONE; } -static bool GetButtonState(uint id) { return inputStateCallback(0, RETRO_DEVICE_JOYPAD, 0, id); } -static float GetAxisState(uint index, uint id) { return inputStateCallback(0, RETRO_DEVICE_ANALOG, index, id); } +static bool getButtonState(uint id) { return inputStateCallback(0, RETRO_DEVICE_JOYPAD, 0, id); } +static float getAxisState(uint index, uint id) { return inputStateCallback(0, RETRO_DEVICE_ANALOG, index, id); } static void inputInit() { static const retro_controller_description controllers[] = { @@ -108,7 +108,7 @@ static void inputInit() { {NULL, 0}, }; - envCallbacks(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports); + envCallback(RETRO_ENVIRONMENT_SET_CONTROLLER_INFO, (void*)ports); retro_input_descriptor desc[] = { {0, RETRO_DEVICE_JOYPAD, 0, RETRO_DEVICE_ID_JOYPAD_LEFT, "Left"}, @@ -128,14 +128,14 @@ static void inputInit() { {0}, }; - envCallbacks(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, &desc); + envCallback(RETRO_ENVIRONMENT_SET_INPUT_DESCRIPTORS, &desc); } -static std::string FetchVariable(std::string key, std::string def) { +static std::string fetchVariable(std::string key, std::string def) { retro_variable var = {nullptr}; var.key = key.c_str(); - if (!envCallbacks(RETRO_ENVIRONMENT_GET_VARIABLE, &var) || var.value == nullptr) { + if (!envCallback(RETRO_ENVIRONMENT_GET_VARIABLE, &var) || var.value == nullptr) { Helpers::warn("Fetching variable %s failed.", key.c_str()); return def; } @@ -143,13 +143,28 @@ static std::string FetchVariable(std::string key, std::string def) { return std::string(var.value); } -static bool FetchVariableBool(std::string key, bool def) { - return FetchVariable(key, def ? "enabled" : "disabled") == "enabled"; +static int fetchVariableInt(std::string key, int def) { + std::string value = fetchVariable(key, std::to_string(def)); + + if (!value.empty() && std::isdigit(value[0])) { + return std::stoi(value); + } + + return 0; +} + +static bool fetchVariableBool(std::string key, bool def) { + return fetchVariable(key, def ? "enabled" : "disabled") == "enabled"; +} + +static int fetchVariableRange(std::string key, int min, int max) { + return std::clamp(fetchVariableInt(key, min), min, max); } static void configInit() { static const retro_variable values[] = { - {"panda3ds_use_shader_jit", "Enable shader JIT; enabled|disabled"}, + {"panda3ds_use_shader_jit", EmulatorConfig::shaderJitDefault ? "Enable shader JIT; enabled|disabled" + : "Enable shader JIT; disabled|enabled"}, {"panda3ds_accurate_shader_mul", "Enable accurate shader multiplication; disabled|enabled"}, {"panda3ds_use_ubershader", EmulatorConfig::ubershaderDefault ? "Use ubershaders (No stutter, maybe slower); enabled|disabled" : "Use ubershaders (No stutter, maybe slower); disabled|enabled"}, @@ -165,33 +180,33 @@ static void configInit() { {nullptr, nullptr}, }; - envCallbacks(RETRO_ENVIRONMENT_SET_VARIABLES, (void*)values); + envCallback(RETRO_ENVIRONMENT_SET_VARIABLES, (void*)values); } static void configUpdate() { EmulatorConfig& config = emulator->getConfig(); config.rendererType = RendererType::OpenGL; - config.vsyncEnabled = FetchVariableBool("panda3ds_use_vsync", true); - config.shaderJitEnabled = FetchVariableBool("panda3ds_use_shader_jit", true); - config.chargerPlugged = FetchVariableBool("panda3ds_use_charger", true); - config.batteryPercentage = std::clamp(std::stoi(FetchVariable("panda3ds_battery_level", "5")), 0, 100); - config.dspType = Audio::DSPCore::typeFromString(FetchVariable("panda3ds_dsp_emulation", "null")); - config.audioEnabled = FetchVariableBool("panda3ds_use_audio", false); - config.sdCardInserted = FetchVariableBool("panda3ds_use_virtual_sd", true); - config.sdWriteProtected = FetchVariableBool("panda3ds_write_protect_virtual_sd", false); - config.accurateShaderMul = FetchVariableBool("panda3ds_accurate_shader_mul", false); - config.useUbershaders = FetchVariableBool("panda3ds_use_ubershader", true); - config.forceShadergenForLights = FetchVariableBool("panda3ds_ubershader_lighting_override", true); - config.lightShadergenThreshold = std::clamp(std::stoi(FetchVariable("panda3ds_ubershader_lighting_override_threshold", "1")), 1, 8); + config.vsyncEnabled = fetchVariableBool("panda3ds_use_vsync", true); + config.shaderJitEnabled = fetchVariableBool("panda3ds_use_shader_jit", EmulatorConfig::shaderJitDefault); + config.chargerPlugged = fetchVariableBool("panda3ds_use_charger", true); + config.batteryPercentage = fetchVariableRange("panda3ds_battery_level", 5, 100); + config.dspType = Audio::DSPCore::typeFromString(fetchVariable("panda3ds_dsp_emulation", "null")); + config.audioEnabled = fetchVariableBool("panda3ds_use_audio", false); + config.sdCardInserted = fetchVariableBool("panda3ds_use_virtual_sd", true); + config.sdWriteProtected = fetchVariableBool("panda3ds_write_protect_virtual_sd", false); + config.accurateShaderMul = fetchVariableBool("panda3ds_accurate_shader_mul", false); + config.useUbershaders = fetchVariableBool("panda3ds_use_ubershader", EmulatorConfig::ubershaderDefault); + config.forceShadergenForLights = fetchVariableBool("panda3ds_ubershader_lighting_override", true); + config.lightShadergenThreshold = fetchVariableRange("panda3ds_ubershader_lighting_override_threshold", 1, 8); config.discordRpcEnabled = false; config.save(); } -static void ConfigCheckVariables() { +static void configCheckVariables() { bool updated = false; - envCallbacks(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated); + envCallback(RETRO_ENVIRONMENT_GET_VARIABLE_UPDATE, &updated); if (updated) { configUpdate(); @@ -203,7 +218,7 @@ void retro_get_system_info(retro_system_info* info) { info->valid_extensions = "3ds|3dsx|elf|axf|cci|cxi|app"; info->library_version = PANDA3DS_VERSION; info->library_name = "Panda3DS"; - info->block_extract = true; + info->block_extract = false; } void retro_get_system_av_info(retro_system_av_info* info) { @@ -219,11 +234,11 @@ void retro_get_system_av_info(retro_system_av_info* info) { } void retro_set_environment(retro_environment_t cb) { - envCallbacks = cb; + envCallback = cb; } void retro_set_video_refresh(retro_video_refresh_t cb) { - videoCallbacks = cb; + videoCallback = cb; } void retro_set_audio_sample_batch(retro_audio_sample_batch_t cb) { @@ -242,15 +257,15 @@ void retro_set_input_state(retro_input_state_t cb) { void retro_init() { enum retro_pixel_format xrgb888 = RETRO_PIXEL_FORMAT_XRGB8888; - envCallbacks(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &xrgb888); + envCallback(RETRO_ENVIRONMENT_SET_PIXEL_FORMAT, &xrgb888); - char* save_dir = nullptr; + char* saveDir = nullptr; - if (!envCallbacks(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &save_dir) || save_dir == nullptr) { + if (!envCallback(RETRO_ENVIRONMENT_GET_SAVE_DIRECTORY, &saveDir) || saveDir == nullptr) { Helpers::warn("No save directory provided by LibRetro."); savePath = std::filesystem::current_path(); } else { - savePath = std::filesystem::path(save_dir); + savePath = std::filesystem::path(saveDir); } emulator = std::make_unique(); @@ -289,31 +304,31 @@ void retro_reset() { } void retro_run() { - ConfigCheckVariables(); + configCheckVariables(); - renderer->setFBO(hw_render.get_current_framebuffer()); + renderer->setFBO(hwRender.get_current_framebuffer()); renderer->resetStateManager(); inputPollCallback(); HIDService& hid = emulator->getServiceManager().getHID(); - hid.setKey(HID::Keys::A, GetButtonState(RETRO_DEVICE_ID_JOYPAD_A)); - hid.setKey(HID::Keys::B, GetButtonState(RETRO_DEVICE_ID_JOYPAD_B)); - hid.setKey(HID::Keys::X, GetButtonState(RETRO_DEVICE_ID_JOYPAD_X)); - hid.setKey(HID::Keys::Y, GetButtonState(RETRO_DEVICE_ID_JOYPAD_Y)); - hid.setKey(HID::Keys::L, GetButtonState(RETRO_DEVICE_ID_JOYPAD_L)); - hid.setKey(HID::Keys::R, GetButtonState(RETRO_DEVICE_ID_JOYPAD_R)); - hid.setKey(HID::Keys::Start, GetButtonState(RETRO_DEVICE_ID_JOYPAD_START)); - hid.setKey(HID::Keys::Select, GetButtonState(RETRO_DEVICE_ID_JOYPAD_SELECT)); - hid.setKey(HID::Keys::Up, GetButtonState(RETRO_DEVICE_ID_JOYPAD_UP)); - hid.setKey(HID::Keys::Down, GetButtonState(RETRO_DEVICE_ID_JOYPAD_DOWN)); - hid.setKey(HID::Keys::Left, GetButtonState(RETRO_DEVICE_ID_JOYPAD_LEFT)); - hid.setKey(HID::Keys::Right, GetButtonState(RETRO_DEVICE_ID_JOYPAD_RIGHT)); + hid.setKey(HID::Keys::A, getButtonState(RETRO_DEVICE_ID_JOYPAD_A)); + hid.setKey(HID::Keys::B, getButtonState(RETRO_DEVICE_ID_JOYPAD_B)); + hid.setKey(HID::Keys::X, getButtonState(RETRO_DEVICE_ID_JOYPAD_X)); + hid.setKey(HID::Keys::Y, getButtonState(RETRO_DEVICE_ID_JOYPAD_Y)); + hid.setKey(HID::Keys::L, getButtonState(RETRO_DEVICE_ID_JOYPAD_L)); + hid.setKey(HID::Keys::R, getButtonState(RETRO_DEVICE_ID_JOYPAD_R)); + hid.setKey(HID::Keys::Start, getButtonState(RETRO_DEVICE_ID_JOYPAD_START)); + hid.setKey(HID::Keys::Select, getButtonState(RETRO_DEVICE_ID_JOYPAD_SELECT)); + hid.setKey(HID::Keys::Up, getButtonState(RETRO_DEVICE_ID_JOYPAD_UP)); + hid.setKey(HID::Keys::Down, getButtonState(RETRO_DEVICE_ID_JOYPAD_DOWN)); + hid.setKey(HID::Keys::Left, getButtonState(RETRO_DEVICE_ID_JOYPAD_LEFT)); + hid.setKey(HID::Keys::Right, getButtonState(RETRO_DEVICE_ID_JOYPAD_RIGHT)); // Get analog values for the left analog stick (Right analog stick is N3DS-only and unimplemented) - float xLeft = GetAxisState(RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X); - float yLeft = GetAxisState(RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y); + float xLeft = getAxisState(RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_X); + float yLeft = getAxisState(RETRO_DEVICE_INDEX_ANALOG_LEFT, RETRO_DEVICE_ID_ANALOG_Y); hid.setCirclepadX((xLeft / +32767) * 0x9C); hid.setCirclepadY((yLeft / -32767) * 0x9C); @@ -351,7 +366,7 @@ void retro_run() { hid.updateInputs(emulator->getTicks()); emulator->runFrame(); - videoCallbacks(RETRO_HW_FRAME_BUFFER_VALID, emulator->width, emulator->height, 0); + videoCallback(RETRO_HW_FRAME_BUFFER_VALID, emulator->width, emulator->height, 0); } void retro_set_controller_port_device(uint port, uint device) {} From c12c3bce8d9c660ba09443cb7df6d1ab8178e4a7 Mon Sep 17 00:00:00 2001 From: Jonian Guveli Date: Sat, 7 Sep 2024 16:20:41 +0300 Subject: [PATCH 206/251] Prevent app crash when miniaudio samples bigger than capacity (#596) * Prevent app crash when miniaudio samples bigger than capacity * Update miniaudio_device.cpp --------- Co-authored-by: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> --- src/core/audio/miniaudio_device.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/audio/miniaudio_device.cpp b/src/core/audio/miniaudio_device.cpp index fa36cb84..dd5cfa85 100644 --- a/src/core/audio/miniaudio_device.cpp +++ b/src/core/audio/miniaudio_device.cpp @@ -90,16 +90,17 @@ void MiniAudioDevice::init(Samples& samples, bool safe) { deviceConfig.dataCallback = [](ma_device* device, void* out, const void* input, ma_uint32 frameCount) { auto self = reinterpret_cast(device->pUserData); s16* output = reinterpret_cast(out); + const usize maxSamples = std::min(self->samples->Capacity(), usize(frameCount * channelCount)); // Wait until there's enough samples to pop - while (self->samples->size() < frameCount * channelCount) { + while (self->samples->size() < maxSamples) { // If audio output is disabled from the emulator thread, make sure that this callback will return and not hang if (!self->running) { return; } } - self->samples->pop(output, frameCount * channelCount); + self->samples->pop(output, maxSamples); }; if (ma_device_init(&context, &deviceConfig, &device) != MA_SUCCESS) { From c0c8545dc204d9edffc8d8427d35613d28ea36fe Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:39:05 +0300 Subject: [PATCH 207/251] Qt: Fix Linguist issues and format files --- include/panda_qt/cheats_window.hpp | 64 +++++++++++++++++++++++ include/panda_qt/main_window.hpp | 2 +- src/panda_qt/about_window.cpp | 4 +- src/panda_qt/cheats_window.cpp | 84 +++++------------------------- src/panda_qt/mappings.cpp | 4 +- src/panda_qt/shader_editor.cpp | 3 +- 6 files changed, 84 insertions(+), 77 deletions(-) diff --git a/include/panda_qt/cheats_window.hpp b/include/panda_qt/cheats_window.hpp index c82b2bd8..93228d5e 100644 --- a/include/panda_qt/cheats_window.hpp +++ b/include/panda_qt/cheats_window.hpp @@ -1,6 +1,13 @@ #pragma once #include +#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -24,3 +31,60 @@ class CheatsWindow final : public QWidget { std::filesystem::path cheatPath; Emulator* emu; }; + +struct CheatMetadata { + u32 handle = Cheats::badCheatHandle; + std::string name = "New cheat"; + std::string code; + bool enabled = true; +}; + +class CheatEntryWidget : public QWidget { + Q_OBJECT + + public: + CheatEntryWidget(Emulator* emu, CheatMetadata metadata, QListWidget* parent); + + void Update() { + name->setText(metadata.name.c_str()); + enabled->setChecked(metadata.enabled); + update(); + } + + void Remove() { + emu->getCheats().removeCheat(metadata.handle); + cheatList->takeItem(cheatList->row(listItem)); + deleteLater(); + } + + const CheatMetadata& getMetadata() { return metadata; } + void setMetadata(const CheatMetadata& metadata) { this->metadata = metadata; } + + private: + void checkboxChanged(int state); + void editClicked(); + + Emulator* emu; + CheatMetadata metadata; + u32 handle; + QLabel* name; + QCheckBox* enabled; + QListWidget* cheatList; + QListWidgetItem* listItem; +}; + +class CheatEditDialog : public QDialog { + Q_OBJECT + + public: + CheatEditDialog(Emulator* emu, CheatEntryWidget& cheatEntry); + + void accepted(); + void rejected(); + + private: + Emulator* emu; + CheatEntryWidget& cheatEntry; + QTextEdit* codeEdit; + QLineEdit* nameEdit; +}; \ No newline at end of file diff --git a/include/panda_qt/main_window.hpp b/include/panda_qt/main_window.hpp index 3ff16a1d..fff99d20 100644 --- a/include/panda_qt/main_window.hpp +++ b/include/panda_qt/main_window.hpp @@ -140,7 +140,7 @@ class MainWindow : public QMainWindow { MainWindow(QApplication* app, QWidget* parent = nullptr); ~MainWindow(); - void closeEvent(QCloseEvent *event) override; + void closeEvent(QCloseEvent* event) override; void keyPressEvent(QKeyEvent* event) override; void keyReleaseEvent(QKeyEvent* event) override; void mousePressEvent(QMouseEvent* event) override; diff --git a/src/panda_qt/about_window.cpp b/src/panda_qt/about_window.cpp index 60a91272..a388dad3 100644 --- a/src/panda_qt/about_window.cpp +++ b/src/panda_qt/about_window.cpp @@ -1,11 +1,13 @@ #include "panda_qt/about_window.hpp" -#include "version.hpp" +#include #include #include #include #include +#include "version.hpp" + // Based on https://github.com/dolphin-emu/dolphin/blob/master/Source/Core/DolphinQt/AboutDialog.cpp AboutWindow::AboutWindow(QWidget* parent) : QDialog(parent) { diff --git a/src/panda_qt/cheats_window.cpp b/src/panda_qt/cheats_window.cpp index dbd251cc..cc2c94f6 100644 --- a/src/panda_qt/cheats_window.cpp +++ b/src/panda_qt/cheats_window.cpp @@ -1,15 +1,9 @@ #include "panda_qt/cheats_window.hpp" -#include -#include #include -#include -#include -#include -#include -#include -#include +#include #include +#include #include #include "cheats.hpp" @@ -18,71 +12,17 @@ MainWindow* mainWindow = nullptr; -struct CheatMetadata { - u32 handle = Cheats::badCheatHandle; - std::string name = "New cheat"; - std::string code; - bool enabled = true; -}; - void dispatchToMainThread(std::function callback) { - QTimer* timer = new QTimer(); - timer->moveToThread(qApp->thread()); - timer->setSingleShot(true); - QObject::connect(timer, &QTimer::timeout, [=]() - { - callback(); - timer->deleteLater(); - }); - QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection, Q_ARG(int, 0)); + QTimer* timer = new QTimer(); + timer->moveToThread(qApp->thread()); + timer->setSingleShot(true); + QObject::connect(timer, &QTimer::timeout, [=]() { + callback(); + timer->deleteLater(); + }); + QMetaObject::invokeMethod(timer, "start", Qt::QueuedConnection, Q_ARG(int, 0)); } -class CheatEntryWidget : public QWidget { - public: - CheatEntryWidget(Emulator* emu, CheatMetadata metadata, QListWidget* parent); - - void Update() { - name->setText(metadata.name.c_str()); - enabled->setChecked(metadata.enabled); - update(); - } - - void Remove() { - emu->getCheats().removeCheat(metadata.handle); - cheatList->takeItem(cheatList->row(listItem)); - deleteLater(); - } - - const CheatMetadata& getMetadata() { return metadata; } - void setMetadata(const CheatMetadata& metadata) { this->metadata = metadata; } - - private: - void checkboxChanged(int state); - void editClicked(); - - Emulator* emu; - CheatMetadata metadata; - u32 handle; - QLabel* name; - QCheckBox* enabled; - QListWidget* cheatList; - QListWidgetItem* listItem; -}; - -class CheatEditDialog : public QDialog { - public: - CheatEditDialog(Emulator* emu, CheatEntryWidget& cheatEntry); - - void accepted(); - void rejected(); - - private: - Emulator* emu; - CheatEntryWidget& cheatEntry; - QTextEdit* codeEdit; - QLineEdit* nameEdit; -}; - CheatEntryWidget::CheatEntryWidget(Emulator* emu, CheatMetadata metadata, QListWidget* parent) : QWidget(), emu(emu), metadata(metadata), cheatList(parent) { QHBoxLayout* layout = new QHBoxLayout; @@ -219,7 +159,7 @@ void CheatEditDialog::rejected() { CheatsWindow::CheatsWindow(Emulator* emu, const std::filesystem::path& cheatPath, QWidget* parent) : QWidget(parent, Qt::Window), emu(emu), cheatPath(cheatPath) { - mainWindow = static_cast(parent); + mainWindow = static_cast(parent); QVBoxLayout* layout = new QVBoxLayout; layout->setContentsMargins(6, 6, 6, 6); @@ -265,4 +205,4 @@ void CheatsWindow::removeClicked() { CheatEntryWidget* entry = static_cast(cheatList->itemWidget(item)); entry->Remove(); -} +} \ No newline at end of file diff --git a/src/panda_qt/mappings.cpp b/src/panda_qt/mappings.cpp index 22741a73..d41b0a31 100644 --- a/src/panda_qt/mappings.cpp +++ b/src/panda_qt/mappings.cpp @@ -1,7 +1,7 @@ -#include "input_mappings.hpp" - #include +#include "input_mappings.hpp" + InputMappings InputMappings::defaultKeyboardMappings() { InputMappings mappings; mappings.setMapping(Qt::Key_L, HID::Keys::A); diff --git a/src/panda_qt/shader_editor.cpp b/src/panda_qt/shader_editor.cpp index 122d841f..4ca41e22 100644 --- a/src/panda_qt/shader_editor.cpp +++ b/src/panda_qt/shader_editor.cpp @@ -1,8 +1,9 @@ +#include "panda_qt/shader_editor.hpp" + #include #include #include "panda_qt/main_window.hpp" -#include "panda_qt/shader_editor.hpp" using namespace Zep; From 3567a4bc55ffa3ace89078f29e5abd07c980828e Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 16 Sep 2024 22:51:33 +0300 Subject: [PATCH 208/251] Update Github Actions checkout/upload-artifact versions --- .github/workflows/HTTP_Build.yml | 2 +- .github/workflows/Hydra_Build.yml | 8 ++++---- .github/workflows/Linux_AppImage_Build.yml | 4 ++-- .github/workflows/Linux_Build.yml | 4 ++-- .github/workflows/Qt_Build.yml | 8 ++++---- .github/workflows/Windows_Build.yml | 4 ++-- 6 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/HTTP_Build.yml b/.github/workflows/HTTP_Build.yml index 7bfe9c7f..0bdaa4f7 100644 --- a/.github/workflows/HTTP_Build.yml +++ b/.github/workflows/HTTP_Build.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Fetch submodules run: git submodule update --init --recursive diff --git a/.github/workflows/Hydra_Build.yml b/.github/workflows/Hydra_Build.yml index a269e839..e2c2004b 100644 --- a/.github/workflows/Hydra_Build.yml +++ b/.github/workflows/Hydra_Build.yml @@ -15,7 +15,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Fetch submodules run: git submodule update --init --recursive @@ -58,7 +58,7 @@ jobs: runs-on: macos-13 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Fetch submodules run: git submodule update --init --recursive @@ -101,7 +101,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Fetch submodules run: git submodule update --init --recursive @@ -154,7 +154,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Fetch submodules run: git submodule update --init --recursive diff --git a/.github/workflows/Linux_AppImage_Build.yml b/.github/workflows/Linux_AppImage_Build.yml index 7d198b9c..f32a7d38 100644 --- a/.github/workflows/Linux_AppImage_Build.yml +++ b/.github/workflows/Linux_AppImage_Build.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Fetch submodules run: git submodule update --init --recursive @@ -52,7 +52,7 @@ jobs: run: ./.github/linux-appimage.sh - name: Upload executable - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Linux executable path: './Alber-x86_64.AppImage' diff --git a/.github/workflows/Linux_Build.yml b/.github/workflows/Linux_Build.yml index 78e5cc5a..9cb05303 100644 --- a/.github/workflows/Linux_Build.yml +++ b/.github/workflows/Linux_Build.yml @@ -19,7 +19,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Fetch submodules run: git submodule update --init --recursive @@ -49,7 +49,7 @@ jobs: run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - name: Upload executable - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Linux executable path: './build/Alber' diff --git a/.github/workflows/Qt_Build.yml b/.github/workflows/Qt_Build.yml index 4485cc1c..d3a09866 100644 --- a/.github/workflows/Qt_Build.yml +++ b/.github/workflows/Qt_Build.yml @@ -15,7 +15,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Fetch submodules run: git submodule update --init --recursive @@ -45,7 +45,7 @@ jobs: windeployqt --dir upload upload/Alber.exe - name: Upload executable - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Windows executable path: upload @@ -99,7 +99,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Fetch submodules run: git submodule update --init --recursive @@ -135,7 +135,7 @@ jobs: ./.github/linux-appimage-qt.sh - name: Upload executable - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Linux executable path: './Alber-x86_64.AppImage' diff --git a/.github/workflows/Windows_Build.yml b/.github/workflows/Windows_Build.yml index a06889eb..5497c3ef 100644 --- a/.github/workflows/Windows_Build.yml +++ b/.github/workflows/Windows_Build.yml @@ -19,7 +19,7 @@ jobs: runs-on: windows-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Fetch submodules run: git submodule update --init --recursive @@ -40,7 +40,7 @@ jobs: run: cmake --build ${{github.workspace}}/build --config ${{env.BUILD_TYPE}} - name: Upload executable - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v4 with: name: Windows executable path: './build/${{ env.BUILD_TYPE }}/Alber.exe' From 683b4186d8b34d414bd5418721dd831cf63922c6 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 16 Sep 2024 23:36:42 +0300 Subject: [PATCH 209/251] Qt: Add CMake target for generating translations --- CMakeLists.txt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8407eccd..88a92f8a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -519,6 +519,9 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE) if(NOT ENABLE_OPENGL) message(FATAL_ERROR "Qt frontend requires OpenGL") endif() + + option(GENERATE_QT_TRANSLATION "Generate Qt translation file" OFF) + set(QT_LANGUAGES docs/translations) set(FRONTEND_SOURCE_FILES src/panda_qt/main.cpp src/panda_qt/screen.cpp src/panda_qt/main_window.cpp src/panda_qt/about_window.cpp src/panda_qt/config_window.cpp src/panda_qt/zep.cpp src/panda_qt/text_editor.cpp src/panda_qt/cheats_window.cpp src/panda_qt/mappings.cpp @@ -556,6 +559,17 @@ if(NOT BUILD_HYDRA_CORE AND NOT BUILD_LIBRETRO_CORE) endif() endif() + # Generates an en.ts file for translations + # To update the file, use cmake --build --target Alber_lupdate + if(GENERATE_QT_TRANSLATION) + find_package(Qt6 REQUIRED COMPONENTS LinguistTools) + qt_add_lupdate(Alber TS_FILES ${QT_LANGUAGES}/en.ts + SOURCES ${FRONTEND_SOURCE_FILES} + INCLUDE_DIRECTORIES ${FRONTEND_HEADER_FILES} + NO_GLOBAL_TARGET + ) + endif() + qt_add_resources(AlberCore "app_images" PREFIX "/" FILES From 90420160f2092b2c2cc4bf14b261480bc8849875 Mon Sep 17 00:00:00 2001 From: Samuliak Date: Tue, 24 Sep 2024 14:51:27 +0200 Subject: [PATCH 210/251] correct comment --- include/renderer_mtl/mtl_vertex_buffer_cache.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/renderer_mtl/mtl_vertex_buffer_cache.hpp b/include/renderer_mtl/mtl_vertex_buffer_cache.hpp index 8aa299da..cc552477 100644 --- a/include/renderer_mtl/mtl_vertex_buffer_cache.hpp +++ b/include/renderer_mtl/mtl_vertex_buffer_cache.hpp @@ -11,7 +11,7 @@ struct BufferHandle { size_t offset; }; -// 64MB buffer for caching vertex data +// 128MB buffer for caching vertex data #define CACHE_BUFFER_SIZE 128 * 1024 * 1024 class VertexBufferCache { From 1c8b7a61b07a40f8d0657101e1ec467e7c6fca69 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 28 Sep 2024 22:56:26 +0300 Subject: [PATCH 211/251] Lua: Expose read/write functions for floats and doubles --- src/lua.cpp | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/lua.cpp b/src/lua.cpp index 6a16ab5b..5b78cec2 100644 --- a/src/lua.cpp +++ b/src/lua.cpp @@ -130,6 +130,32 @@ MAKE_MEMORY_FUNCTIONS(32) MAKE_MEMORY_FUNCTIONS(64) #undef MAKE_MEMORY_FUNCTIONS +static int readFloatThunk(lua_State* L) { + const u32 vaddr = (u32)lua_tonumber(L, 1); + lua_pushnumber(L, (lua_Number)Helpers::bit_cast(LuaManager::g_emulator->getMemory().read32(vaddr))); + return 1; +} + +static int writeFloatThunk(lua_State* L) { + const u32 vaddr = (u32)lua_tonumber(L, 1); + const float value = (float)lua_tonumber(L, 2); + LuaManager::g_emulator->getMemory().write32(vaddr, Helpers::bit_cast(value)); + return 0; +} + +static int readDoubleThunk(lua_State* L) { + const u32 vaddr = (u32)lua_tonumber(L, 1); + lua_pushnumber(L, (lua_Number)Helpers::bit_cast(LuaManager::g_emulator->getMemory().read64(vaddr))); + return 1; +} + +static int writeDoubleThunk(lua_State* L) { + const u32 vaddr = (u32)lua_tonumber(L, 1); + const double value = (double)lua_tonumber(L, 2); + LuaManager::g_emulator->getMemory().write64(vaddr, Helpers::bit_cast(value)); + return 0; +} + static int getAppIDThunk(lua_State* L) { std::optional id = LuaManager::g_emulator->getMemory().getProgramID(); @@ -248,10 +274,14 @@ static constexpr luaL_Reg functions[] = { { "__read16", read16Thunk }, { "__read32", read32Thunk }, { "__read64", read64Thunk }, + { "__readFloat", readFloatThunk }, + { "__readDouble", readDoubleThunk }, { "__write8", write8Thunk} , { "__write16", write16Thunk }, { "__write32", write32Thunk }, { "__write64", write64Thunk }, + { "__writeFloat", writeFloatThunk }, + { "__writeDouble", writeDoubleThunk }, { "__getAppID", getAppIDThunk }, { "__pause", pauseThunk }, { "__resume", resumeThunk }, @@ -273,10 +303,15 @@ void LuaManager::initializeThunks() { read16 = function(addr) return GLOBALS.__read16(addr) end, read32 = function(addr) return GLOBALS.__read32(addr) end, read64 = function(addr) return GLOBALS.__read64(addr) end, + readFloat = function(addr) return GLOBALS.__readFloat(addr) end, + readDouble = function(addr) return GLOBALS.__readDouble(addr) end, + write8 = function(addr, value) GLOBALS.__write8(addr, value) end, write16 = function(addr, value) GLOBALS.__write16(addr, value) end, write32 = function(addr, value) GLOBALS.__write32(addr, value) end, write64 = function(addr, value) GLOBALS.__write64(addr, value) end, + writeFloat = function(addr, value) GLOBALS.__writeFloat(addr, value) end, + writeDouble = function(addr, value) GLOBALS.__writeDouble(addr, value) end, getAppID = function() local ffi = require("ffi") From 3e72323653cdfd31b0d1428f919847b5d20077fc Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 29 Sep 2024 00:32:51 +0300 Subject: [PATCH 212/251] HLE DSP: Initial mixer work --- include/audio/dsp_shared_mem.hpp | 8 +++--- include/audio/hle_core.hpp | 46 +++++++++++++++++++++++++++++-- src/core/audio/hle_core.cpp | 47 ++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+), 7 deletions(-) diff --git a/include/audio/dsp_shared_mem.hpp b/include/audio/dsp_shared_mem.hpp index e776211d..272edf7e 100644 --- a/include/audio/dsp_shared_mem.hpp +++ b/include/audio/dsp_shared_mem.hpp @@ -324,8 +324,8 @@ namespace Audio::HLE { BitField<15, 1, u32> outputBufferCountDirty; BitField<16, 1, u32> masterVolumeDirty; - BitField<24, 1, u32> auxReturnVolume0Dirty; - BitField<25, 1, u32> auxReturnVolume1Dirty; + BitField<24, 1, u32> auxVolume0Dirty; + BitField<25, 1, u32> auxVolume1Dirty; BitField<26, 1, u32> outputFormatDirty; BitField<27, 1, u32> clippingModeDirty; BitField<28, 1, u32> headphonesConnectedDirty; @@ -337,7 +337,7 @@ namespace Audio::HLE { /// The DSP has three intermediate audio mixers. This controls the volume level (0.0-1.0) for /// each at the final mixer. float_le masterVolume; - std::array auxReturnVolume; + std::array auxVolumes; u16_le outputBufferCount; u16 pad1[2]; @@ -422,7 +422,7 @@ namespace Audio::HLE { struct DspStatus { u16_le unknown; - u16_le dropped_frames; + u16_le droppedFrames; u16 pad0[0xE]; }; ASSERT_DSP_STRUCT(DspStatus, 32); diff --git a/include/audio/hle_core.hpp b/include/audio/hle_core.hpp index c36f0500..bd717237 100644 --- a/include/audio/hle_core.hpp +++ b/include/audio/hle_core.hpp @@ -95,8 +95,7 @@ namespace Audio { DSPSource() { reset(); } }; - class HLE_DSP : public DSPCore { - // The audio frame types are public in case we want to use them for unit tests + class DSPMixer { public: template using Sample = std::array; @@ -113,6 +112,43 @@ namespace Audio { template using QuadFrame = Frame; + private: + using ChannelFormat = HLE::DspConfiguration::OutputFormat; + // The audio from each DSP voice is converted to quadraphonic and then fed into 3 intermediate mixing stages + // Two of these intermediate mixers (second and third) are used for effects, including custom effects done on the CPU + static constexpr usize mixerStageCount = 3; + + public: + ChannelFormat channelFormat = ChannelFormat::Stereo; + std::array volumes; + std::array enableAuxStages; + + void reset() { + channelFormat = ChannelFormat::Stereo; + + volumes.fill(0.0); + enableAuxStages.fill(false); + } + }; + + class HLE_DSP : public DSPCore { + // The audio frame types are public in case we want to use them for unit tests + public: + template + using Sample = DSPMixer::Sample; + + template + using Frame = DSPMixer::Frame; + + template + using MonoFrame = DSPMixer::MonoFrame; + + template + using StereoFrame = DSPMixer::StereoFrame; + + template + using QuadFrame = DSPMixer::QuadFrame; + using Source = Audio::DSPSource; using SampleBuffer = Source::SampleBuffer; @@ -131,6 +167,7 @@ namespace Audio { std::array sources; // DSP voices Audio::HLE::DspMemory dspRam; + Audio::DSPMixer mixer; std::unique_ptr aacDecoder; void resetAudioPipe(); @@ -175,10 +212,13 @@ namespace Audio { void handleAACRequest(const AAC::Message& request); void updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config, s16_le* adpcmCoefficients); + void updateMixerConfig(HLE::SharedMemory& sharedMem); void generateFrame(StereoFrame& frame); void generateFrame(DSPSource& source); void outputFrame(); - + // Perform the final mix, mixing the quadraphonic samples from all voices into the output audio frame + void performMix(Audio::HLE::SharedMemory& readRegion, Audio::HLE::SharedMemory& writeRegion); + // Decode an entire buffer worth of audio void decodeBuffer(DSPSource& source); diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index b4f9ab02..a616f317 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -76,6 +76,7 @@ namespace Audio { source.reset(); } + mixer.reset(); // Note: Reset audio pipe AFTER resetting all pipes, otherwise the new data will be yeeted resetAudioPipe(); } @@ -250,6 +251,8 @@ namespace Audio { source.isBufferIDDirty = false; } + + performMix(read, write); } void HLE_DSP::updateSourceConfig(Source& source, HLE::SourceConfiguration::Configuration& config, s16_le* adpcmCoefficients) { @@ -465,6 +468,50 @@ namespace Audio { } } + void HLE_DSP::performMix(Audio::HLE::SharedMemory& readRegion, Audio::HLE::SharedMemory& writeRegion) { + updateMixerConfig(readRegion); + // TODO: Do the actual audio mixing + + auto& dspStatus = writeRegion.dspStatus; + // Stub the DSP status. It's unknown what the "unknown" field is but Citra sets it to 0, so we do too to be safe + dspStatus.droppedFrames = 0; + dspStatus.unknown = 0; + } + + void HLE_DSP::updateMixerConfig(Audio::HLE::SharedMemory& sharedMem) { + auto& config = sharedMem.dspConfiguration; + // No configs have been changed, so there's nothing to update + if (config.dirtyRaw == 0) { + return; + } + + if (config.outputFormatDirty) { + mixer.channelFormat = config.outputFormat; + } + + if (config.masterVolumeDirty) { + mixer.volumes[0] = config.masterVolume; + } + + if (config.auxVolume0Dirty) { + mixer.volumes[1] = config.auxVolumes[0]; + } + + if (config.auxVolume1Dirty) { + mixer.volumes[2] = config.auxVolumes[1]; + } + + if (config.auxBusEnable0Dirty) { + mixer.enableAuxStages[0] = config.auxBusEnable[0] != 0; + } + + if (config.auxBusEnable1Dirty) { + mixer.enableAuxStages[1] = config.auxBusEnable[1] != 0; + } + + config.dirtyRaw = 0; + } + HLE_DSP::SampleBuffer HLE_DSP::decodePCM8(const u8* data, usize sampleCount, Source& source) { SampleBuffer decodedSamples(sampleCount); From 82068031f3fb2a5b010a77c9029558be2f275d9f Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 13 Oct 2024 23:27:08 +0300 Subject: [PATCH 213/251] Shadergen: Pre-allocate space for shadergen string --- src/core/PICA/shader_gen_glsl.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 69f74930..61694fcc 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -34,6 +34,8 @@ static constexpr const char* uniformDefinition = R"( std::string FragmentGenerator::getDefaultVertexShader() { std::string ret = ""; + // Reserve some space (128KB) in the output string to avoid too many allocations later + ret.reserve(128 * 1024); switch (api) { case API::GL: ret += "#version 410 core"; break; From fa9ce5fc70c71905a68a536610de01c8763836f4 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 13 Oct 2024 21:17:24 +0000 Subject: [PATCH 214/251] GLES: Implement logic ops via fb fetch (#608) * GLES: Implement logic ops via fb fetch * Attempt to fix deprecated libglx-mesa0 package * Update Qt_Build.yml * GLES: Enable fb fetch instead of requiring it * GLES: Add support for GL_ARM_shader_framebuffer_fetch * Fix GL_EXT_shader_framebuffer_fetch behavior --- .github/workflows/Hydra_Build.yml | 4 +- .github/workflows/Linux_AppImage_Build.yml | 2 +- .github/workflows/Linux_Build.yml | 2 +- .github/workflows/Qt_Build.yml | 3 +- CMakeLists.txt | 2 +- include/PICA/pica_frag_config.hpp | 5 ++ include/PICA/regs.hpp | 19 +++++++ include/PICA/shader_gen.hpp | 3 +- include/renderer_gl/gl_driver.hpp | 12 +++++ include/renderer_gl/renderer_gl.hpp | 2 + src/core/PICA/shader_gen_glsl.cpp | 59 ++++++++++++++++++++-- src/core/renderer_gl/renderer_gl.cpp | 10 +++- 12 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 include/renderer_gl/gl_driver.hpp diff --git a/.github/workflows/Hydra_Build.yml b/.github/workflows/Hydra_Build.yml index e2c2004b..ce48966c 100644 --- a/.github/workflows/Hydra_Build.yml +++ b/.github/workflows/Hydra_Build.yml @@ -107,7 +107,7 @@ jobs: - name: Install misc packages run: | - sudo apt-get update && sudo apt install libx11-dev libgl1-mesa-glx mesa-common-dev libfuse2 libwayland-dev + sudo apt-get update && sudo apt install libx11-dev libgl1 libglx-mesa0 mesa-common-dev libfuse2 libwayland-dev - name: Install newer Clang run: | @@ -160,7 +160,7 @@ jobs: - name: Install misc packages run: | - sudo apt-get update && sudo apt install libx11-dev libgl1-mesa-glx mesa-common-dev libfuse2 libwayland-dev + sudo apt-get update && sudo apt install libx11-dev libgl1 libglx-mesa0 mesa-common-dev libfuse2 libwayland-dev - name: Setup Vulkan SDK uses: humbletim/setup-vulkan-sdk@v1.2.0 diff --git a/.github/workflows/Linux_AppImage_Build.yml b/.github/workflows/Linux_AppImage_Build.yml index f32a7d38..1e3ec061 100644 --- a/.github/workflows/Linux_AppImage_Build.yml +++ b/.github/workflows/Linux_AppImage_Build.yml @@ -24,7 +24,7 @@ jobs: run: git submodule update --init --recursive - name: Install misc packages - run: sudo apt-get update && sudo apt install libx11-dev libgl1-mesa-glx mesa-common-dev libfuse2 + run: sudo apt-get update && sudo apt install libx11-dev libgl1 libglx-mesa0 mesa-common-dev libfuse2 - name: Install newer Clang run: | diff --git a/.github/workflows/Linux_Build.yml b/.github/workflows/Linux_Build.yml index 9cb05303..05f1fa54 100644 --- a/.github/workflows/Linux_Build.yml +++ b/.github/workflows/Linux_Build.yml @@ -24,7 +24,7 @@ jobs: run: git submodule update --init --recursive - name: Install misc packages - run: sudo apt-get update && sudo apt install libx11-dev libgl1-mesa-glx mesa-common-dev + run: sudo apt-get update && sudo apt install libx11-dev libgl1 libglx-mesa0 mesa-common-dev - name: Install newer Clang run: | diff --git a/.github/workflows/Qt_Build.yml b/.github/workflows/Qt_Build.yml index d3a09866..8a93a156 100644 --- a/.github/workflows/Qt_Build.yml +++ b/.github/workflows/Qt_Build.yml @@ -105,8 +105,7 @@ jobs: - name: Install misc packages run: | - sudo apt-get update && sudo apt install libx11-dev libgl1-mesa-glx mesa-common-dev libfuse2 libwayland-dev libgl1-mesa-dev - sudo add-apt-repository -y ppa:savoury1/qt-6-2 + sudo apt-get update && sudo apt install libx11-dev libgl1 libglx-mesa0 mesa-common-dev libfuse2 libwayland-dev libgl1-mesa-dev sudo apt update sudo apt install qt6-base-dev qt6-base-private-dev diff --git a/CMakeLists.txt b/CMakeLists.txt index 88a92f8a..91dafe9e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -381,7 +381,7 @@ if(ENABLE_OPENGL) set(RENDERER_GL_INCLUDE_FILES third_party/opengl/opengl.hpp include/renderer_gl/renderer_gl.hpp include/renderer_gl/textures.hpp include/renderer_gl/surfaces.hpp include/renderer_gl/surface_cache.hpp - include/renderer_gl/gl_state.hpp + include/renderer_gl/gl_state.hpp include/renderer_gl/gl_driver.hpp ) set(RENDERER_GL_SOURCE_FILES src/core/renderer_gl/renderer_gl.cpp diff --git a/include/PICA/pica_frag_config.hpp b/include/PICA/pica_frag_config.hpp index 5d5f8420..7b63a7b5 100644 --- a/include/PICA/pica_frag_config.hpp +++ b/include/PICA/pica_frag_config.hpp @@ -17,6 +17,7 @@ namespace PICA { // enable == off means a CompareFunction of Always BitField<0, 3, CompareFunction> alphaTestFunction; BitField<3, 1, u32> depthMapEnable; + BitField<4, 4, LogicOpMode> logicOpMode; }; }; @@ -214,6 +215,10 @@ namespace PICA { (alphaTestConfig & 1) ? static_cast(alphaTestFunction) : PICA::CompareFunction::Always; outConfig.depthMapEnable = regs[InternalRegs::DepthmapEnable] & 1; + // Shows if blending is enabled. If it is not enabled, then logic ops are enabled instead + const bool blendingEnabled = (regs[InternalRegs::ColourOperation] & (1 << 8)) != 0; + outConfig.logicOpMode = blendingEnabled ? LogicOpMode::Copy : LogicOpMode(Helpers::getBits<0, 4>(regs[InternalRegs::LogicOp])); + texConfig.texUnitConfig = regs[InternalRegs::TexUnitCfg]; texConfig.texEnvUpdateBuffer = regs[InternalRegs::TexEnvUpdateBuffer]; diff --git a/include/PICA/regs.hpp b/include/PICA/regs.hpp index 636e8f7c..3185d350 100644 --- a/include/PICA/regs.hpp +++ b/include/PICA/regs.hpp @@ -396,6 +396,25 @@ namespace PICA { GreaterOrEqual = 7, }; + enum class LogicOpMode : u32 { + Clear = 0, + And = 1, + ReverseAnd = 2, + Copy = 3, + Set = 4, + InvertedCopy = 5, + Nop = 6, + Invert = 7, + Nand = 8, + Or = 9, + Nor = 10, + Xor = 11, + Equiv = 12, + InvertedAnd = 13, + ReverseOr = 14, + InvertedOr = 15, + }; + enum class FogMode : u32 { Disabled = 0, Fog = 5, diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index 215e5adb..f938b558 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -25,10 +25,11 @@ namespace PICA::ShaderGen { bool isSamplerEnabled(u32 environmentID, u32 lutID); void compileFog(std::string& shader, const PICA::FragmentConfig& config); + void compileLogicOps(std::string& shader, const PICA::FragmentConfig& config); public: FragmentGenerator(API api, Language language) : api(api), language(language) {} - std::string generate(const PICA::FragmentConfig& config); + std::string generate(const PICA::FragmentConfig& config, void* driverInfo = nullptr); std::string getDefaultVertexShader(); void setTarget(API api, Language language) { diff --git a/include/renderer_gl/gl_driver.hpp b/include/renderer_gl/gl_driver.hpp new file mode 100644 index 00000000..a15c061f --- /dev/null +++ b/include/renderer_gl/gl_driver.hpp @@ -0,0 +1,12 @@ +#pragma once + +// Information about our OpenGL/OpenGL ES driver that we should keep track of +// Stuff like whether specific extensions are supported, and potentially things like OpenGL context information +namespace OpenGL { + struct Driver { + bool supportsExtFbFetch = false; + bool supportsArmFbFetch = false; + + bool supportFbFetch() const { return supportsExtFbFetch || supportsArmFbFetch; } + }; +} // namespace OpenGL \ No newline at end of file diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index 42b8bba1..07069536 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -12,6 +12,7 @@ #include "PICA/pica_vertex.hpp" #include "PICA/regs.hpp" #include "PICA/shader_gen.hpp" +#include "gl_driver.hpp" #include "gl_state.hpp" #include "helpers.hpp" #include "logger.hpp" @@ -82,6 +83,7 @@ class RendererGL final : public Renderer { OpenGL::Program& getSpecializedShader(); PICA::ShaderGen::FragmentGenerator fragShaderGen; + OpenGL::Driver driverInfo; MAKE_LOG_FUNCTION(log, rendererLogger) void setupBlending(); diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 61694fcc..e1bdf517 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -1,6 +1,10 @@ #include "PICA/pica_frag_config.hpp" #include "PICA/regs.hpp" #include "PICA/shader_gen.hpp" + +// We can include the driver headers here since they shouldn't have any actual API-specific code +#include "renderer_gl/gl_driver.hpp" + using namespace PICA; using namespace PICA::ShaderGen; @@ -96,7 +100,7 @@ std::string FragmentGenerator::getDefaultVertexShader() { return ret; } -std::string FragmentGenerator::generate(const FragmentConfig& config) { +std::string FragmentGenerator::generate(const FragmentConfig& config, void* driverInfo) { std::string ret = ""; switch (api) { @@ -105,6 +109,27 @@ std::string FragmentGenerator::generate(const FragmentConfig& config) { default: break; } + // For GLES we need to enable & use the framebuffer fetch extension in order to emulate logic ops + bool emitLogicOps = api == API::GLES && config.outConfig.logicOpMode != PICA::LogicOpMode::Copy && driverInfo != nullptr; + + if (emitLogicOps) { + auto driver = static_cast(driverInfo); + + // If the driver does not support framebuffer fetch at all, don't emit logic op code + if (!driver->supportFbFetch()) { + emitLogicOps = false; + } + + // Figure out which fb fetch extension we have and enable it + else { + if (driver->supportsExtFbFetch) { + ret += "\n#extension GL_EXT_shader_framebuffer_fetch : enable\n#define fb_color fragColor\n"; + } else if (driver->supportsArmFbFetch) { + ret += "\n#extension GL_ARM_shader_framebuffer_fetch : enable\n#define fb_color gl_LastFragColorARM[0]\n"; + } + } + } + bool unimplementedFlag = false; if (api == API::GLES) { ret += R"( @@ -194,10 +219,13 @@ std::string FragmentGenerator::generate(const FragmentConfig& config) { } compileFog(ret, config); - applyAlphaTest(ret, config); - ret += "fragColor = combinerOutput;\n}"; // End of main function + if (!emitLogicOps) { + ret += "fragColor = combinerOutput;\n}"; // End of main function + } else { + compileLogicOps(ret, config); + } return ret; } @@ -673,3 +701,28 @@ void FragmentGenerator::compileFog(std::string& shader, const PICA::FragmentConf shader += "float fog_factor = clamp(value.r + value.g * delta, 0.0, 1.0);"; shader += "combinerOutput.rgb = mix(fog_color, combinerOutput.rgb, fog_factor);"; } + +void FragmentGenerator::compileLogicOps(std::string& shader, const PICA::FragmentConfig& config) { + if (api != API::GLES) [[unlikely]] { + Helpers::warn("Shadergen: Unsupported API for compileLogicOps"); + shader += "fragColor = combinerOutput;\n}"; // End of main function + + return; + } + + shader += "fragColor = "; + switch (config.outConfig.logicOpMode) { + case PICA::LogicOpMode::Copy: shader += "combinerOutput"; break; + case PICA::LogicOpMode::Nop: shader += "fb_color"; break; + case PICA::LogicOpMode::Clear: shader += "vec4(0.0)"; break; + case PICA::LogicOpMode::Set: shader += "vec4(uintBitsToFloat(0xFFFFFFFFu))"; break; + case PICA::LogicOpMode::InvertedCopy: shader += "vec4(uvec4(combinerOutput * 255.0) ^ uvec4(0xFFu)) * (1.0 / 255.0)"; break; + + default: + shader += "combinerOutput"; + Helpers::warn("Shadergen: Unimplemented logic op mode"); + break; + } + + shader += ";\n}"; // End of main function +} \ No newline at end of file diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index 5146370a..ecbee3a2 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -167,6 +167,10 @@ void RendererGL::initGraphicsContextInternal() { reset(); + // Populate our driver info structure + driverInfo.supportsExtFbFetch = GLAD_GL_EXT_shader_framebuffer_fetch != 0; + driverInfo.supportsArmFbFetch = GLAD_GL_ARM_shader_framebuffer_fetch != 0; + // Initialize the default vertex shader used with shadergen std::string defaultShadergenVSSource = fragShaderGen.getDefaultVertexShader(); defaultShadergenVs.create({defaultShadergenVSSource.c_str(), defaultShadergenVSSource.size()}, OpenGL::Vertex); @@ -839,12 +843,16 @@ OpenGL::Program& RendererGL::getSpecializedShader() { constexpr uint uboBlockBinding = 2; PICA::FragmentConfig fsConfig(regs); + // If we're not on GLES, ignore the logic op configuration and don't generate redundant shaders for it, since we use hw logic ops +#ifndef USING_GLES + fsConfig.outConfig.logicOpMode = PICA::LogicOpMode(0); +#endif CachedProgram& programEntry = shaderCache[fsConfig]; OpenGL::Program& program = programEntry.program; if (!program.exists()) { - std::string fs = fragShaderGen.generate(fsConfig); + std::string fs = fragShaderGen.generate(fsConfig, &driverInfo); OpenGL::Shader fragShader({fs.c_str(), fs.size()}, OpenGL::Fragment); program.create({defaultShadergenVs, fragShader}); From afaf18f1248f7ffbb7bb36c1ef86ce150ecd1a00 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Mon, 14 Oct 2024 00:42:35 +0300 Subject: [PATCH 215/251] GLES: Fix Set logic op --- src/core/PICA/shader_gen_glsl.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index e1bdf517..13d5aa58 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -715,7 +715,7 @@ void FragmentGenerator::compileLogicOps(std::string& shader, const PICA::Fragmen case PICA::LogicOpMode::Copy: shader += "combinerOutput"; break; case PICA::LogicOpMode::Nop: shader += "fb_color"; break; case PICA::LogicOpMode::Clear: shader += "vec4(0.0)"; break; - case PICA::LogicOpMode::Set: shader += "vec4(uintBitsToFloat(0xFFFFFFFFu))"; break; + case PICA::LogicOpMode::Set: shader += "vec4(1.0)"; break; case PICA::LogicOpMode::InvertedCopy: shader += "vec4(uvec4(combinerOutput * 255.0) ^ uvec4(0xFFu)) * (1.0 / 255.0)"; break; default: @@ -725,4 +725,4 @@ void FragmentGenerator::compileLogicOps(std::string& shader, const PICA::Fragmen } shader += ";\n}"; // End of main function -} \ No newline at end of file +} From 49a94a13c53b317157544b329eadf65667b004a9 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 19 Oct 2024 16:53:51 +0300 Subject: [PATCH 216/251] Moar shader decompiler (#559) * Renderer: Add prepareForDraw callback * Add fmt submodule and port shader decompiler instructions to it * Add shader acceleration setting * Hook up vertex shaders to shader cache * Shader decompiler: Fix redundant compilations * Shader Decompiler: Fix vertex attribute upload * Shader compiler: Simplify generated code for reading and faster compilation * Further simplify shader decompiler output * Shader decompiler: More smallen-ing * Shader decompiler: Get PICA uniforms uploaded to the GPU * Shader decompiler: Readd clipping * Shader decompiler: Actually `break` on control flow instructions * Shader decompiler: More control flow handling * Shader decompiler: Fix desitnation mask * Shader Decomp: Remove pair member capture in lambda (unsupported on NDK) * Disgusting changes to handle the fact that hw shader shaders are 2x as big * Shader decompiler: Implement proper output semantic mapping * Moar instructions * Shader decompiler: Add FLR/SLT/SLTI/SGE/SGEI * Shader decompiler: Add register indexing * Shader decompiler: Optimize mova with both x and y masked * Shader decompiler: Add DPH/DPHI * Fix shader caching being broken * PICA decompiler: Cache VS uniforms * Simply vertex cache code * Simplify vertex cache code * Shader decompiler: Add loops * Shader decompiler: Implement safe multiplication * Shader decompiler: Implement LG2/EX2 * Shader decompiler: More control flow * Shader decompiler: Fix JMPU condition * Shader decompiler: Convert main function to void * PICA: Start implementing GPU vertex fetch * More hw VAO work * More hw VAO work * More GPU vertex fetch code * Add GL Stream Buffer from Duckstation * GL: Actually upload data to stream buffers * GPU: Cleanup immediate mode handling * Get first renders working with accelerated draws * Shader decompiler: Fix control flow analysis bugs * HW shaders: Accelerate indexed draws * Shader decompiler: Add support for compilation errors * GLSL decompiler: Fall back for LITP * Add Renderdoc scope classes * Fix control flow analysis bug * HW shaders: Fix attribute fetch * Rewriting hw vertex fetch * Stream buffer: Fix copy-paste mistake * HW shaders: Fix indexed rendering * HW shaders: Add padding attributes * HW shaders: Avoid redundant glVertexAttrib4f calls * HW shaders: Fix loops * HW shaders: Make generated shaders slightly smaller * Fix libretro build * HW shaders: Fix android * Remove redundant ubershader checks * Set accelerate shader default to true * Shader decompiler: Don't declare VS input attributes as an array * Change ubuntu-latest to Ubuntu 24.04 because Microsoft screwed up their CI again * fix merge conflict bug --- .github/workflows/Android_Build.yml | 4 +- .github/workflows/HTTP_Build.yml | 2 +- .github/workflows/Hydra_Build.yml | 4 +- .github/workflows/Linux_AppImage_Build.yml | 2 +- .github/workflows/Linux_Build.yml | 2 +- .github/workflows/Qt_Build.yml | 2 +- .gitmodules | 3 + CMakeLists.txt | 12 +- include/PICA/draw_acceleration.hpp | 45 ++ include/PICA/gpu.hpp | 10 +- include/PICA/pica_vert_config.hpp | 57 ++ include/PICA/shader.hpp | 25 +- include/PICA/shader_decompiler.hpp | 19 +- include/PICA/shader_gen.hpp | 3 + include/PICA/shader_unit.hpp | 7 +- include/align.hpp | 99 +++ include/config.hpp | 6 +- include/renderdoc.hpp | 33 +- include/renderer.hpp | 13 +- include/renderer_gl/gl_state.hpp | 9 - include/renderer_gl/renderer_gl.hpp | 73 ++- src/config.cpp | 2 + src/core/PICA/draw_acceleration.cpp | 148 +++++ src/core/PICA/gpu.cpp | 79 ++- src/core/PICA/regs.cpp | 3 +- src/core/PICA/shader_decompiler.cpp | 624 ++++++++++++++++--- src/core/PICA/shader_gen_glsl.cpp | 111 ++++ src/core/PICA/shader_unit.cpp | 1 + src/core/renderer_gl/gl_state.cpp | 3 - src/core/renderer_gl/renderer_gl.cpp | 380 ++++++++--- src/libretro_core.cpp | 7 +- third_party/duckstation/gl/stream_buffer.cpp | 288 +++++++++ third_party/duckstation/gl/stream_buffer.h | 53 ++ third_party/fmt | 1 + 34 files changed, 1877 insertions(+), 253 deletions(-) create mode 100644 include/PICA/draw_acceleration.hpp create mode 100644 include/PICA/pica_vert_config.hpp create mode 100644 include/align.hpp create mode 100644 src/core/PICA/draw_acceleration.cpp create mode 100644 third_party/duckstation/gl/stream_buffer.cpp create mode 100644 third_party/duckstation/gl/stream_buffer.h create mode 160000 third_party/fmt diff --git a/.github/workflows/Android_Build.yml b/.github/workflows/Android_Build.yml index 11811f8b..b7e64f5f 100644 --- a/.github/workflows/Android_Build.yml +++ b/.github/workflows/Android_Build.yml @@ -8,7 +8,7 @@ on: jobs: x64: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: @@ -73,7 +73,7 @@ jobs: ./src/pandroid/app/build/outputs/apk/${{ env.BUILD_TYPE }}/app-${{ env.BUILD_TYPE }}.apk arm64: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 strategy: matrix: diff --git a/.github/workflows/HTTP_Build.yml b/.github/workflows/HTTP_Build.yml index 0bdaa4f7..c4f7cfee 100644 --- a/.github/workflows/HTTP_Build.yml +++ b/.github/workflows/HTTP_Build.yml @@ -16,7 +16,7 @@ jobs: # well on Windows or Mac. You can convert this to a matrix build if you need # cross-platform coverage. # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/Hydra_Build.yml b/.github/workflows/Hydra_Build.yml index ce48966c..785e0e4a 100644 --- a/.github/workflows/Hydra_Build.yml +++ b/.github/workflows/Hydra_Build.yml @@ -98,7 +98,7 @@ jobs: ${{github.workspace}}/docs/libretro/panda3ds_libretro.info Linux: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -151,7 +151,7 @@ jobs: ${{github.workspace}}/docs/libretro/panda3ds_libretro.info Android-x64: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/Linux_AppImage_Build.yml b/.github/workflows/Linux_AppImage_Build.yml index 1e3ec061..51c4a933 100644 --- a/.github/workflows/Linux_AppImage_Build.yml +++ b/.github/workflows/Linux_AppImage_Build.yml @@ -16,7 +16,7 @@ jobs: # well on Windows or Mac. You can convert this to a matrix build if you need # cross-platform coverage. # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/Linux_Build.yml b/.github/workflows/Linux_Build.yml index 05f1fa54..dfcb6954 100644 --- a/.github/workflows/Linux_Build.yml +++ b/.github/workflows/Linux_Build.yml @@ -16,7 +16,7 @@ jobs: # well on Windows or Mac. You can convert this to a matrix build if you need # cross-platform coverage. # See: https://docs.github.com/en/free-pro-team@latest/actions/learn-github-actions/managing-complex-workflows#using-a-build-matrix - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/Qt_Build.yml b/.github/workflows/Qt_Build.yml index 8a93a156..3b846a27 100644 --- a/.github/workflows/Qt_Build.yml +++ b/.github/workflows/Qt_Build.yml @@ -96,7 +96,7 @@ jobs: path: 'Alber.zip' Linux: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 diff --git a/.gitmodules b/.gitmodules index 97bc129c..f1a70f41 100644 --- a/.gitmodules +++ b/.gitmodules @@ -76,6 +76,9 @@ [submodule "third_party/metal-cpp"] path = third_party/metal-cpp url = https://github.com/Panda3DS-emu/metal-cpp +[submodule "third_party/fmt"] + path = third_party/fmt + url = https://github.com/fmtlib/fmt [submodule "third_party/fdk-aac"] path = third_party/fdk-aac url = https://github.com/Panda3DS-emu/fdk-aac/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 91dafe9e..fe72f3b6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,11 +146,13 @@ if (NOT ANDROID) target_link_libraries(AlberCore PUBLIC SDL2-static) endif() +add_subdirectory(third_party/fmt) add_subdirectory(third_party/toml11) include_directories(${SDL2_INCLUDE_DIR}) include_directories(third_party/toml11) include_directories(third_party/glm) include_directories(third_party/renderdoc) +include_directories(third_party/duckstation) add_subdirectory(third_party/cmrc) @@ -263,7 +265,7 @@ set(PICA_SOURCE_FILES src/core/PICA/gpu.cpp src/core/PICA/regs.cpp src/core/PICA src/core/PICA/shader_interpreter.cpp src/core/PICA/dynapica/shader_rec.cpp src/core/PICA/dynapica/shader_rec_emitter_x64.cpp src/core/PICA/pica_hash.cpp src/core/PICA/dynapica/shader_rec_emitter_arm64.cpp src/core/PICA/shader_gen_glsl.cpp - src/core/PICA/shader_decompiler.cpp + src/core/PICA/shader_decompiler.cpp src/core/PICA/draw_acceleration.cpp ) set(LOADER_SOURCE_FILES src/core/loader/elf.cpp src/core/loader/ncsd.cpp src/core/loader/ncch.cpp src/core/loader/3dsx.cpp src/core/loader/lz77.cpp) @@ -315,7 +317,8 @@ set(HEADER_FILES include/emulator.hpp include/helpers.hpp include/termcolor.hpp include/audio/miniaudio_device.hpp include/ring_buffer.hpp include/bitfield.hpp include/audio/dsp_shared_mem.hpp include/audio/hle_core.hpp include/capstone.hpp include/audio/aac.hpp include/PICA/pica_frag_config.hpp include/PICA/pica_frag_uniforms.hpp include/PICA/shader_gen_types.hpp include/PICA/shader_decompiler.hpp - include/sdl_sensors.hpp include/renderdoc.hpp include/audio/aac_decoder.hpp + include/PICA/pica_vert_config.hpp include/sdl_sensors.hpp include/PICA/draw_acceleration.hpp include/renderdoc.hpp + include/align.hpp include/audio/aac_decoder.hpp ) cmrc_add_resource_library( @@ -348,7 +351,6 @@ if(ENABLE_LUAJIT AND NOT ANDROID) endif() if(ENABLE_QT_GUI) - include_directories(third_party/duckstation) set(THIRD_PARTY_SOURCE_FILES ${THIRD_PARTY_SOURCE_FILES} third_party/duckstation/window_info.cpp third_party/duckstation/gl/context.cpp) if(APPLE) @@ -391,6 +393,8 @@ if(ENABLE_OPENGL) src/host_shaders/opengl_fragment_shader.frag ) + set(THIRD_PARTY_SOURCE_FILES ${THIRD_PARTY_SOURCE_FILES} third_party/duckstation/gl/stream_buffer.cpp) + set(HEADER_FILES ${HEADER_FILES} ${RENDERER_GL_INCLUDE_FILES}) source_group("Source Files\\Core\\OpenGL Renderer" FILES ${RENDERER_GL_SOURCE_FILES}) @@ -480,7 +484,7 @@ set(ALL_SOURCES ${SOURCE_FILES} ${FS_SOURCE_FILES} ${CRYPTO_SOURCE_FILES} ${KERN target_sources(AlberCore PRIVATE ${ALL_SOURCES}) target_link_libraries(AlberCore PRIVATE dynarmic cryptopp glad resources_console_fonts teakra fdk-aac) -target_link_libraries(AlberCore PUBLIC glad capstone) +target_link_libraries(AlberCore PUBLIC glad capstone fmt::fmt) if(ENABLE_DISCORD_RPC AND NOT ANDROID) target_compile_definitions(AlberCore PUBLIC "PANDA3DS_ENABLE_DISCORD_RPC=1") diff --git a/include/PICA/draw_acceleration.hpp b/include/PICA/draw_acceleration.hpp new file mode 100644 index 00000000..6a66cdc1 --- /dev/null +++ b/include/PICA/draw_acceleration.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include "helpers.hpp" + +namespace PICA { + struct DrawAcceleration { + static constexpr u32 maxAttribCount = 16; + static constexpr u32 maxLoaderCount = 12; + + struct AttributeInfo { + u32 offset; + u32 stride; + + u8 type; + u8 componentCount; + + std::array fixedValue; // For fixed attributes + }; + + struct Loader { + // Data to upload for this loader + u8* data; + usize size; + }; + + u8* indexBuffer; + + // Minimum and maximum index in the index buffer for a draw call + u16 minimumIndex, maximumIndex; + u32 totalAttribCount; + u32 totalLoaderCount; + u32 enabledAttributeMask; + u32 fixedAttributes; + u32 vertexDataSize; + + std::array attributeInfo; + std::array loaders; + + bool canBeAccelerated; + bool indexed; + bool useShortIndices; + }; +} // namespace PICA \ No newline at end of file diff --git a/include/PICA/gpu.hpp b/include/PICA/gpu.hpp index ac2a49e6..c168a9bf 100644 --- a/include/PICA/gpu.hpp +++ b/include/PICA/gpu.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include "PICA/draw_acceleration.hpp" #include "PICA/dynapica/shader_rec.hpp" #include "PICA/float_types.hpp" #include "PICA/pica_vertex.hpp" @@ -13,6 +14,12 @@ #include "memory.hpp" #include "renderer.hpp" +enum class ShaderExecMode { + Interpreter, // Interpret shaders on the CPU + JIT, // Recompile shaders to CPU machine code + Hardware, // Recompiler shaders to host shaders and run them on the GPU +}; + class GPU { static constexpr u32 regNum = 0x300; static constexpr u32 extRegNum = 0x1000; @@ -45,7 +52,7 @@ class GPU { uint immediateModeVertIndex; uint immediateModeAttrIndex; // Index of the immediate mode attribute we're uploading - template + template void drawArrays(); // Silly method of avoiding linking problems. TODO: Change to something less silly @@ -81,6 +88,7 @@ class GPU { std::unique_ptr renderer; PICA::Vertex getImmediateModeVertex(); + void getAcceleratedDrawInfo(PICA::DrawAcceleration& accel, bool indexed); public: // 256 entries per LUT with each LUT as its own row forming a 2D image 256 * LUT_COUNT // Encoded in PICA native format diff --git a/include/PICA/pica_vert_config.hpp b/include/PICA/pica_vert_config.hpp new file mode 100644 index 00000000..4300e454 --- /dev/null +++ b/include/PICA/pica_vert_config.hpp @@ -0,0 +1,57 @@ +#pragma once +#include +#include +#include +#include +#include + +#include "PICA/pica_hash.hpp" +#include "PICA/regs.hpp" +#include "PICA/shader.hpp" +#include "bitfield.hpp" +#include "helpers.hpp" + +namespace PICA { + // Configuration struct used + struct VertConfig { + PICAHash::HashType shaderHash; + PICAHash::HashType opdescHash; + u32 entrypoint; + + // PICA registers for configuring shader output->fragment semantic mapping + std::array outmaps{}; + u16 outputMask; + u8 outputCount; + bool usingUbershader; + + // Pad to 56 bytes so that the compiler won't insert unnecessary padding, which in turn will affect our unordered_map lookup + // As the padding will get hashed and memcmp'd... + u32 pad{}; + + bool operator==(const VertConfig& config) const { + // Hash function and equality operator required by std::unordered_map + return std::memcmp(this, &config, sizeof(VertConfig)) == 0; + } + + VertConfig(PICAShader& shader, const std::array& regs, bool usingUbershader) : usingUbershader(usingUbershader) { + shaderHash = shader.getCodeHash(); + opdescHash = shader.getOpdescHash(); + entrypoint = shader.entrypoint; + + outputCount = regs[PICA::InternalRegs::ShaderOutputCount] & 7; + outputMask = regs[PICA::InternalRegs::VertexShaderOutputMask]; + for (int i = 0; i < outputCount; i++) { + // Mask out unused bits + outmaps[i] = regs[PICA::InternalRegs::ShaderOutmap0 + i] & 0x1F1F1F1F; + } + } + }; +} // namespace PICA + +static_assert(sizeof(PICA::VertConfig) == 56); + +// Override std::hash for our vertex config class +template <> +struct std::hash { + std::size_t operator()(const PICA::VertConfig& config) const noexcept { return PICAHash::computeHash((const char*)&config, sizeof(config)); } +}; \ No newline at end of file diff --git a/include/PICA/shader.hpp b/include/PICA/shader.hpp index e5f57c72..1040d2ff 100644 --- a/include/PICA/shader.hpp +++ b/include/PICA/shader.hpp @@ -107,6 +107,11 @@ class PICAShader { alignas(16) std::array inputs; // Attributes passed to the shader alignas(16) std::array outputs; alignas(16) vec4f dummy = vec4f({f24::zero(), f24::zero(), f24::zero(), f24::zero()}); // Dummy register used by the JIT + + // We use a hashmap for matching 3DS shaders to their equivalent compiled code in our shader cache in the shader JIT + // We choose our hash type to be a 64-bit integer by default, as the collision chance is very tiny and generating it is decently optimal + // Ideally we want to be able to support multiple different types of hash depending on compilation settings, but let's get this working first + using Hash = PICAHash::HashType; protected: std::array operandDescriptors; @@ -125,14 +130,13 @@ class PICAShader { std::array callInfo; ShaderType type; - // We use a hashmap for matching 3DS shaders to their equivalent compiled code in our shader cache in the shader JIT - // We choose our hash type to be a 64-bit integer by default, as the collision chance is very tiny and generating it is decently optimal - // Ideally we want to be able to support multiple different types of hash depending on compilation settings, but let's get this working first - using Hash = PICAHash::HashType; - Hash lastCodeHash = 0; // Last hash computed for the shader code (Used for the JIT caching mechanism) Hash lastOpdescHash = 0; // Last hash computed for the operand descriptors (Also used for the JIT) + public: + bool uniformsDirty = false; + + protected: bool codeHashDirty = false; bool opdescHashDirty = false; @@ -284,6 +288,7 @@ class PICAShader { uniform[2] = f24::fromRaw(((floatUniformBuffer[0] & 0xff) << 16) | (floatUniformBuffer[1] >> 16)); uniform[3] = f24::fromRaw(floatUniformBuffer[0] >> 8); } + uniformsDirty = true; } } @@ -295,6 +300,12 @@ class PICAShader { u[1] = getBits<8, 8>(word); u[2] = getBits<16, 8>(word); u[3] = getBits<24, 8>(word); + uniformsDirty = true; + } + + void uploadBoolUniform(u32 value) { + boolUniform = value; + uniformsDirty = true; } void run(); @@ -302,6 +313,10 @@ class PICAShader { Hash getCodeHash(); Hash getOpdescHash(); + + // Returns how big the PICA uniforms are combined. Used for hw accelerated shaders where we upload the uniforms to our GPU. + static constexpr usize totalUniformSize() { return sizeof(floatUniforms) + sizeof(intUniforms) + sizeof(boolUniform); } + void* getUniformPointer() { return static_cast(&floatUniforms); } }; static_assert( diff --git a/include/PICA/shader_decompiler.hpp b/include/PICA/shader_decompiler.hpp index 1253226f..4a5cdc13 100644 --- a/include/PICA/shader_decompiler.hpp +++ b/include/PICA/shader_decompiler.hpp @@ -1,8 +1,11 @@ #pragma once +#include + +#include #include #include #include -#include +#include #include #include "PICA/shader.hpp" @@ -41,9 +44,12 @@ namespace PICA::ShaderGen { explicit Function(u32 start, u32 end) : start(start), end(end) {} bool operator<(const Function& other) const { return AddressRange(start, end) < AddressRange(other.start, other.end); } - std::string getIdentifier() const { return "func_" + std::to_string(start) + "_to_" + std::to_string(end); } - std::string getForwardDecl() const { return "void " + getIdentifier() + "();\n"; } - std::string getCallStatement() const { return getIdentifier() + "()"; } + std::string getIdentifier() const { return fmt::format("fn_{}_{}", start, end); } + // To handle weird control flow, we have to return from each function a bool that indicates whether or not the shader reached an end + // instruction and should thus terminate. This is necessary for games like Rayman and Gravity Falls, which have "END" instructions called + // from within functions deep in the callstack + std::string getForwardDecl() const { return fmt::format("bool fn_{}_{}();\n", start, end); } + std::string getCallStatement() const { return fmt::format("fn_{}_{}()", start, end); } }; std::set functions{}; @@ -93,9 +99,11 @@ namespace PICA::ShaderGen { API api; Language language; + bool compilationError = false; void compileInstruction(u32& pc, bool& finished); - void compileRange(const AddressRange& range); + // Compile range "range" and returns the end PC or if we're "finished" with the program (called an END instruction) + std::pair compileRange(const AddressRange& range); void callFunction(const Function& function); const Function* findFunction(const AddressRange& range); @@ -105,6 +113,7 @@ namespace PICA::ShaderGen { std::string getDest(u32 dest) const; std::string getSwizzlePattern(u32 swizzle) const; std::string getDestSwizzle(u32 destinationMask) const; + const char* getCondition(u32 cond, u32 refX, u32 refY); void setDest(u32 operandDescriptor, const std::string& dest, const std::string& value); // Returns if the instruction uses the typical register encodings most instructions use diff --git a/include/PICA/shader_gen.hpp b/include/PICA/shader_gen.hpp index f938b558..b6751e05 100644 --- a/include/PICA/shader_gen.hpp +++ b/include/PICA/shader_gen.hpp @@ -3,6 +3,7 @@ #include "PICA/gpu.hpp" #include "PICA/pica_frag_config.hpp" +#include "PICA/pica_vert_config.hpp" #include "PICA/regs.hpp" #include "PICA/shader_gen_types.hpp" #include "helpers.hpp" @@ -31,6 +32,8 @@ namespace PICA::ShaderGen { FragmentGenerator(API api, Language language) : api(api), language(language) {} std::string generate(const PICA::FragmentConfig& config, void* driverInfo = nullptr); std::string getDefaultVertexShader(); + // For when PICA shader is acceleration is enabled. Turn the PICA shader source into a proper vertex shader + std::string getVertexShaderAccelerated(const std::string& picaSource, const PICA::VertConfig& vertConfig, bool usingUbershader); void setTarget(API api, Language language) { this->api = api; diff --git a/include/PICA/shader_unit.hpp b/include/PICA/shader_unit.hpp index d8d93160..80e01346 100644 --- a/include/PICA/shader_unit.hpp +++ b/include/PICA/shader_unit.hpp @@ -2,10 +2,9 @@ #include "PICA/shader.hpp" class ShaderUnit { - -public: - PICAShader vs; // Vertex shader - PICAShader gs; // Geometry shader + public: + PICAShader vs; // Vertex shader + PICAShader gs; // Geometry shader ShaderUnit() : vs(ShaderType::Vertex), gs(ShaderType::Geometry) {} void reset(); diff --git a/include/align.hpp b/include/align.hpp new file mode 100644 index 00000000..6b79a656 --- /dev/null +++ b/include/align.hpp @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2019-2022 Connor McLaughlin +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#pragma once + +#include + +#include "helpers.hpp" + +#ifdef _MSC_VER +#include +#endif + +namespace Common { + template + constexpr bool isAligned(T value, unsigned int alignment) { + return (value % static_cast(alignment)) == 0; + } + + template + constexpr T alignUp(T value, unsigned int alignment) { + return (value + static_cast(alignment - 1)) / static_cast(alignment) * static_cast(alignment); + } + + template + constexpr T alignDown(T value, unsigned int alignment) { + return value / static_cast(alignment) * static_cast(alignment); + } + + template + constexpr bool isAlignedPow2(T value, unsigned int alignment) { + return (value & static_cast(alignment - 1)) == 0; + } + + template + constexpr T alignUpPow2(T value, unsigned int alignment) { + return (value + static_cast(alignment - 1)) & static_cast(~static_cast(alignment - 1)); + } + + template + constexpr T alignDownPow2(T value, unsigned int alignment) { + return value & static_cast(~static_cast(alignment - 1)); + } + + template + constexpr bool isPow2(T value) { + return (value & (value - 1)) == 0; + } + + template + constexpr T previousPow2(T value) { + if (value == static_cast(0)) return 0; + + value |= (value >> 1); + value |= (value >> 2); + value |= (value >> 4); + if constexpr (sizeof(T) >= 16) value |= (value >> 8); + if constexpr (sizeof(T) >= 32) value |= (value >> 16); + if constexpr (sizeof(T) >= 64) value |= (value >> 32); + return value - (value >> 1); + } + + template + constexpr T nextPow2(T value) { + // https://graphics.stanford.edu/~seander/bithacks.html#RoundUpPowerOf2 + if (value == static_cast(0)) return 0; + + value--; + value |= (value >> 1); + value |= (value >> 2); + value |= (value >> 4); + if constexpr (sizeof(T) >= 16) value |= (value >> 8); + if constexpr (sizeof(T) >= 32) value |= (value >> 16); + if constexpr (sizeof(T) >= 64) value |= (value >> 32); + value++; + return value; + } + + ALWAYS_INLINE static void* alignedMalloc(size_t size, size_t alignment) { +#ifdef _MSC_VER + return _aligned_malloc(size, alignment); +#else + // Unaligned sizes are slow on macOS. +#ifdef __APPLE__ + if (isPow2(alignment)) size = (size + alignment - 1) & ~(alignment - 1); +#endif + void* ret = nullptr; + return (posix_memalign(&ret, alignment, size) == 0) ? ret : nullptr; +#endif + } + + ALWAYS_INLINE static void alignedFree(void* ptr) { +#ifdef _MSC_VER + _aligned_free(ptr); +#else + free(ptr); +#endif + } +} // namespace Common \ No newline at end of file diff --git a/include/config.hpp b/include/config.hpp index 459f0907..a8ba8946 100644 --- a/include/config.hpp +++ b/include/config.hpp @@ -20,11 +20,13 @@ struct EmulatorConfig { #else static constexpr bool ubershaderDefault = true; #endif - + static constexpr bool accelerateShadersDefault = true; + bool shaderJitEnabled = shaderJitDefault; - bool discordRpcEnabled = false; bool useUbershaders = ubershaderDefault; + bool accelerateShaders = accelerateShadersDefault; bool accurateShaderMul = false; + bool discordRpcEnabled = false; // Toggles whether to force shadergen when there's more than N lights active and we're using the ubershader, for better performance bool forceShadergenForLights = true; diff --git a/include/renderdoc.hpp b/include/renderdoc.hpp index 94a0f494..ea2c8a3d 100644 --- a/include/renderdoc.hpp +++ b/include/renderdoc.hpp @@ -35,4 +35,35 @@ namespace Renderdoc { static void setOutputDir(const std::string& path, const std::string& prefix) {} static constexpr bool isSupported() { return false; } } // namespace Renderdoc -#endif \ No newline at end of file +#endif + +namespace Renderdoc { + // RAII scope class that encloses a Renderdoc capture, as long as it's triggered by triggerCapture + struct Scope { + Scope() { Renderdoc::startCapture(); } + ~Scope() { Renderdoc::endCapture(); } + + Scope(const Scope&) = delete; + Scope& operator=(const Scope&) = delete; + + Scope(Scope&&) = delete; + Scope& operator=(const Scope&&) = delete; + }; + + // RAII scope class that encloses a Renderdoc capture. Unlike regular Scope it doesn't wait for a trigger, it will always issue the capture + // trigger on its own and take a capture + struct InstantScope { + InstantScope() { + Renderdoc::triggerCapture(); + Renderdoc::startCapture(); + } + + ~InstantScope() { Renderdoc::endCapture(); } + + InstantScope(const InstantScope&) = delete; + InstantScope& operator=(const InstantScope&) = delete; + + InstantScope(InstantScope&&) = delete; + InstantScope& operator=(const InstantScope&&) = delete; + }; +} // namespace Renderdoc \ No newline at end of file diff --git a/include/renderer.hpp b/include/renderer.hpp index 569a730b..5a1efc77 100644 --- a/include/renderer.hpp +++ b/include/renderer.hpp @@ -1,9 +1,10 @@ #pragma once #include +#include #include #include -#include +#include "PICA/draw_acceleration.hpp" #include "PICA/pica_vertex.hpp" #include "PICA/regs.hpp" #include "helpers.hpp" @@ -21,9 +22,11 @@ enum class RendererType : s8 { }; struct EmulatorConfig; -class GPU; struct SDL_Window; +class GPU; +class ShaderUnit; + class Renderer { protected: GPU& gpu; @@ -77,7 +80,11 @@ class Renderer { virtual std::string getUbershader() { return ""; } virtual void setUbershader(const std::string& shader) {} - virtual void setUbershaderSetting(bool value) {} + // This function is called on every draw call before parsing vertex data. + // It is responsible for things like looking up which vertex/fragment shaders to use, recompiling them if they don't exist, choosing between + // ubershaders and shadergen, and so on. + // Returns whether this draw is eligible for using hardware-accelerated shaders or if shaders should run on the CPU + virtual bool prepareForDraw(ShaderUnit& shaderUnit, PICA::DrawAcceleration* accel) { return false; } // Functions for initializing the graphics context for the Qt frontend, where we don't have the convenience of SDL_Window #ifdef PANDA3DS_FRONTEND_QT diff --git a/include/renderer_gl/gl_state.hpp b/include/renderer_gl/gl_state.hpp index e5591ea0..4085cabc 100644 --- a/include/renderer_gl/gl_state.hpp +++ b/include/renderer_gl/gl_state.hpp @@ -38,7 +38,6 @@ struct GLStateManager { GLuint stencilMask; GLuint boundVAO; - GLuint boundVBO; GLuint currentProgram; GLuint boundUBO; @@ -173,13 +172,6 @@ struct GLStateManager { } } - void bindVBO(GLuint handle) { - if (boundVBO != handle) { - boundVBO = handle; - glBindBuffer(GL_ARRAY_BUFFER, handle); - } - } - void useProgram(GLuint handle) { if (currentProgram != handle) { currentProgram = handle; @@ -195,7 +187,6 @@ struct GLStateManager { } void bindVAO(const OpenGL::VertexArray& vao) { bindVAO(vao.handle()); } - void bindVBO(const OpenGL::VertexBuffer& vbo) { bindVBO(vbo.handle()); } void useProgram(const OpenGL::Program& program) { useProgram(program.handle()); } void setColourMask(bool r, bool g, bool b, bool a) { diff --git a/include/renderer_gl/renderer_gl.hpp b/include/renderer_gl/renderer_gl.hpp index 07069536..fab239f2 100644 --- a/include/renderer_gl/renderer_gl.hpp +++ b/include/renderer_gl/renderer_gl.hpp @@ -3,15 +3,20 @@ #include #include #include +#include +#include #include #include +#include #include "PICA/float_types.hpp" #include "PICA/pica_frag_config.hpp" #include "PICA/pica_hash.hpp" +#include "PICA/pica_vert_config.hpp" #include "PICA/pica_vertex.hpp" #include "PICA/regs.hpp" #include "PICA/shader_gen.hpp" +#include "gl/stream_buffer.h" #include "gl_driver.hpp" #include "gl_state.hpp" #include "helpers.hpp" @@ -29,9 +34,11 @@ class RendererGL final : public Renderer { OpenGL::Program triangleProgram; OpenGL::Program displayProgram; - OpenGL::VertexArray vao; + // VAO for when not using accelerated vertex shaders. Contains attribute declarations matching to the PICA fixed function fragment attributes + OpenGL::VertexArray defaultVAO; + // VAO for when using accelerated vertex shaders. The PICA vertex shader inputs are passed as attributes without CPU processing. + OpenGL::VertexArray hwShaderVAO; OpenGL::VertexBuffer vbo; - bool enableUbershader = true; // Data struct { @@ -54,6 +61,21 @@ class RendererGL final : public Renderer { float oldDepthScale = -1.0; float oldDepthOffset = 0.0; bool oldDepthmapEnable = false; + // Set by prepareForDraw, tells us whether the current draw is using hw-accelerated shader + bool usingAcceleratedShader = false; + bool performIndexedRender = false; + bool usingShortIndices = false; + + // Set by prepareForDraw, metadata for indexed renders + GLuint minimumIndex = 0; + GLuint maximumIndex = 0; + void* hwIndexBufferOffset = nullptr; + + // When doing hw shaders, we cache which attributes are enabled in our VAO to avoid having to enable/disable all attributes on each draw + u32 previousAttributeMask = 0; + + // Cached pointer to the current vertex shader when using HW accelerated shaders + OpenGL::Shader* generatedVertexShader = nullptr; SurfaceCache depthBufferCache; SurfaceCache colourBufferCache; @@ -71,12 +93,51 @@ class RendererGL final : public Renderer { // We can compile this once and then link it with all other generated fragment shaders OpenGL::Shader defaultShadergenVs; GLuint shadergenFragmentUBO; + // UBO for uploading the PICA uniforms when using hw shaders + GLuint hwShaderUniformUBO; + + using StreamBuffer = OpenGLStreamBuffer; + std::unique_ptr hwVertexBuffer; + std::unique_ptr hwIndexBuffer; + + // Cache of fixed attribute values so that we don't do any duplicate updates + std::array, 16> fixedAttrValues; // Cached recompiled fragment shader struct CachedProgram { OpenGL::Program program; }; - std::unordered_map shaderCache; + + struct ShaderCache { + std::unordered_map> vertexShaderCache; + std::unordered_map fragmentShaderCache; + + // Program cache indexed by GLuints for the vertex and fragment shader to use + // Top 32 bits are the vertex shader GLuint, bottom 32 bits are the fs GLuint + std::unordered_map programCache; + + void clear() { + for (auto& it : programCache) { + CachedProgram& cachedProgram = it.second; + cachedProgram.program.free(); + } + + for (auto& it : vertexShaderCache) { + if (it.second.has_value()) { + it.second->free(); + } + } + + for (auto& it : fragmentShaderCache) { + it.second.free(); + } + + programCache.clear(); + vertexShaderCache.clear(); + fragmentShaderCache.clear(); + } + }; + ShaderCache shaderCache; OpenGL::Framebuffer getColourFBO(); OpenGL::Texture getTexture(Texture& tex); @@ -95,6 +156,8 @@ class RendererGL final : public Renderer { void updateFogLUT(); void initGraphicsContextInternal(); + void accelerateVertexUpload(ShaderUnit& shaderUnit, PICA::DrawAcceleration* accel); + public: RendererGL(GPU& gpu, const std::array& internalRegs, const std::array& externalRegs) : Renderer(gpu, internalRegs, externalRegs), fragShaderGen(PICA::ShaderGen::API::GL, PICA::ShaderGen::Language::GLSL) {} @@ -112,15 +175,13 @@ class RendererGL final : public Renderer { virtual bool supportsShaderReload() override { return true; } virtual std::string getUbershader() override; virtual void setUbershader(const std::string& shader) override; - - virtual void setUbershaderSetting(bool value) override { enableUbershader = value; } + virtual bool prepareForDraw(ShaderUnit& shaderUnit, PICA::DrawAcceleration* accel) override; std::optional getColourBuffer(u32 addr, PICA::ColorFmt format, u32 width, u32 height, bool createIfnotFound = true); // Note: The caller is responsible for deleting the currently bound FBO before calling this void setFBO(uint handle) { screenFramebuffer.m_handle = handle; } void resetStateManager() { gl.reset(); } - void clearShaderCache(); void initUbershader(OpenGL::Program& program); #ifdef PANDA3DS_FRONTEND_QT diff --git a/src/config.cpp b/src/config.cpp index 70f2189c..331ab137 100644 --- a/src/config.cpp +++ b/src/config.cpp @@ -67,6 +67,7 @@ void EmulatorConfig::load() { vsyncEnabled = toml::find_or(gpu, "EnableVSync", true); useUbershaders = toml::find_or(gpu, "UseUbershaders", ubershaderDefault); accurateShaderMul = toml::find_or(gpu, "AccurateShaderMultiplication", false); + accelerateShaders = toml::find_or(gpu, "AccelerateShaders", accelerateShadersDefault); forceShadergenForLights = toml::find_or(gpu, "ForceShadergenForLighting", true); lightShadergenThreshold = toml::find_or(gpu, "ShadergenLightThreshold", 1); @@ -141,6 +142,7 @@ void EmulatorConfig::save() { data["GPU"]["UseUbershaders"] = useUbershaders; data["GPU"]["ForceShadergenForLighting"] = forceShadergenForLights; data["GPU"]["ShadergenLightThreshold"] = lightShadergenThreshold; + data["GPU"]["AccelerateShaders"] = accelerateShaders; data["GPU"]["EnableRenderdoc"] = enableRenderdoc; data["Audio"]["DSPEmulation"] = std::string(Audio::DSPCore::typeToString(dspType)); diff --git a/src/core/PICA/draw_acceleration.cpp b/src/core/PICA/draw_acceleration.cpp new file mode 100644 index 00000000..1850d819 --- /dev/null +++ b/src/core/PICA/draw_acceleration.cpp @@ -0,0 +1,148 @@ +#include "PICA/draw_acceleration.hpp" + +#include +#include + +#include "PICA/gpu.hpp" +#include "PICA/regs.hpp" + +void GPU::getAcceleratedDrawInfo(PICA::DrawAcceleration& accel, bool indexed) { + accel.indexed = indexed; + accel.totalAttribCount = totalAttribCount; + accel.enabledAttributeMask = 0; + + const u32 vertexBase = ((regs[PICA::InternalRegs::VertexAttribLoc] >> 1) & 0xfffffff) * 16; + const u32 vertexCount = regs[PICA::InternalRegs::VertexCountReg]; // Total # of vertices to transfer + + if (indexed) { + u32 indexBufferConfig = regs[PICA::InternalRegs::IndexBufferConfig]; + u32 indexBufferPointer = vertexBase + (indexBufferConfig & 0xfffffff); + + u8* indexBuffer = getPointerPhys(indexBufferPointer); + u16 minimumIndex = std::numeric_limits::max(); + u16 maximumIndex = 0; + + // Check whether the index buffer uses u16 indices or u8 + accel.useShortIndices = Helpers::getBit<31>(indexBufferConfig); // Indicates whether vert indices are 16-bit or 8-bit + + // Calculate the minimum and maximum indices used in the index buffer, so we'll only upload them + if (accel.useShortIndices) { + u16* indexBuffer16 = reinterpret_cast(indexBuffer); + + for (int i = 0; i < vertexCount; i++) { + u16 index = indexBuffer16[i]; + minimumIndex = std::min(minimumIndex, index); + maximumIndex = std::max(maximumIndex, index); + } + } else { + for (int i = 0; i < vertexCount; i++) { + u16 index = u16(indexBuffer[i]); + minimumIndex = std::min(minimumIndex, index); + maximumIndex = std::max(maximumIndex, index); + } + } + + accel.indexBuffer = indexBuffer; + accel.minimumIndex = minimumIndex; + accel.maximumIndex = maximumIndex; + } else { + accel.indexBuffer = nullptr; + accel.minimumIndex = regs[PICA::InternalRegs::VertexOffsetReg]; + accel.maximumIndex = accel.minimumIndex + vertexCount - 1; + } + + const u64 vertexCfg = u64(regs[PICA::InternalRegs::AttribFormatLow]) | (u64(regs[PICA::InternalRegs::AttribFormatHigh]) << 32); + const u64 inputAttrCfg = getVertexShaderInputConfig(); + + u32 attrCount = 0; + u32 loaderOffset = 0; + accel.vertexDataSize = 0; + accel.totalLoaderCount = 0; + + for (int i = 0; i < PICA::DrawAcceleration::maxLoaderCount; i++) { + auto& loaderData = attributeInfo[i]; // Get information for this attribute loader + + // This loader is empty, skip it + if (loaderData.componentCount == 0 || loaderData.size == 0) { + continue; + } + + auto& loader = accel.loaders[accel.totalLoaderCount++]; + + // The size of the loader in bytes is equal to the bytes supplied for 1 vertex, multiplied by the number of vertices we'll be uploading + // Which is equal to maximumIndex - minimumIndex + 1 + const u32 bytes = loaderData.size * (accel.maximumIndex - accel.minimumIndex + 1); + loader.size = bytes; + + // Add it to the total vertex data size, aligned to 4 bytes. + accel.vertexDataSize += (bytes + 3) & ~3; + + // Get a pointer to the data where this loader's data is stored + const u32 loaderAddress = vertexBase + loaderData.offset + (accel.minimumIndex * loaderData.size); + loader.data = getPointerPhys(loaderAddress); + + u64 attrCfg = loaderData.getConfigFull(); // Get config1 | (config2 << 32) + u32 attributeOffset = 0; + + for (int component = 0; component < loaderData.componentCount; component++) { + uint attributeIndex = (attrCfg >> (component * 4)) & 0xf; // Get index of attribute in vertexCfg + + // Vertex attributes used as padding + // 12, 13, 14 and 15 are equivalent to 4, 8, 12 and 16 bytes of padding respectively + if (attributeIndex >= 12) [[unlikely]] { + // Align attribute address up to a 4 byte boundary + attributeOffset = (attributeOffset + 3) & -4; + attributeOffset += (attributeIndex - 11) << 2; + continue; + } + + const u32 attribInfo = (vertexCfg >> (attributeIndex * 4)) & 0xf; + const u32 attribType = attribInfo & 0x3; // Type of attribute (sbyte/ubyte/short/float) + const u32 size = (attribInfo >> 2) + 1; // Total number of components + + // Size of each component based on the attribute type + static constexpr u32 sizePerComponent[4] = {1, 1, 2, 4}; + const u32 inputReg = (inputAttrCfg >> (attributeIndex * 4)) & 0xf; + // Mark the attribute as enabled + accel.enabledAttributeMask |= 1 << inputReg; + + auto& attr = accel.attributeInfo[inputReg]; + attr.componentCount = size; + attr.offset = attributeOffset + loaderOffset; + attr.stride = loaderData.size; + attr.type = attribType; + attributeOffset += size * sizePerComponent[attribType]; + } + + loaderOffset += loader.size; + } + + u32 fixedAttributes = fixedAttribMask; + accel.fixedAttributes = 0; + + // Fetch values for all fixed attributes using CLZ on the fixed attribute mask to find the attributes that are actually fixed + while (fixedAttributes != 0) { + // Get index of next fixed attribute and turn it off + const u32 index = std::countr_zero(fixedAttributes); + const u32 mask = 1u << index; + fixedAttributes ^= mask; + + // PICA register this fixed attribute is meant to go to + const u32 inputReg = (inputAttrCfg >> (index * 4)) & 0xf; + const u32 inputRegMask = 1u << inputReg; + + // If this input reg is already used for a non-fixed attribute then it will not be replaced by a fixed attribute + if ((accel.enabledAttributeMask & inputRegMask) == 0) { + vec4f& fixedAttr = shaderUnit.vs.fixedAttributes[index]; + auto& attr = accel.attributeInfo[inputReg]; + + accel.fixedAttributes |= inputRegMask; + + for (int i = 0; i < 4; i++) { + attr.fixedValue[i] = fixedAttr[i].toFloat32(); + } + } + } + + accel.canBeAccelerated = true; +} \ No newline at end of file diff --git a/src/core/PICA/gpu.cpp b/src/core/PICA/gpu.cpp index 3c4d4c5b..2624903f 100644 --- a/src/core/PICA/gpu.cpp +++ b/src/core/PICA/gpu.cpp @@ -117,37 +117,62 @@ void GPU::reset() { externalRegs[Framebuffer1Config] = static_cast(PICA::ColorFmt::RGB8); externalRegs[Framebuffer1Select] = 0; - renderer->setUbershaderSetting(config.useUbershaders); renderer->reset(); } -// Call the correct version of drawArrays based on whether this is an indexed draw (first template parameter) -// And whether we are going to use the shader JIT (second template parameter) -void GPU::drawArrays(bool indexed) { - const bool shaderJITEnabled = ShaderJIT::isAvailable() && config.shaderJitEnabled; - - if (indexed) { - if (shaderJITEnabled) - drawArrays(); - else - drawArrays(); - } else { - if (shaderJITEnabled) - drawArrays(); - else - drawArrays(); - } -} - static std::array vertices; -template -void GPU::drawArrays() { - if constexpr (useShaderJIT) { - shaderJIT.prepare(shaderUnit.vs); +// Call the correct version of drawArrays based on whether this is an indexed draw (first template parameter) +// And whether we are going to use the shader JIT (second template parameter) +void GPU::drawArrays(bool indexed) { + PICA::DrawAcceleration accel; + + if (config.accelerateShaders) { + // If we are potentially going to use hw shaders, gather necessary to do vertex fetch, index buffering, etc on the GPU + // This includes parsing which vertices to upload, getting pointers to the index buffer data & vertex data, and so on + getAcceleratedDrawInfo(accel, indexed); } - setVsOutputMask(regs[PICA::InternalRegs::VertexShaderOutputMask]); + const bool hwShaders = renderer->prepareForDraw(shaderUnit, &accel); + + if (hwShaders) { + // Hardware shaders have their own accelerated code path for draws, so they skip everything here + const PICA::PrimType primType = static_cast(Helpers::getBits<8, 2>(regs[PICA::InternalRegs::PrimitiveConfig])); + // Total # of vertices to render + const u32 vertexCount = regs[PICA::InternalRegs::VertexCountReg]; + + // Note: In the hardware shader path the vertices span shouldn't actually be used as the renderer will perform its own attribute fetching + renderer->drawVertices(primType, std::span(vertices).first(vertexCount)); + } else { + const bool shaderJITEnabled = ShaderJIT::isAvailable() && config.shaderJitEnabled; + + if (indexed) { + if (shaderJITEnabled) { + drawArrays(); + } else { + drawArrays(); + } + } else { + if (shaderJITEnabled) { + drawArrays(); + } else { + drawArrays(); + } + } + } +} + +template +void GPU::drawArrays() { + if constexpr (mode == ShaderExecMode::JIT) { + shaderJIT.prepare(shaderUnit.vs); + } else if constexpr (mode == ShaderExecMode::Hardware) { + // Hardware shaders have their own accelerated code path for draws, so they're not meant to take this path + Helpers::panic("GPU::DrawArrays: Hardware shaders shouldn't take this path!"); + } + + // We can have up to 16 attributes, each one consisting of 4 floats + constexpr u32 maxAttrSizeInFloats = 16 * 4; // Base address for vertex attributes // The vertex base is always on a quadword boundary because the PICA does weird alignment shit any time possible @@ -312,8 +337,6 @@ void GPU::drawArrays() { } // Fill the remaining attribute lanes with default parameters (1.0 for alpha/w, 0.0) for everything else - // Corgi does this although I'm not sure if it's actually needed for anything. - // TODO: Find out while (component < 4) { attribute[component] = (component == 3) ? f24::fromFloat32(1.0) : f24::fromFloat32(0.0); component++; @@ -327,13 +350,13 @@ void GPU::drawArrays() { // Before running the shader, the PICA maps the fetched attributes from the attribute registers to the shader input registers // Based on the SH_ATTRIBUTES_PERMUTATION registers. - // Ie it might attribute #0 to v2, #1 to v7, etc + // Ie it might map attribute #0 to v2, #1 to v7, etc for (int j = 0; j < totalAttribCount; j++) { const u32 mapping = (inputAttrCfg >> (j * 4)) & 0xf; std::memcpy(&shaderUnit.vs.inputs[mapping], ¤tAttributes[j], sizeof(vec4f)); } - if constexpr (useShaderJIT) { + if constexpr (mode == ShaderExecMode::JIT) { shaderJIT.run(shaderUnit.vs); } else { shaderUnit.vs.run(); diff --git a/src/core/PICA/regs.cpp b/src/core/PICA/regs.cpp index f805de60..4c865d12 100644 --- a/src/core/PICA/regs.cpp +++ b/src/core/PICA/regs.cpp @@ -249,6 +249,7 @@ void GPU::writeInternalReg(u32 index, u32 value, u32 mask) { // If we've reached 3 verts, issue a draw call // Handle rendering depending on the primitive type if (immediateModeVertIndex == 3) { + renderer->prepareForDraw(shaderUnit, nullptr); renderer->drawVertices(PICA::PrimType::TriangleList, immediateModeVertices); switch (primType) { @@ -300,7 +301,7 @@ void GPU::writeInternalReg(u32 index, u32 value, u32 mask) { } case VertexBoolUniform: { - shaderUnit.vs.boolUniform = value & 0xffff; + shaderUnit.vs.uploadBoolUniform(value & 0xffff); break; } diff --git a/src/core/PICA/shader_decompiler.cpp b/src/core/PICA/shader_decompiler.cpp index 482aa36c..467c4727 100644 --- a/src/core/PICA/shader_decompiler.cpp +++ b/src/core/PICA/shader_decompiler.cpp @@ -1,5 +1,10 @@ #include "PICA/shader_decompiler.hpp" +#include + +#include +#include + #include "config.hpp" using namespace PICA; @@ -13,11 +18,45 @@ void ControlFlow::analyze(const PICAShader& shader, u32 entrypoint) { analysisFailed = false; const Function* function = addFunction(shader, entrypoint, PICAShader::maxInstructionCount); - if (function == nullptr) { + if (function == nullptr || function->exitMode != ExitMode::AlwaysEnd) { analysisFailed = true; } } +// Helpers for merging parallel/series exit methods from Citra +// Merges exit method of two parallel branches. +static ExitMode exitParallel(ExitMode a, ExitMode b) { + if (a == ExitMode::Unknown) { + return b; + } + else if (b == ExitMode::Unknown) { + return a; + } + else if (a == b) { + return a; + } + return ExitMode::Conditional; +} + +// Cascades exit method of two blocks of code. +static ExitMode exitSeries(ExitMode a, ExitMode b) { + assert(a != ExitMode::AlwaysEnd); + + if (a == ExitMode::Unknown) { + return ExitMode::Unknown; + } + + if (a == ExitMode::AlwaysReturn) { + return b; + } + + if (b == ExitMode::Unknown || b == ExitMode::AlwaysEnd) { + return ExitMode::AlwaysEnd; + } + + return ExitMode::Conditional; +} + ExitMode ControlFlow::analyzeFunction(const PICAShader& shader, u32 start, u32 end, Function::Labels& labels) { // Initialize exit mode to unknown by default, in order to detect things like unending loops auto [it, inserted] = exitMap.emplace(AddressRange(start, end), ExitMode::Unknown); @@ -32,25 +71,132 @@ ExitMode ControlFlow::analyzeFunction(const PICAShader& shader, u32 start, u32 e const u32 opcode = instruction >> 26; switch (opcode) { - case ShaderOpcodes::JMPC: Helpers::panic("Unimplemented control flow operation (JMPC)"); - case ShaderOpcodes::JMPU: Helpers::panic("Unimplemented control flow operation (JMPU)"); - case ShaderOpcodes::IFU: Helpers::panic("Unimplemented control flow operation (IFU)"); - case ShaderOpcodes::IFC: Helpers::panic("Unimplemented control flow operation (IFC)"); - case ShaderOpcodes::CALL: Helpers::panic("Unimplemented control flow operation (CALL)"); - case ShaderOpcodes::CALLC: Helpers::panic("Unimplemented control flow operation (CALLC)"); - case ShaderOpcodes::CALLU: Helpers::panic("Unimplemented control flow operation (CALLU)"); - case ShaderOpcodes::LOOP: Helpers::panic("Unimplemented control flow operation (LOOP)"); - case ShaderOpcodes::END: it->second = ExitMode::AlwaysEnd; return it->second; + case ShaderOpcodes::JMPC: + case ShaderOpcodes::JMPU: { + const u32 dest = getBits<10, 12>(instruction); + // Register this jump address to our outLabels set + labels.insert(dest); + // This opens up 2 parallel paths of execution + auto branchTakenExit = analyzeFunction(shader, dest, end, labels); + auto branchNotTakenExit = analyzeFunction(shader, pc + 1, end, labels); + it->second = exitParallel(branchTakenExit, branchNotTakenExit); + return it->second; + } + + case ShaderOpcodes::IFU: + case ShaderOpcodes::IFC: { + const u32 num = instruction & 0xff; + const u32 dest = getBits<10, 12>(instruction); + + const Function* branchTakenFunc = addFunction(shader, pc + 1, dest); + // Check if analysis of the branch taken func failed and return unknown if it did + if (analysisFailed) { + it->second = ExitMode::Unknown; + return it->second; + } + + // Next analyze the not taken func + ExitMode branchNotTakenExitMode = ExitMode::AlwaysReturn; + if (num != 0) { + const Function* branchNotTakenFunc = addFunction(shader, dest, dest + num); + // Check if analysis failed and return unknown if it did + if (analysisFailed) { + it->second = ExitMode::Unknown; + return it->second; + } + + branchNotTakenExitMode = branchNotTakenFunc->exitMode; + } + + auto parallel = exitParallel(branchTakenFunc->exitMode, branchNotTakenExitMode); + // Both branches of the if/else end, so there's nothing after the call + if (parallel == ExitMode::AlwaysEnd) { + it->second = parallel; + return it->second; + } else { + ExitMode afterConditional = analyzeFunction(shader, dest + num, end, labels); + ExitMode conditionalExitMode = exitSeries(parallel, afterConditional); + it->second = conditionalExitMode; + return it->second; + } + break; + } + + case ShaderOpcodes::CALL: { + const u32 num = instruction & 0xff; + const u32 dest = getBits<10, 12>(instruction); + const Function* calledFunction = addFunction(shader, dest, dest + num); + + // Check if analysis of the branch taken func failed and return unknown if it did + if (analysisFailed) { + it->second = ExitMode::Unknown; + return it->second; + } + + if (calledFunction->exitMode == ExitMode::AlwaysEnd) { + it->second = ExitMode::AlwaysEnd; + return it->second; + } + + // Exit mode of the remainder of this function, after we return from the callee + const ExitMode postCallExitMode = analyzeFunction(shader, pc + 1, end, labels); + const ExitMode exitMode = exitSeries(calledFunction->exitMode, postCallExitMode); + + it->second = exitMode; + return exitMode; + } + + case ShaderOpcodes::CALLC: + case ShaderOpcodes::CALLU: { + const u32 num = instruction & 0xff; + const u32 dest = getBits<10, 12>(instruction); + const Function* calledFunction = addFunction(shader, dest, dest + num); + + // Check if analysis of the branch taken func failed and return unknown if it did + if (analysisFailed) { + it->second = ExitMode::Unknown; + return it->second; + } + + // Exit mode of the remainder of this function, after we return from the callee + const ExitMode postCallExitMode = analyzeFunction(shader, pc + 1, end, labels); + const ExitMode exitMode = exitSeries(exitParallel(calledFunction->exitMode, ExitMode::AlwaysReturn), postCallExitMode); + + it->second = exitMode; + return exitMode; + } + + case ShaderOpcodes::LOOP: { + u32 dest = getBits<10, 12>(instruction); + const Function* loopFunction = addFunction(shader, pc + 1, dest + 1); + if (analysisFailed) { + it->second = ExitMode::Unknown; + return it->second; + } + + if (loopFunction->exitMode == ExitMode::AlwaysEnd) { + it->second = ExitMode::AlwaysEnd; + return it->second; + } + + const ExitMode afterLoop = analyzeFunction(shader, dest + 1, end, labels); + const ExitMode exitMode = exitSeries(loopFunction->exitMode, afterLoop); + it->second = exitMode; + return it->second; + } + + case ShaderOpcodes::END: it->second = ExitMode::AlwaysEnd; return it->second; default: break; } } // A function without control flow instructions will always reach its "return point" and return - return ExitMode::AlwaysReturn; + it->second = ExitMode::AlwaysReturn; + return it->second; } -void ShaderDecompiler::compileRange(const AddressRange& range) { +std::pair ShaderDecompiler::compileRange(const AddressRange& range) { u32 pc = range.start; const u32 end = range.end >= range.start ? range.end : PICAShader::maxInstructionCount; bool finished = false; @@ -58,6 +204,8 @@ void ShaderDecompiler::compileRange(const AddressRange& range) { while (pc < end && !finished) { compileInstruction(pc, finished); } + + return std::make_pair(pc, finished); } const Function* ShaderDecompiler::findFunction(const AddressRange& range) { @@ -71,20 +219,43 @@ const Function* ShaderDecompiler::findFunction(const AddressRange& range) { } void ShaderDecompiler::writeAttributes() { + // Annoyingly, GLES does not support having an array as an input attribute, so declare each attribute separately for now decompiledShader += R"( - layout(location = 0) in vec4 inputs[8]; + layout(location = 0) in vec4 attr0; + layout(location = 1) in vec4 attr1; + layout(location = 2) in vec4 attr2; + layout(location = 3) in vec4 attr3; + layout(location = 4) in vec4 attr4; + layout(location = 5) in vec4 attr5; + layout(location = 6) in vec4 attr6; + layout(location = 7) in vec4 attr7; + layout(location = 8) in vec4 attr8; + layout(location = 9) in vec4 attr9; + layout(location = 10) in vec4 attr10; + layout(location = 11) in vec4 attr11; + layout(location = 12) in vec4 attr12; + layout(location = 13) in vec4 attr13; + layout(location = 14) in vec4 attr14; + layout(location = 15) in vec4 attr15; - layout(std140) uniform PICAShaderUniforms { - vec4 uniform_float[96]; - uvec4 uniform_int; - uint uniform_bool; - }; - - vec4 temp_registers[16]; - vec4 dummy_vec = vec4(0.0); + layout(std140) uniform PICAShaderUniforms { + vec4 uniform_f[96]; + uvec4 uniform_i; + uint uniform_bool; + }; + + vec4 temp[16]; + vec4 out_regs[16]; + vec4 dummy_vec = vec4(0.0); + ivec3 addr_reg = ivec3(0); + bvec2 cmp_reg = bvec2(false); + + vec4 uniform_indexed(int source, int offset) { + int clipped_offs = (offset >= -128 && offset <= 127) ? offset : 0; + uint index = uint(clipped_offs + source) & 127u; + return (index < 96u) ? uniform_f[index] : vec4(1.0); + } )"; - - decompiledShader += "\n"; } std::string ShaderDecompiler::decompile() { @@ -94,11 +265,14 @@ std::string ShaderDecompiler::decompile() { return ""; } - decompiledShader = ""; + compilationError = false; + decompiledShader.clear(); + // Reserve some memory for the shader string to avoid memory allocations + decompiledShader.reserve(256 * 1024); switch (api) { case API::GL: decompiledShader += "#version 410 core\n"; break; - case API::GLES: decompiledShader += "#version 300 es\n"; break; + case API::GLES: decompiledShader += "#version 300 es\nprecision mediump float;\nprecision mediump int;\n"; break; default: break; } @@ -109,7 +283,7 @@ std::string ShaderDecompiler::decompile() { decompiledShader += R"( vec4 safe_mul(vec4 a, vec4 b) { vec4 res = a * b; - return mix(res, mix(mix(vec4(0.0), res, isnan(rhs)), product, isnan(lhs)), isnan(res)); + return mix(res, mix(mix(vec4(0.0), res, isnan(b)), res, isnan(a)), isnan(res)); } )"; } @@ -121,17 +295,61 @@ std::string ShaderDecompiler::decompile() { decompiledShader += "void pica_shader_main() {\n"; AddressRange mainFunctionRange(entrypoint, PICAShader::maxInstructionCount); - callFunction(*findFunction(mainFunctionRange)); - decompiledShader += "}\n"; + auto mainFunc = findFunction(mainFunctionRange); - for (auto& func : controlFlow.functions) { - if (func.outLabels.size() > 0) { - Helpers::panic("Function with out labels"); + decompiledShader += mainFunc->getCallStatement() + ";\n}\n"; + + for (const Function& func : controlFlow.functions) { + if (func.outLabels.empty()) { + decompiledShader += fmt::format("bool {}() {{\n", func.getIdentifier()); + + auto [pc, finished] = compileRange(AddressRange(func.start, func.end)); + if (!finished) { + decompiledShader += "return false;"; + } + + decompiledShader += "}\n"; + } else { + auto labels = func.outLabels; + labels.insert(func.start); + + // If a function has jumps and "labels", this needs to be emulated using a switch-case, with the variable being switched on being the + // current PC + decompiledShader += fmt::format("bool {}() {{\n", func.getIdentifier()); + decompiledShader += fmt::format("uint pc = {}u;\n", func.start); + decompiledShader += "while(true){\nswitch(pc){\n"; + + for (u32 label : labels) { + decompiledShader += fmt::format("case {}u: {{", label); + // Fetch the next label whose address > label + auto it = labels.lower_bound(label + 1); + u32 next = (it == labels.end()) ? func.end : *it; + + auto [endPC, finished] = compileRange(AddressRange(label, next)); + if (endPC > next && !finished) { + labels.insert(endPC); + decompiledShader += fmt::format("pc = {}u; break;", endPC); + } + + // Fallthrough to next label + decompiledShader += "}\n"; + } + + decompiledShader += "default: return false;\n"; + // Exit the switch and loop + decompiledShader += "} }\n"; + + // Exit the function + decompiledShader += "return false;\n"; + decompiledShader += "}\n"; } + } - decompiledShader += "void " + func.getIdentifier() + "() {\n"; - compileRange(AddressRange(func.start, func.end)); - decompiledShader += "}\n"; + // We allow some leeway for "compilation errors" in addition to control flow errors, in cases where eg an unimplemented instruction + // or an instruction that we can't emulate in GLSL is found in the instruction stream. Just like control flow errors, these return an empty string + // and the renderer core will decide to use CPU shaders instead + if (compilationError) [[unlikely]] { + return ""; } return decompiledShader; @@ -139,30 +357,41 @@ std::string ShaderDecompiler::decompile() { std::string ShaderDecompiler::getSource(u32 source, [[maybe_unused]] u32 index) const { if (source < 0x10) { - return "inputs[" + std::to_string(source) + "]"; + return "attr" + std::to_string(source); } else if (source < 0x20) { - return "temp_registers[" + std::to_string(source - 0x10) + "]"; + return "temp[" + std::to_string(source - 0x10) + "]"; } else { const usize floatIndex = (source - 0x20) & 0x7f; - if (floatIndex >= 96) [[unlikely]] { - return "dummy_vec"; + if (index == 0) { + if (floatIndex >= 96) [[unlikely]] { + return "dummy_vec"; + } + return "uniform_f[" + std::to_string(floatIndex) + "]"; + } else { + static constexpr std::array offsets = {"0", "addr_reg.x", "addr_reg.y", "addr_reg.z"}; + return fmt::format("uniform_indexed({}, {})", floatIndex, offsets[index]); } - return "uniform_float[" + std::to_string(floatIndex) + "]"; } } std::string ShaderDecompiler::getDest(u32 dest) const { if (dest < 0x10) { - return "output_registers[" + std::to_string(dest) + "]"; + return "out_regs[" + std::to_string(dest) + "]"; } else if (dest < 0x20) { - return "temp_registers[" + std::to_string(dest - 0x10) + "]"; + return "temp[" + std::to_string(dest - 0x10) + "]"; } else { return "dummy_vec"; } } std::string ShaderDecompiler::getSwizzlePattern(u32 swizzle) const { + // If the swizzle field is this value then the swizzle pattern is .xyzw so we don't need a shuffle + static constexpr uint noSwizzle = 0x1B; + if (swizzle == noSwizzle) { + return ""; + } + static constexpr std::array names = {'x', 'y', 'z', 'w'}; std::string ret(". "); @@ -176,7 +405,6 @@ std::string ShaderDecompiler::getSwizzlePattern(u32 swizzle) const { std::string ShaderDecompiler::getDestSwizzle(u32 destinationMask) const { std::string ret = "."; - if (destinationMask & 0b1000) { ret += "x"; } @@ -208,11 +436,12 @@ void ShaderDecompiler::setDest(u32 operandDescriptor, const std::string& dest, c return; } - decompiledShader += dest + destSwizzle + " = "; - if (writtenLaneCount == 1) { - decompiledShader += "float(" + value + ");\n"; - } else { - decompiledShader += "vec" + std::to_string(writtenLaneCount) + "(" + value + ");\n"; + // Don't write destination swizzle if all lanes are getting written to + decompiledShader += fmt::format("{}{} = ", dest, writtenLaneCount == 4 ? "" : destSwizzle); + if (writtenLaneCount <= 3) { + decompiledShader += fmt::format("({}){};\n", value, destSwizzle); + } else if (writtenLaneCount == 4) { + decompiledShader += fmt::format("{};\n", value); } } @@ -246,26 +475,101 @@ void ShaderDecompiler::compileInstruction(u32& pc, bool& finished) { std::string dest = getDest(destIndex); - if (idx != 0) { - Helpers::panic("GLSL recompiler: Indexed instruction"); - } - - if (invertSources) { - Helpers::panic("GLSL recompiler: Inverted instruction"); - } - switch (opcode) { case ShaderOpcodes::MOV: setDest(operandDescriptor, dest, src1); break; - case ShaderOpcodes::ADD: setDest(operandDescriptor, dest, src1 + " + " + src2); break; - case ShaderOpcodes::MUL: setDest(operandDescriptor, dest, src1 + " * " + src2); break; - case ShaderOpcodes::MAX: setDest(operandDescriptor, dest, "max(" + src1 + ", " + src2 + ")"); break; - case ShaderOpcodes::MIN: setDest(operandDescriptor, dest, "min(" + src1 + ", " + src2 + ")"); break; + case ShaderOpcodes::ADD: setDest(operandDescriptor, dest, fmt::format("{} + {}", src1, src2)); break; + case ShaderOpcodes::MUL: + if (!config.accurateShaderMul) { + setDest(operandDescriptor, dest, fmt::format("{} * {}", src1, src2)); + } else { + setDest(operandDescriptor, dest, fmt::format("safe_mul({}, {})", src1, src2)); + } + break; + case ShaderOpcodes::MAX: setDest(operandDescriptor, dest, fmt::format("max({}, {})", src1, src2)); break; + case ShaderOpcodes::MIN: setDest(operandDescriptor, dest, fmt::format("min({}, {})", src1, src2)); break; - case ShaderOpcodes::DP3: setDest(operandDescriptor, dest, "vec4(dot(" + src1 + ".xyz, " + src2 + ".xyz))"); break; - case ShaderOpcodes::DP4: setDest(operandDescriptor, dest, "vec4(dot(" + src1 + ", " + src2 + "))"); break; - case ShaderOpcodes::RSQ: setDest(operandDescriptor, dest, "vec4(inversesqrt(" + src1 + ".x))"); break; + case ShaderOpcodes::DP3: + if (!config.accurateShaderMul) { + setDest(operandDescriptor, dest, fmt::format("vec4(dot({}.xyz, {}.xyz))", src1, src2)); + } else { + // A dot product between a and b is equivalent to the per-lane multiplication of a and b followed by a dot product with vec3(1.0) + setDest(operandDescriptor, dest, fmt::format("vec4(dot(safe_mul({}, {}).xyz, vec3(1.0)))", src1, src2)); + } + break; + case ShaderOpcodes::DP4: + if (!config.accurateShaderMul) { + setDest(operandDescriptor, dest, fmt::format("vec4(dot({}, {}))", src1, src2)); + } else { + // A dot product between a and b is equivalent to the per-lane multiplication of a and b followed by a dot product with vec4(1.0) + setDest(operandDescriptor, dest, fmt::format("vec4(dot(safe_mul({}, {}), vec4(1.0)))", src1, src2)); + } + break; + case ShaderOpcodes::FLR: setDest(operandDescriptor, dest, fmt::format("floor({})", src1)); break; + case ShaderOpcodes::RSQ: setDest(operandDescriptor, dest, fmt::format("vec4(inversesqrt({}.x))", src1)); break; + case ShaderOpcodes::RCP: setDest(operandDescriptor, dest, fmt::format("vec4(1.0 / {}.x)", src1)); break; + case ShaderOpcodes::LG2: setDest(operandDescriptor, dest, fmt::format("vec4(log2({}.x))", src1)); break; + case ShaderOpcodes::EX2: setDest(operandDescriptor, dest, fmt::format("vec4(exp2({}.x))", src1)); break; - default: Helpers::panic("GLSL recompiler: Unknown common opcode: %X", opcode); break; + case ShaderOpcodes::SLT: + case ShaderOpcodes::SLTI: setDest(operandDescriptor, dest, fmt::format("vec4(lessThan({}, {}))", src1, src2)); break; + + case ShaderOpcodes::SGE: + case ShaderOpcodes::SGEI: setDest(operandDescriptor, dest, fmt::format("vec4(greaterThanEqual({}, {}))", src1, src2)); break; + + case ShaderOpcodes::DPH: + case ShaderOpcodes::DPHI: + if (!config.accurateShaderMul) { + setDest(operandDescriptor, dest, fmt::format("vec4(dot(vec4({}.xyz, 1.0), {}))", src1, src2)); + } else { + // A dot product between a and b is equivalent to the per-lane multiplication of a and b followed by a dot product with vec4(1.0) + setDest(operandDescriptor, dest, fmt::format("vec4(dot(safe_mul(vec4({}.xyz, 1.0), {}), vec4(1.0)))", src1, src2)); + } + break; + + case ShaderOpcodes::CMP1: + case ShaderOpcodes::CMP2: { + static constexpr std::array operators = { + // The last 2 operators always return true and are handled specially + "==", "!=", "<", "<=", ">", ">=", "", "", + }; + + const u32 cmpY = getBits<21, 3>(instruction); + const u32 cmpX = getBits<24, 3>(instruction); + + // Compare x first + if (cmpX >= 6) { + decompiledShader += "cmp_reg.x = true;\n"; + } else { + decompiledShader += fmt::format("cmp_reg.x = {}.x {} {}.x;\n", src1, operators[cmpX], src2); + } + + // Then compare Y + if (cmpY >= 6) { + decompiledShader += "cmp_reg.y = true;\n"; + } else { + decompiledShader += fmt::format("cmp_reg.y = {}.y {} {}.y;\n", src1, operators[cmpY], src2); + } + break; + } + + case ShaderOpcodes::MOVA: { + const bool writeX = getBit<3>(operandDescriptor); // Should we write the x component of the address register? + const bool writeY = getBit<2>(operandDescriptor); + + if (writeX && writeY) { + decompiledShader += fmt::format("addr_reg.xy = ivec2({}.xy);\n", src1); + } else if (writeX) { + decompiledShader += fmt::format("addr_reg.x = int({}.x);\n", src1); + } else if (writeY) { + decompiledShader += fmt::format("addr_reg.y = int({}.y);\n", src1); + } + break; + } + + default: + Helpers::warn("GLSL recompiler: Unknown common opcode: %02X. Falling back to CPU shaders", opcode); + compilationError = true; + break; } } else if (opcode >= 0x30 && opcode <= 0x3F) { // MAD and MADI const u32 operandDescriptor = shader.operandDescriptors[instruction & 0x1f]; @@ -299,23 +603,156 @@ void ShaderDecompiler::compileInstruction(u32& pc, bool& finished) { src3 += getSwizzlePattern(swizzle3); std::string dest = getDest(destIndex); - - if (idx != 0) { - Helpers::panic("GLSL recompiler: Indexed instruction"); + if (!config.accurateShaderMul) { + setDest(operandDescriptor, dest, fmt::format("{} * {} + {}", src1, src2, src3)); + } else { + setDest(operandDescriptor, dest, fmt::format("safe_mul({}, {}) + {}", src1, src2, src3)); } - - setDest(operandDescriptor, dest, src1 + " * " + src2 + " + " + src3); } else { switch (opcode) { - case ShaderOpcodes::END: finished = true; return; - default: Helpers::panic("GLSL recompiler: Unknown opcode: %X", opcode); break; + case ShaderOpcodes::JMPC: { + const u32 dest = getBits<10, 12>(instruction); + const u32 condOp = getBits<22, 2>(instruction); + const uint refY = getBit<24>(instruction); + const uint refX = getBit<25>(instruction); + const char* condition = getCondition(condOp, refX, refY); + + decompiledShader += fmt::format("if ({}) {{ pc = {}u; break; }}\n", condition, dest); + break; + } + + case ShaderOpcodes::JMPU: { + const u32 dest = getBits<10, 12>(instruction); + const u32 bit = getBits<22, 4>(instruction); // Bit of the bool uniform to check + const u32 mask = 1u << bit; + const u32 test = (instruction & 1) ^ 1; // If the LSB is 0 we jump if bit = 1, otherwise 0 + + decompiledShader += fmt::format("if ((uniform_bool & {}u) {} 0u) {{ pc = {}u; break; }}\n", mask, (test != 0) ? "!=" : "==", dest); + break; + } + + case ShaderOpcodes::IFU: + case ShaderOpcodes::IFC: { + const u32 num = instruction & 0xff; + const u32 dest = getBits<10, 12>(instruction); + const Function* conditionalFunc = findFunction(AddressRange(pc + 1, dest)); + + if (opcode == ShaderOpcodes::IFC) { + const u32 condOp = getBits<22, 2>(instruction); + const uint refY = getBit<24>(instruction); + const uint refX = getBit<25>(instruction); + const char* condition = getCondition(condOp, refX, refY); + + decompiledShader += fmt::format("if ({}) {{", condition); + } else { + const u32 bit = getBits<22, 4>(instruction); // Bit of the bool uniform to check + const u32 mask = 1u << bit; + + decompiledShader += fmt::format("if ((uniform_bool & {}u) != 0u) {{", mask); + } + + callFunction(*conditionalFunc); + decompiledShader += "}\n"; + + pc = dest; + if (num > 0) { + const Function* elseFunc = findFunction(AddressRange(dest, dest + num)); + pc = dest + num; + + decompiledShader += "else { "; + callFunction(*elseFunc); + decompiledShader += "}\n"; + + if (conditionalFunc->exitMode == ExitMode::AlwaysEnd && elseFunc->exitMode == ExitMode::AlwaysEnd) { + finished = true; + return; + } + } + + return; + } + + case ShaderOpcodes::CALL: + case ShaderOpcodes::CALLC: + case ShaderOpcodes::CALLU: { + const u32 num = instruction & 0xff; + const u32 dest = getBits<10, 12>(instruction); + const Function* calledFunc = findFunction(AddressRange(dest, dest + num)); + + // Handle conditions for CALLC/CALLU + if (opcode == ShaderOpcodes::CALLC) { + const u32 condOp = getBits<22, 2>(instruction); + const uint refY = getBit<24>(instruction); + const uint refX = getBit<25>(instruction); + const char* condition = getCondition(condOp, refX, refY); + + decompiledShader += fmt::format("if ({}) {{", condition); + } else if (opcode == ShaderOpcodes::CALLU) { + const u32 bit = getBits<22, 4>(instruction); // Bit of the bool uniform to check + const u32 mask = 1u << bit; + + decompiledShader += fmt::format("if ((uniform_bool & {}u) != 0u) {{", mask); + } + + callFunction(*calledFunc); + + // Close brackets for CALLC/CALLU + if (opcode != ShaderOpcodes::CALL) { + decompiledShader += "}"; + } + + if (opcode == ShaderOpcodes::CALL && calledFunc->exitMode == ExitMode::AlwaysEnd) { + finished = true; + return; + } + break; + } + + case ShaderOpcodes::LOOP: { + const u32 dest = getBits<10, 12>(instruction); + const u32 uniformIndex = getBits<22, 2>(instruction); + + // loop counter = uniform.y + decompiledShader += fmt::format("addr_reg.z = int((uniform_i[{}] >> 8u) & 0xFFu);\n", uniformIndex); + decompiledShader += fmt::format( + "for (uint loopCtr{} = 0u; loopCtr{} <= (uniform_i[{}] & 0xFFu); loopCtr{}++, addr_reg.z += int((uniform_i[{}] >> " + "16u) & 0xFFu)) {{\n", + pc, pc, uniformIndex, pc, uniformIndex + ); + + AddressRange range(pc + 1, dest + 1); + const Function* func = findFunction(range); + callFunction(*func); + decompiledShader += "}\n"; + + // Jump to the end of the loop. We don't want to compile the code inside the loop again. + // This will be incremented by 1 due to the pc++ at the end of this loop. + pc = dest; + + if (func->exitMode == ExitMode::AlwaysEnd) { + finished = true; + return; + } + break; + } + + case ShaderOpcodes::END: + decompiledShader += "return true;\n"; + finished = true; + return; + + case ShaderOpcodes::NOP: break; + + default: + Helpers::warn("GLSL recompiler: Unknown opcode: %02X. Falling back to CPU shaders", opcode); + compilationError = true; + break; } } pc++; } - bool ShaderDecompiler::usesCommonEncoding(u32 instruction) const { const u32 opcode = instruction >> 26; switch (opcode) { @@ -339,16 +776,57 @@ bool ShaderDecompiler::usesCommonEncoding(u32 instruction) const { case ShaderOpcodes::SLT: case ShaderOpcodes::SLTI: case ShaderOpcodes::SGE: - case ShaderOpcodes::SGEI: return true; + case ShaderOpcodes::SGEI: + case ShaderOpcodes::LITP: return true; default: return false; } } -void ShaderDecompiler::callFunction(const Function& function) { decompiledShader += function.getCallStatement() + ";\n"; } +void ShaderDecompiler::callFunction(const Function& function) { + switch (function.exitMode) { + // This function always ends, so call it and return true to signal that we're gonna be ending the shader + case ExitMode::AlwaysEnd: decompiledShader += function.getCallStatement() + ";\nreturn true;\n"; break; + // This function will potentially end. Call it, see if it returns that it ended, and return that we're ending if it did + case ExitMode::Conditional: decompiledShader += fmt::format("if ({}) {{ return true; }}\n", function.getCallStatement()); break; + // This function will not end. Just call it like a normal function. + default: decompiledShader += function.getCallStatement() + ";\n"; break; + } +} std::string ShaderGen::decompileShader(PICAShader& shader, EmulatorConfig& config, u32 entrypoint, API api, Language language) { ShaderDecompiler decompiler(shader, config, entrypoint, api, language); return decompiler.decompile(); } + +const char* ShaderDecompiler::getCondition(u32 cond, u32 refX, u32 refY) { + static constexpr std::array conditions = { + // ref(Y, X) = (0, 0) + "!all(cmp_reg)", + "all(not(cmp_reg))", + "!cmp_reg.x", + "!cmp_reg.y", + + // ref(Y, X) = (0, 1) + "cmp_reg.x || !cmp_reg.y", + "cmp_reg.x && !cmp_reg.y", + "cmp_reg.x", + "!cmp_reg.y", + + // ref(Y, X) = (1, 0) + "!cmp_reg.x || cmp_reg.y", + "!cmp_reg.x && cmp_reg.y", + "!cmp_reg.x", + "cmp_reg.y", + + // ref(Y, X) = (1, 1) + "any(cmp_reg)", + "all(cmp_reg)", + "cmp_reg.x", + "cmp_reg.y", + }; + const u32 key = (cond & 0b11) | (refX << 2) | (refY << 3); + + return conditions[key]; +} diff --git a/src/core/PICA/shader_gen_glsl.cpp b/src/core/PICA/shader_gen_glsl.cpp index 13d5aa58..44a75134 100644 --- a/src/core/PICA/shader_gen_glsl.cpp +++ b/src/core/PICA/shader_gen_glsl.cpp @@ -1,3 +1,7 @@ +#include + +#include + #include "PICA/pica_frag_config.hpp" #include "PICA/regs.hpp" #include "PICA/shader_gen.hpp" @@ -702,6 +706,113 @@ void FragmentGenerator::compileFog(std::string& shader, const PICA::FragmentConf shader += "combinerOutput.rgb = mix(fog_color, combinerOutput.rgb, fog_factor);"; } +std::string FragmentGenerator::getVertexShaderAccelerated(const std::string& picaSource, const PICA::VertConfig& vertConfig, bool usingUbershader) { + // First, calculate output register -> Fixed function fragment semantics based on the VAO config + // This array contains the mappings for the 32 fixed function semantics (8 variables, with 4 lanes each). + // Each entry is a pair, containing the output reg to use for this semantic (first) and which lane of that register (second) + std::array, 32> outputMappings{}; + // Output registers adjusted according to VS_OUTPUT_MASK, which handles enabling and disabling output attributes + std::array vsOutputRegisters; + + { + uint count = 0; + u16 outputMask = vertConfig.outputMask; + + // See which registers are actually enabled and ignore the disabled ones + for (int i = 0; i < 16; i++) { + if (outputMask & 1) { + vsOutputRegisters[count++] = i; + } + + outputMask >>= 1; + } + + // For the others, map the index to a vs output directly (TODO: What does hw actually do?) + for (; count < 16; count++) { + vsOutputRegisters[count] = count; + } + + for (int i = 0; i < vertConfig.outputCount; i++) { + const u32 config = vertConfig.outmaps[i]; + for (int j = 0; j < 4; j++) { + const u32 mapping = (config >> (j * 8)) & 0x1F; + outputMappings[mapping] = std::make_pair(vsOutputRegisters[i], j); + } + } + } + + auto getSemanticName = [&](u32 semanticIndex) { + auto [reg, lane] = outputMappings[semanticIndex]; + return fmt::format("out_regs[{}][{}]", reg, lane); + }; + + std::string semantics = fmt::format( + R"( + vec4 a_coords = vec4({}, {}, {}, {}); + vec4 a_quaternion = vec4({}, {}, {}, {}); + vec4 a_vertexColour = vec4({}, {}, {}, {}); + vec2 a_texcoord0 = vec2({}, {}); + float a_texcoord0_w = {}; + vec2 a_texcoord1 = vec2({}, {}); + vec2 a_texcoord2 = vec2({}, {}); + vec3 a_view = vec3({}, {}, {}); +)", + getSemanticName(0), getSemanticName(1), getSemanticName(2), getSemanticName(3), getSemanticName(4), getSemanticName(5), getSemanticName(6), + getSemanticName(7), getSemanticName(8), getSemanticName(9), getSemanticName(10), getSemanticName(11), getSemanticName(12), + getSemanticName(13), getSemanticName(16), getSemanticName(14), getSemanticName(15), getSemanticName(22), getSemanticName(23), + getSemanticName(18), getSemanticName(19), getSemanticName(20) + ); + + if (usingUbershader) { + Helpers::panic("Unimplemented: GetVertexShaderAccelerated for ubershader"); + return picaSource; + } else { + // TODO: Uniforms and don't hardcode fixed-function semantic indices... + std::string ret = picaSource; + if (api == API::GLES) { + ret += "\n#define USING_GLES\n"; + } + + ret += uniformDefinition; + + ret += R"( +out vec4 v_quaternion; +out vec4 v_colour; +out vec3 v_texcoord0; +out vec2 v_texcoord1; +out vec3 v_view; +out vec2 v_texcoord2; + +#ifndef USING_GLES + out float gl_ClipDistance[2]; +#endif + +void main() { + pica_shader_main(); +)"; + // Transfer fixed function fragment registers from vertex shader output to the fragment shader + ret += semantics; + + ret += R"( + gl_Position = a_coords; + vec4 colourAbs = abs(a_vertexColour); + v_colour = min(colourAbs, vec4(1.f)); + + v_texcoord0 = vec3(a_texcoord0.x, 1.0 - a_texcoord0.y, a_texcoord0_w); + v_texcoord1 = vec2(a_texcoord1.x, 1.0 - a_texcoord1.y); + v_texcoord2 = vec2(a_texcoord2.x, 1.0 - a_texcoord2.y); + v_view = a_view; + v_quaternion = a_quaternion; + +#ifndef USING_GLES + gl_ClipDistance[0] = -a_coords.z; + gl_ClipDistance[1] = dot(clipCoords, a_coords); +#endif +})"; + return ret; + } +} + void FragmentGenerator::compileLogicOps(std::string& shader, const PICA::FragmentConfig& config) { if (api != API::GLES) [[unlikely]] { Helpers::warn("Shadergen: Unsupported API for compileLogicOps"); diff --git a/src/core/PICA/shader_unit.cpp b/src/core/PICA/shader_unit.cpp index 759849a8..6b291d31 100644 --- a/src/core/PICA/shader_unit.cpp +++ b/src/core/PICA/shader_unit.cpp @@ -34,4 +34,5 @@ void PICAShader::reset() { codeHashDirty = true; opdescHashDirty = true; + uniformsDirty = true; } \ No newline at end of file diff --git a/src/core/renderer_gl/gl_state.cpp b/src/core/renderer_gl/gl_state.cpp index 3d1c0681..785cac41 100644 --- a/src/core/renderer_gl/gl_state.cpp +++ b/src/core/renderer_gl/gl_state.cpp @@ -73,10 +73,7 @@ void GLStateManager::resetVAO() { } void GLStateManager::resetBuffers() { - boundVBO = 0; boundUBO = 0; - - glBindBuffer(GL_ARRAY_BUFFER, 0); glBindBuffer(GL_UNIFORM_BUFFER, 0); } diff --git a/src/core/renderer_gl/renderer_gl.cpp b/src/core/renderer_gl/renderer_gl.cpp index ecbee3a2..30f9be91 100644 --- a/src/core/renderer_gl/renderer_gl.cpp +++ b/src/core/renderer_gl/renderer_gl.cpp @@ -2,13 +2,15 @@ #include +#include #include -#include "config.hpp" #include "PICA/float_types.hpp" -#include "PICA/pica_frag_uniforms.hpp" #include "PICA/gpu.hpp" +#include "PICA/pica_frag_uniforms.hpp" #include "PICA/regs.hpp" +#include "PICA/shader_decompiler.hpp" +#include "config.hpp" #include "math_util.hpp" CMRC_DECLARE(RendererGL); @@ -24,7 +26,7 @@ void RendererGL::reset() { colourBufferCache.reset(); textureCache.reset(); - clearShaderCache(); + shaderCache.clear(); // Init the colour/depth buffer settings to some random defaults on reset colourBufferLoc = 0; @@ -77,40 +79,56 @@ void RendererGL::initGraphicsContextInternal() { gl.useProgram(displayProgram); glUniform1i(OpenGL::uniformLocation(displayProgram, "u_texture"), 0); // Init sampler object + // Create stream buffers for vertex, index and uniform buffers + static constexpr usize hwIndexBufferSize = 2_MB; + static constexpr usize hwVertexBufferSize = 16_MB; + + hwIndexBuffer = StreamBuffer::Create(GL_ELEMENT_ARRAY_BUFFER, hwIndexBufferSize); + hwVertexBuffer = StreamBuffer::Create(GL_ARRAY_BUFFER, hwVertexBufferSize); + // Allocate memory for the shadergen fragment uniform UBO glGenBuffers(1, &shadergenFragmentUBO); gl.bindUBO(shadergenFragmentUBO); glBufferData(GL_UNIFORM_BUFFER, sizeof(PICA::FragmentUniforms), nullptr, GL_DYNAMIC_DRAW); - vbo.createFixedSize(sizeof(Vertex) * vertexBufferSize, GL_STREAM_DRAW); - gl.bindVBO(vbo); - vao.create(); - gl.bindVAO(vao); + // Allocate memory for the accelerated vertex shader uniform UBO + glGenBuffers(1, &hwShaderUniformUBO); + gl.bindUBO(hwShaderUniformUBO); + glBufferData(GL_UNIFORM_BUFFER, PICAShader::totalUniformSize(), nullptr, GL_DYNAMIC_DRAW); + + vbo.createFixedSize(sizeof(Vertex) * vertexBufferSize * 2, GL_STREAM_DRAW); + vbo.bind(); + // Initialize the VAO used when not using hw shaders + defaultVAO.create(); + gl.bindVAO(defaultVAO); // Position (x, y, z, w) attributes - vao.setAttributeFloat(0, 4, sizeof(Vertex), offsetof(Vertex, s.positions)); - vao.enableAttribute(0); + defaultVAO.setAttributeFloat(0, 4, sizeof(Vertex), offsetof(Vertex, s.positions)); + defaultVAO.enableAttribute(0); // Quaternion attribute - vao.setAttributeFloat(1, 4, sizeof(Vertex), offsetof(Vertex, s.quaternion)); - vao.enableAttribute(1); + defaultVAO.setAttributeFloat(1, 4, sizeof(Vertex), offsetof(Vertex, s.quaternion)); + defaultVAO.enableAttribute(1); // Colour attribute - vao.setAttributeFloat(2, 4, sizeof(Vertex), offsetof(Vertex, s.colour)); - vao.enableAttribute(2); + defaultVAO.setAttributeFloat(2, 4, sizeof(Vertex), offsetof(Vertex, s.colour)); + defaultVAO.enableAttribute(2); // UV 0 attribute - vao.setAttributeFloat(3, 2, sizeof(Vertex), offsetof(Vertex, s.texcoord0)); - vao.enableAttribute(3); + defaultVAO.setAttributeFloat(3, 2, sizeof(Vertex), offsetof(Vertex, s.texcoord0)); + defaultVAO.enableAttribute(3); // UV 1 attribute - vao.setAttributeFloat(4, 2, sizeof(Vertex), offsetof(Vertex, s.texcoord1)); - vao.enableAttribute(4); + defaultVAO.setAttributeFloat(4, 2, sizeof(Vertex), offsetof(Vertex, s.texcoord1)); + defaultVAO.enableAttribute(4); // UV 0 W-component attribute - vao.setAttributeFloat(5, 1, sizeof(Vertex), offsetof(Vertex, s.texcoord0_w)); - vao.enableAttribute(5); + defaultVAO.setAttributeFloat(5, 1, sizeof(Vertex), offsetof(Vertex, s.texcoord0_w)); + defaultVAO.enableAttribute(5); // View - vao.setAttributeFloat(6, 3, sizeof(Vertex), offsetof(Vertex, s.view)); - vao.enableAttribute(6); + defaultVAO.setAttributeFloat(6, 3, sizeof(Vertex), offsetof(Vertex, s.view)); + defaultVAO.enableAttribute(6); // UV 2 attribute - vao.setAttributeFloat(7, 2, sizeof(Vertex), offsetof(Vertex, s.texcoord2)); - vao.enableAttribute(7); + defaultVAO.setAttributeFloat(7, 2, sizeof(Vertex), offsetof(Vertex, s.texcoord2)); + defaultVAO.enableAttribute(7); + + // Initialize the VAO used for hw shaders + hwShaderVAO.create(); dummyVBO.create(); dummyVAO.create(); @@ -165,6 +183,12 @@ void RendererGL::initGraphicsContextInternal() { OpenGL::clearColor(); OpenGL::setViewport(oldViewport[0], oldViewport[1], oldViewport[2], oldViewport[3]); + // Initialize fixed attributes + for (int i = 0; i < fixedAttrValues.size(); i++) { + fixedAttrValues[i] = {0.f, 0.f, 0.f, 0.f}; + glVertexAttrib4f(i, 0.0, 0.0, 0.0, 0.0); + } + reset(); // Populate our driver info structure @@ -418,29 +442,14 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v OpenGL::Triangle, }; - bool usingUbershader = enableUbershader; - if (usingUbershader) { - const bool lightsEnabled = (regs[InternalRegs::LightingEnable] & 1) != 0; - const uint lightCount = (regs[InternalRegs::LightNumber] & 0x7) + 1; - - // Emulating lights in the ubershader is incredibly slow, so we've got an option to render draws using moret han N lights via shadergen - // This way we generate fewer shaders overall than with full shadergen, but don't tank performance - if (emulatorConfig->forceShadergenForLights && lightsEnabled && lightCount >= emulatorConfig->lightShadergenThreshold) { - usingUbershader = false; - } - } - - if (usingUbershader) { - gl.useProgram(triangleProgram); - } else { - OpenGL::Program& program = getSpecializedShader(); - gl.useProgram(program); - } - const auto primitiveTopology = primTypes[static_cast(primType)]; gl.disableScissor(); - gl.bindVBO(vbo); - gl.bindVAO(vao); + + // If we're using accelerated shaders, the hw VAO, VBO and EBO objects will have already been bound in prepareForDraw + if (!usingAcceleratedShader) { + vbo.bind(); + gl.bindVAO(defaultVAO); + } gl.enableClipPlane(0); // Clipping plane 0 is always enabled if (regs[PICA::InternalRegs::ClipEnable] & 1) { @@ -458,38 +467,9 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v const int depthFunc = getBits<4, 3>(depthControl); const int colourMask = getBits<8, 4>(depthControl); gl.setColourMask(colourMask & 1, colourMask & 2, colourMask & 4, colourMask & 8); - static constexpr std::array depthModes = {GL_NEVER, GL_ALWAYS, GL_EQUAL, GL_NOTEQUAL, GL_LESS, GL_LEQUAL, GL_GREATER, GL_GEQUAL}; - // Update ubershader uniforms - if (usingUbershader) { - const float depthScale = f24::fromRaw(regs[PICA::InternalRegs::DepthScale] & 0xffffff).toFloat32(); - const float depthOffset = f24::fromRaw(regs[PICA::InternalRegs::DepthOffset] & 0xffffff).toFloat32(); - const bool depthMapEnable = regs[PICA::InternalRegs::DepthmapEnable] & 1; - - if (oldDepthScale != depthScale) { - oldDepthScale = depthScale; - glUniform1f(ubershaderData.depthScaleLoc, depthScale); - } - - if (oldDepthOffset != depthOffset) { - oldDepthOffset = depthOffset; - glUniform1f(ubershaderData.depthOffsetLoc, depthOffset); - } - - if (oldDepthmapEnable != depthMapEnable) { - oldDepthmapEnable = depthMapEnable; - glUniform1i(ubershaderData.depthmapEnableLoc, depthMapEnable); - } - - // Upload PICA Registers as a single uniform. The shader needs access to the rasterizer registers (for depth, starting from index 0x48) - // The texturing and the fragment lighting registers. Therefore we upload them all in one go to avoid multiple slow uniform updates - glUniform1uiv(ubershaderData.picaRegLoc, 0x200 - 0x48, ®s[0x48]); - setupUbershaderTexEnv(); - } - bindTexturesToSlots(); - if (gpu.fogLUTDirty) { updateFogLUT(); } @@ -532,8 +512,22 @@ void RendererGL::drawVertices(PICA::PrimType primType, std::span v setupStencilTest(stencilEnable); - vbo.bufferVertsSub(vertices); - OpenGL::draw(primitiveTopology, GLsizei(vertices.size())); + if (!usingAcceleratedShader) { + vbo.bufferVertsSub(vertices); + OpenGL::draw(primitiveTopology, GLsizei(vertices.size())); + } else { + if (performIndexedRender) { + // When doing indexed rendering, use glDrawRangeElementsBaseVertex to issue the indexed draw + hwIndexBuffer->Bind(); + glDrawRangeElementsBaseVertex( + primitiveTopology, minimumIndex, maximumIndex, GLsizei(vertices.size()), usingShortIndices ? GL_UNSIGNED_SHORT : GL_UNSIGNED_BYTE, + hwIndexBufferOffset, -GLint(minimumIndex) + ); + } else { + // When doing non-indexed rendering, just use glDrawArrays + OpenGL::draw(primitiveTopology, GLsizei(vertices.size())); + } + } } void RendererGL::display() { @@ -840,7 +834,8 @@ std::optional RendererGL::getColourBuffer(u32 addr, PICA::ColorFmt } OpenGL::Program& RendererGL::getSpecializedShader() { - constexpr uint uboBlockBinding = 2; + constexpr uint vsUBOBlockBinding = 1; + constexpr uint fsUBOBlockBinding = 2; PICA::FragmentConfig fsConfig(regs); // If we're not on GLES, ignore the logic op configuration and don't generate redundant shaders for it, since we use hw logic ops @@ -848,30 +843,44 @@ OpenGL::Program& RendererGL::getSpecializedShader() { fsConfig.outConfig.logicOpMode = PICA::LogicOpMode(0); #endif - CachedProgram& programEntry = shaderCache[fsConfig]; + OpenGL::Shader& fragShader = shaderCache.fragmentShaderCache[fsConfig]; + if (!fragShader.exists()) { + std::string fs = fragShaderGen.generate(fsConfig); + fragShader.create({fs.c_str(), fs.size()}, OpenGL::Fragment); + } + + // Get the handle of the current vertex shader + OpenGL::Shader& vertexShader = usingAcceleratedShader ? *generatedVertexShader : defaultShadergenVs; + // And form the key for looking up a shader program + const u64 programKey = (u64(vertexShader.handle()) << 32) | u64(fragShader.handle()); + + CachedProgram& programEntry = shaderCache.programCache[programKey]; OpenGL::Program& program = programEntry.program; if (!program.exists()) { - std::string fs = fragShaderGen.generate(fsConfig, &driverInfo); - - OpenGL::Shader fragShader({fs.c_str(), fs.size()}, OpenGL::Fragment); - program.create({defaultShadergenVs, fragShader}); + program.create({vertexShader, fragShader}); gl.useProgram(program); - fragShader.free(); - // Init sampler objects. Texture 0 goes in texture unit 0, texture 1 in TU 1, texture 2 in TU 2, and the light maps go in TU 3 glUniform1i(OpenGL::uniformLocation(program, "u_tex0"), 0); glUniform1i(OpenGL::uniformLocation(program, "u_tex1"), 1); glUniform1i(OpenGL::uniformLocation(program, "u_tex2"), 2); glUniform1i(OpenGL::uniformLocation(program, "u_tex_luts"), 3); - // Set up the binding for our UBO. Sadly we can't specify it in the shader like normal people, + // Set up the binding for our UBOs. Sadly we can't specify it in the shader like normal people, // As it's an OpenGL 4.2 feature that MacOS doesn't support... - uint uboIndex = glGetUniformBlockIndex(program.handle(), "FragmentUniforms"); - glUniformBlockBinding(program.handle(), uboIndex, uboBlockBinding); + uint fsUBOIndex = glGetUniformBlockIndex(program.handle(), "FragmentUniforms"); + glUniformBlockBinding(program.handle(), fsUBOIndex, fsUBOBlockBinding); + + if (usingAcceleratedShader) { + uint vertexUBOIndex = glGetUniformBlockIndex(program.handle(), "PICAShaderUniforms"); + glUniformBlockBinding(program.handle(), vertexUBOIndex, vsUBOBlockBinding); + } + } + glBindBufferBase(GL_UNIFORM_BUFFER, fsUBOBlockBinding, shadergenFragmentUBO); + if (usingAcceleratedShader) { + glBindBufferBase(GL_UNIFORM_BUFFER, vsUBOBlockBinding, hwShaderUniformUBO); } - glBindBufferBase(GL_UNIFORM_BUFFER, uboBlockBinding, shadergenFragmentUBO); // Upload uniform data to our shader's UBO PICA::FragmentUniforms uniforms; @@ -961,6 +970,101 @@ OpenGL::Program& RendererGL::getSpecializedShader() { return program; } +bool RendererGL::prepareForDraw(ShaderUnit& shaderUnit, PICA::DrawAcceleration* accel) { + // First we figure out if we will be using an ubershader + bool usingUbershader = emulatorConfig->useUbershaders; + if (usingUbershader) { + const bool lightsEnabled = (regs[InternalRegs::LightingEnable] & 1) != 0; + const uint lightCount = (regs[InternalRegs::LightNumber] & 0x7) + 1; + + // Emulating lights in the ubershader is incredibly slow, so we've got an option to render draws using moret han N lights via shadergen + // This way we generate fewer shaders overall than with full shadergen, but don't tank performance + if (emulatorConfig->forceShadergenForLights && lightsEnabled && lightCount >= emulatorConfig->lightShadergenThreshold) { + usingUbershader = false; + } + } + + // Then we figure out if we will use hw accelerated shaders, and try to fetch our shader + // TODO: Ubershader support for accelerated shaders + usingAcceleratedShader = emulatorConfig->accelerateShaders && !usingUbershader && accel != nullptr && accel->canBeAccelerated; + + if (usingAcceleratedShader) { + PICA::VertConfig vertexConfig(shaderUnit.vs, regs, usingUbershader); + + std::optional& shader = shaderCache.vertexShaderCache[vertexConfig]; + // If the optional is false, we have never tried to recompile the shader before. Try to recompile it and see if it works. + if (!shader.has_value()) { + // Initialize shader to a "null" shader (handle == 0) + shader = OpenGL::Shader(); + + std::string picaShaderSource = PICA::ShaderGen::decompileShader( + shaderUnit.vs, *emulatorConfig, shaderUnit.vs.entrypoint, + Helpers::isAndroid() ? PICA::ShaderGen::API::GLES : PICA::ShaderGen::API::GL, PICA::ShaderGen::Language::GLSL + ); + + // Empty source means compilation error, if the source is not empty then we convert the recompiled PICA code into a valid shader and upload + // it to the GPU + if (!picaShaderSource.empty()) { + std::string vertexShaderSource = fragShaderGen.getVertexShaderAccelerated(picaShaderSource, vertexConfig, usingUbershader); + shader->create({vertexShaderSource}, OpenGL::Vertex); + } + } + + // Shader generation did not work out, so set usingAcceleratedShader to false + if (!shader->exists()) { + usingAcceleratedShader = false; + } else { + generatedVertexShader = &(*shader); + gl.bindUBO(hwShaderUniformUBO); + + if (shaderUnit.vs.uniformsDirty) { + shaderUnit.vs.uniformsDirty = false; + glBufferSubData(GL_UNIFORM_BUFFER, 0, PICAShader::totalUniformSize(), shaderUnit.vs.getUniformPointer()); + } + + performIndexedRender = accel->indexed; + minimumIndex = GLsizei(accel->minimumIndex); + maximumIndex = GLsizei(accel->maximumIndex); + + // Upload vertex data and index buffer data to our GPU + accelerateVertexUpload(shaderUnit, accel); + } + } + + if (!usingUbershader) { + OpenGL::Program& program = getSpecializedShader(); + gl.useProgram(program); + } else { // Bind ubershader & load ubershader uniforms + gl.useProgram(triangleProgram); + + const float depthScale = f24::fromRaw(regs[PICA::InternalRegs::DepthScale] & 0xffffff).toFloat32(); + const float depthOffset = f24::fromRaw(regs[PICA::InternalRegs::DepthOffset] & 0xffffff).toFloat32(); + const bool depthMapEnable = regs[PICA::InternalRegs::DepthmapEnable] & 1; + + if (oldDepthScale != depthScale) { + oldDepthScale = depthScale; + glUniform1f(ubershaderData.depthScaleLoc, depthScale); + } + + if (oldDepthOffset != depthOffset) { + oldDepthOffset = depthOffset; + glUniform1f(ubershaderData.depthOffsetLoc, depthOffset); + } + + if (oldDepthmapEnable != depthMapEnable) { + oldDepthmapEnable = depthMapEnable; + glUniform1i(ubershaderData.depthmapEnableLoc, depthMapEnable); + } + + // Upload PICA Registers as a single uniform. The shader needs access to the rasterizer registers (for depth, starting from index 0x48) + // The texturing and the fragment lighting registers. Therefore we upload them all in one go to avoid multiple slow uniform updates + glUniform1uiv(ubershaderData.picaRegLoc, 0x200 - 0x48, ®s[0x48]); + setupUbershaderTexEnv(); + } + + return usingAcceleratedShader; +} + void RendererGL::screenshot(const std::string& name) { constexpr uint width = 400; constexpr uint height = 2 * 240; @@ -974,7 +1078,7 @@ void RendererGL::screenshot(const std::string& name) { // Flip the image vertically for (int y = 0; y < height; y++) { - memcpy(&flippedPixels[y * width * 4], &pixels[(height - y - 1) * width * 4], width * 4); + std::memcpy(&flippedPixels[y * width * 4], &pixels[(height - y - 1) * width * 4], width * 4); // Swap R and B channels for (int x = 0; x < width; x++) { std::swap(flippedPixels[y * width * 4 + x * 4 + 0], flippedPixels[y * width * 4 + x * 4 + 2]); @@ -986,21 +1090,12 @@ void RendererGL::screenshot(const std::string& name) { stbi_write_png(name.c_str(), width, height, 4, flippedPixels.data(), 0); } -void RendererGL::clearShaderCache() { - for (auto& shader : shaderCache) { - CachedProgram& cachedProgram = shader.second; - cachedProgram.program.free(); - } - - shaderCache.clear(); -} - void RendererGL::deinitGraphicsContext() { // Invalidate all surface caches since they'll no longer be valid textureCache.reset(); depthBufferCache.reset(); colourBufferCache.reset(); - clearShaderCache(); + shaderCache.clear(); // All other GL objects should be invalidated automatically and be recreated by the next call to initGraphicsContext // TODO: Make it so that depth and colour buffers get written back to 3DS memory @@ -1049,3 +1144,92 @@ void RendererGL::initUbershader(OpenGL::Program& program) { glUniform1i(OpenGL::uniformLocation(program, "u_tex2"), 2); glUniform1i(OpenGL::uniformLocation(program, "u_tex_luts"), 3); } + +void RendererGL::accelerateVertexUpload(ShaderUnit& shaderUnit, PICA::DrawAcceleration* accel) { + u32 buffer = 0; // Vertex buffer index for non-fixed attributes + u32 attrCount = 0; + + const u32 totalAttribCount = accel->totalAttribCount; + + static constexpr GLenum attributeFormats[4] = { + GL_BYTE, // 0: Signed byte + GL_UNSIGNED_BYTE, // 1: Unsigned byte + GL_SHORT, // 2: Short + GL_FLOAT, // 3: Float + }; + + const u32 vertexCount = accel->maximumIndex - accel->minimumIndex + 1; + + // Update index buffer if necessary + if (accel->indexed) { + usingShortIndices = accel->useShortIndices; + const usize indexBufferSize = regs[PICA::InternalRegs::VertexCountReg] * (usingShortIndices ? sizeof(u16) : sizeof(u8)); + + hwIndexBuffer->Bind(); + auto indexBufferRes = hwIndexBuffer->Map(4, indexBufferSize); + hwIndexBufferOffset = reinterpret_cast(usize(indexBufferRes.buffer_offset)); + + std::memcpy(indexBufferRes.pointer, accel->indexBuffer, indexBufferSize); + hwIndexBuffer->Unmap(indexBufferSize); + } + + hwVertexBuffer->Bind(); + auto vertexBufferRes = hwVertexBuffer->Map(4, accel->vertexDataSize); + u8* vertexData = static_cast(vertexBufferRes.pointer); + const u32 vertexBufferOffset = vertexBufferRes.buffer_offset; + + gl.bindVAO(hwShaderVAO); + + // Enable or disable vertex attributes as needed + const u32 currentAttributeMask = accel->enabledAttributeMask; + // Use bitwise xor to calculate which attributes changed + u32 attributeMaskDiff = currentAttributeMask ^ previousAttributeMask; + + while (attributeMaskDiff != 0) { + // Get index of next different attribute and turn it off + const u32 index = 31 - std::countl_zero(attributeMaskDiff); + const u32 mask = 1u << index; + attributeMaskDiff ^= mask; + + if ((currentAttributeMask & mask) != 0) { + // Attribute was disabled and is now enabled + hwShaderVAO.enableAttribute(index); + } else { + // Attribute was enabled and is now disabled + hwShaderVAO.disableAttribute(index); + } + } + + previousAttributeMask = currentAttributeMask; + + // Upload the data for each (enabled) attribute loader into our vertex buffer + for (int i = 0; i < accel->totalLoaderCount; i++) { + auto& loader = accel->loaders[i]; + + std::memcpy(vertexData, loader.data, loader.size); + vertexData += loader.size; + } + + hwVertexBuffer->Unmap(accel->vertexDataSize); + + // Iterate over the 16 PICA input registers and configure how they should be fetched. + for (int i = 0; i < 16; i++) { + const auto& attrib = accel->attributeInfo[i]; + const u32 attributeMask = 1u << i; + + if (accel->fixedAttributes & attributeMask) { + auto& attrValue = fixedAttrValues[i]; + // This is a fixed attribute, so set its fixed value, but only if it actually needs to be updated + if (attrValue[0] != attrib.fixedValue[0] || attrValue[1] != attrib.fixedValue[1] || attrValue[2] != attrib.fixedValue[2] || + attrValue[3] != attrib.fixedValue[3]) { + std::memcpy(attrValue.data(), attrib.fixedValue.data(), sizeof(attrib.fixedValue)); + glVertexAttrib4f(i, attrib.fixedValue[0], attrib.fixedValue[1], attrib.fixedValue[2], attrib.fixedValue[3]); + } + } else if (accel->enabledAttributeMask & attributeMask) { + glVertexAttribPointer( + i, attrib.componentCount, attributeFormats[attrib.type], GL_FALSE, attrib.stride, + reinterpret_cast(vertexBufferOffset + attrib.offset) + ); + } + } +} \ No newline at end of file diff --git a/src/libretro_core.cpp b/src/libretro_core.cpp index 3f92cddd..21a62f23 100644 --- a/src/libretro_core.cpp +++ b/src/libretro_core.cpp @@ -163,8 +163,9 @@ static int fetchVariableRange(std::string key, int min, int max) { static void configInit() { static const retro_variable values[] = { - {"panda3ds_use_shader_jit", EmulatorConfig::shaderJitDefault ? "Enable shader JIT; enabled|disabled" - : "Enable shader JIT; disabled|enabled"}, + {"panda3ds_use_shader_jit", EmulatorConfig::shaderJitDefault ? "Enable shader JIT; enabled|disabled" : "Enable shader JIT; disabled|enabled"}, + {"panda3ds_accelerate_shaders", + EmulatorConfig::accelerateShadersDefault ? "Run 3DS shaders on the GPU; enabled|disabled" : "Run 3DS shaders on the GPU; disabled|enabled"}, {"panda3ds_accurate_shader_mul", "Enable accurate shader multiplication; disabled|enabled"}, {"panda3ds_use_ubershader", EmulatorConfig::ubershaderDefault ? "Use ubershaders (No stutter, maybe slower); enabled|disabled" : "Use ubershaders (No stutter, maybe slower); disabled|enabled"}, @@ -197,6 +198,8 @@ static void configUpdate() { config.sdWriteProtected = fetchVariableBool("panda3ds_write_protect_virtual_sd", false); 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.forceShadergenForLights = fetchVariableBool("panda3ds_ubershader_lighting_override", true); config.lightShadergenThreshold = fetchVariableRange("panda3ds_ubershader_lighting_override_threshold", 1, 8); config.discordRpcEnabled = false; diff --git a/third_party/duckstation/gl/stream_buffer.cpp b/third_party/duckstation/gl/stream_buffer.cpp new file mode 100644 index 00000000..b7a40603 --- /dev/null +++ b/third_party/duckstation/gl/stream_buffer.cpp @@ -0,0 +1,288 @@ +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#include "gl/stream_buffer.h" + +#include +#include + +#include "align.hpp" + +OpenGLStreamBuffer::OpenGLStreamBuffer(GLenum target, GLuint buffer_id, u32 size) : m_target(target), m_buffer_id(buffer_id), m_size(size) {} +OpenGLStreamBuffer::~OpenGLStreamBuffer() { glDeleteBuffers(1, &m_buffer_id); } + +void OpenGLStreamBuffer::Bind() { glBindBuffer(m_target, m_buffer_id); } +void OpenGLStreamBuffer::Unbind() { glBindBuffer(m_target, 0); } + +void OpenGLStreamBuffer::SetDebugName(std::string_view name) { +#ifdef GPU_DEBUG_INFO + if (glObjectLabel) { + glObjectLabel(GL_BUFFER, GetGLBufferId(), static_cast(name.length()), static_cast(name.data())); + } +#endif +} + +namespace { + // Uses glBufferSubData() to update. Preferred for drivers which don't support {ARB,EXT}_buffer_storage. + class BufferSubDataStreamBuffer final : public OpenGLStreamBuffer { + public: + ~BufferSubDataStreamBuffer() override { Common::alignedFree(m_cpu_buffer); } + + MappingResult Map(u32 alignment, u32 min_size) override { return MappingResult{static_cast(m_cpu_buffer), 0, 0, m_size / alignment}; } + + u32 Unmap(u32 used_size) override { + if (used_size == 0) return 0; + + glBindBuffer(m_target, m_buffer_id); + glBufferSubData(m_target, 0, used_size, m_cpu_buffer); + return 0; + } + + u32 GetChunkSize() const override { return m_size; } + + static std::unique_ptr Create(GLenum target, u32 size) { + glGetError(); + + GLuint buffer_id; + glGenBuffers(1, &buffer_id); + glBindBuffer(target, buffer_id); + glBufferData(target, size, nullptr, GL_STREAM_DRAW); + + GLenum err = glGetError(); + if (err != GL_NO_ERROR) { + glBindBuffer(target, 0); + glDeleteBuffers(1, &buffer_id); + return {}; + } + + return std::unique_ptr(new BufferSubDataStreamBuffer(target, buffer_id, size)); + } + + private: + BufferSubDataStreamBuffer(GLenum target, GLuint buffer_id, u32 size) : OpenGLStreamBuffer(target, buffer_id, size) { + m_cpu_buffer = static_cast(Common::alignedMalloc(size, 32)); + if (!m_cpu_buffer) Panic("Failed to allocate CPU storage for GL buffer"); + } + + u8* m_cpu_buffer; + }; + + // Uses BufferData() to orphan the buffer after every update. Used on Mali where BufferSubData forces a sync. + class BufferDataStreamBuffer final : public OpenGLStreamBuffer { + public: + ~BufferDataStreamBuffer() override { Common::alignedFree(m_cpu_buffer); } + + MappingResult Map(u32 alignment, u32 min_size) override { return MappingResult{static_cast(m_cpu_buffer), 0, 0, m_size / alignment}; } + + u32 Unmap(u32 used_size) override { + if (used_size == 0) return 0; + + glBindBuffer(m_target, m_buffer_id); + glBufferData(m_target, used_size, m_cpu_buffer, GL_STREAM_DRAW); + return 0; + } + + u32 GetChunkSize() const override { return m_size; } + + static std::unique_ptr Create(GLenum target, u32 size) { + glGetError(); + + GLuint buffer_id; + glGenBuffers(1, &buffer_id); + glBindBuffer(target, buffer_id); + glBufferData(target, size, nullptr, GL_STREAM_DRAW); + + GLenum err = glGetError(); + if (err != GL_NO_ERROR) { + glBindBuffer(target, 0); + glDeleteBuffers(1, &buffer_id); + return {}; + } + + return std::unique_ptr(new BufferDataStreamBuffer(target, buffer_id, size)); + } + + private: + BufferDataStreamBuffer(GLenum target, GLuint buffer_id, u32 size) : OpenGLStreamBuffer(target, buffer_id, size) { + m_cpu_buffer = static_cast(Common::alignedMalloc(size, 32)); + if (!m_cpu_buffer) Panic("Failed to allocate CPU storage for GL buffer"); + } + + u8* m_cpu_buffer; + }; + + // Base class for implementations which require syncing. + class SyncingStreamBuffer : public OpenGLStreamBuffer { + public: + enum : u32 { NUM_SYNC_POINTS = 16 }; + + virtual ~SyncingStreamBuffer() override { + for (u32 i = m_available_block_index; i <= m_used_block_index; i++) { + glDeleteSync(m_sync_objects[i]); + } + } + + protected: + SyncingStreamBuffer(GLenum target, GLuint buffer_id, u32 size) + : OpenGLStreamBuffer(target, buffer_id, size), m_bytes_per_block((size + (NUM_SYNC_POINTS)-1) / NUM_SYNC_POINTS) {} + + ALWAYS_INLINE u32 GetSyncIndexForOffset(u32 offset) { return offset / m_bytes_per_block; } + + ALWAYS_INLINE void AddSyncsForOffset(u32 offset) { + const u32 end = GetSyncIndexForOffset(offset); + for (; m_used_block_index < end; m_used_block_index++) { + if (m_sync_objects[m_used_block_index]) { + Helpers::warn("GL stream buffer: Fence slot we're trying to insert is already in use"); + } + + m_sync_objects[m_used_block_index] = glFenceSync(GL_SYNC_GPU_COMMANDS_COMPLETE, 0); + } + } + + ALWAYS_INLINE void WaitForSync(GLsync& sync) { + glClientWaitSync(sync, GL_SYNC_FLUSH_COMMANDS_BIT, GL_TIMEOUT_IGNORED); + glDeleteSync(sync); + sync = nullptr; + } + + ALWAYS_INLINE void EnsureSyncsWaitedForOffset(u32 offset) { + const u32 end = std::min(GetSyncIndexForOffset(offset) + 1, NUM_SYNC_POINTS); + for (; m_available_block_index < end; m_available_block_index++) { + if (!m_sync_objects[m_available_block_index]) [[unlikely]] { + Helpers::warn("GL stream buffer: Fence slot we're trying to wait on is not in use"); + } + + WaitForSync(m_sync_objects[m_available_block_index]); + } + } + + void AllocateSpace(u32 size) { + // add sync objects for writes since the last allocation + AddSyncsForOffset(m_position); + + // wait for sync objects for the space we want to use + EnsureSyncsWaitedForOffset(m_position + size); + + // wrap-around? + if ((m_position + size) > m_size) { + // current position ... buffer end + AddSyncsForOffset(m_size); + + // rewind, and try again + m_position = 0; + + // wait for the sync at the start of the buffer + WaitForSync(m_sync_objects[0]); + m_available_block_index = 1; + + // and however much more we need to satisfy the allocation + EnsureSyncsWaitedForOffset(size); + m_used_block_index = 0; + } + } + + u32 GetChunkSize() const override { return m_size / NUM_SYNC_POINTS; } + + u32 m_position = 0; + u32 m_used_block_index = 0; + u32 m_available_block_index = NUM_SYNC_POINTS; + u32 m_bytes_per_block; + std::array m_sync_objects{}; + }; + + class BufferStorageStreamBuffer : public SyncingStreamBuffer { + public: + ~BufferStorageStreamBuffer() override { + glBindBuffer(m_target, m_buffer_id); + glUnmapBuffer(m_target); + glBindBuffer(m_target, 0); + } + + MappingResult Map(u32 alignment, u32 min_size) override { + if (m_position > 0) m_position = Common::alignUp(m_position, alignment); + + AllocateSpace(min_size); + if ((m_position + min_size) > (m_available_block_index * m_bytes_per_block)) [[unlikely]] { + Helpers::panic("GL stream buffer: Invalid size passed to Unmap"); + } + + const u32 free_space_in_block = ((m_available_block_index * m_bytes_per_block) - m_position); + return MappingResult{static_cast(m_mapped_ptr + m_position), m_position, m_position / alignment, free_space_in_block / alignment}; + } + + u32 Unmap(u32 used_size) override { + if ((m_position + used_size) > m_size) [[unlikely]] { + Helpers::panic("GL stream buffer: Invalid size passed to Unmap"); + } + + if (!m_coherent) { + if (GLAD_GL_VERSION_4_5 || GLAD_GL_ARB_direct_state_access) { + glFlushMappedNamedBufferRange(m_buffer_id, m_position, used_size); + } else { + Bind(); + glFlushMappedBufferRange(m_target, m_position, used_size); + } + } + + const u32 prev_position = m_position; + m_position += used_size; + return prev_position; + } + + static std::unique_ptr Create(GLenum target, u32 size, bool coherent = true) { + glGetError(); + + GLuint buffer_id; + glGenBuffers(1, &buffer_id); + glBindBuffer(target, buffer_id); + + const u32 flags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | (coherent ? GL_MAP_COHERENT_BIT : 0); + const u32 map_flags = GL_MAP_WRITE_BIT | GL_MAP_PERSISTENT_BIT | (coherent ? 0 : GL_MAP_FLUSH_EXPLICIT_BIT); + if (GLAD_GL_VERSION_4_4 || GLAD_GL_ARB_buffer_storage) + glBufferStorage(target, size, nullptr, flags); + else if (GLAD_GL_EXT_buffer_storage) + glBufferStorageEXT(target, size, nullptr, flags); + + GLenum err = glGetError(); + if (err != GL_NO_ERROR) { + glBindBuffer(target, 0); + glDeleteBuffers(1, &buffer_id); + return {}; + } + + u8* mapped_ptr = static_cast(glMapBufferRange(target, 0, size, map_flags)); + AssertMsg(mapped_ptr, "Persistent buffer was mapped"); + + return std::unique_ptr(new BufferStorageStreamBuffer(target, buffer_id, size, mapped_ptr, coherent)); + } + + private: + BufferStorageStreamBuffer(GLenum target, GLuint buffer_id, u32 size, u8* mapped_ptr, bool coherent) + : SyncingStreamBuffer(target, buffer_id, size), m_mapped_ptr(mapped_ptr), m_coherent(coherent) {} + + u8* m_mapped_ptr; + bool m_coherent; + }; + +} // namespace + +std::unique_ptr OpenGLStreamBuffer::Create(GLenum target, u32 size) { + std::unique_ptr buf; + if (GLAD_GL_VERSION_4_4 || GLAD_GL_ARB_buffer_storage || GLAD_GL_EXT_buffer_storage) { + buf = BufferStorageStreamBuffer::Create(target, size); + if (buf) return buf; + } + + // BufferSubData is slower on all drivers except NVIDIA... +#if 0 + const char* vendor = reinterpret_cast(glGetString(GL_VENDOR)); + if (std::strcmp(vendor, "ARM") == 0 || std::strcmp(vendor, "Qualcomm") == 0) { + // Mali and Adreno drivers can't do sub-buffer tracking... + return BufferDataStreamBuffer::Create(target, size); + } + + return BufferSubDataStreamBuffer::Create(target, size); +#else + return BufferDataStreamBuffer::Create(target, size); +#endif +} \ No newline at end of file diff --git a/third_party/duckstation/gl/stream_buffer.h b/third_party/duckstation/gl/stream_buffer.h new file mode 100644 index 00000000..6b3562e7 --- /dev/null +++ b/third_party/duckstation/gl/stream_buffer.h @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2019-2023 Connor McLaughlin +// SPDX-License-Identifier: (GPL-3.0 OR CC-BY-NC-ND-4.0) + +#pragma once + +#include +// Comment to avoid clang-format reordering the glad header + +#include +#include +#include +#include + +#include "duckstation_compat.h" +#include "helpers.hpp" + +class OpenGLStreamBuffer { + public: + virtual ~OpenGLStreamBuffer(); + + ALWAYS_INLINE GLuint GetGLBufferId() const { return m_buffer_id; } + ALWAYS_INLINE GLenum GetGLTarget() const { return m_target; } + ALWAYS_INLINE u32 GetSize() const { return m_size; } + + void Bind(); + void Unbind(); + + void SetDebugName(std::string_view name); + + struct MappingResult { + void* pointer; + u32 buffer_offset; + u32 index_aligned; // offset / alignment, suitable for base vertex + u32 space_aligned; // remaining space / alignment + }; + + virtual MappingResult Map(u32 alignment, u32 min_size) = 0; + + /// Returns the position in the buffer *before* the start of used_size. + virtual u32 Unmap(u32 used_size) = 0; + + /// Returns the minimum granularity of blocks which sync objects will be created around. + virtual u32 GetChunkSize() const = 0; + + static std::unique_ptr Create(GLenum target, u32 size); + + protected: + OpenGLStreamBuffer(GLenum target, GLuint buffer_id, u32 size); + + GLenum m_target; + GLuint m_buffer_id; + u32 m_size; +}; \ No newline at end of file diff --git a/third_party/fmt b/third_party/fmt new file mode 160000 index 00000000..f8581bce --- /dev/null +++ b/third_party/fmt @@ -0,0 +1 @@ +Subproject commit f8581bcecf317e8753887b68187c9ef1ba0524f4 From 89d129211e6cbdd96e3ca78d830150ae92fd8d85 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 19 Oct 2024 17:23:54 +0300 Subject: [PATCH 217/251] HLE DSP: Add AAC decoder toggle for enabling/disabling AAC decoding (#611) * HLE DSP: Add AAC decoder toggle * Fix derp --- src/core/audio/hle_core.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/audio/hle_core.cpp b/src/core/audio/hle_core.cpp index a616f317..70a8e71d 100644 --- a/src/core/audio/hle_core.cpp +++ b/src/core/audio/hle_core.cpp @@ -632,7 +632,7 @@ namespace Audio { AAC::Message response; switch (request.command) { - case AAC::Command::EncodeDecode: + case AAC::Command::EncodeDecode: { // Dummy response to stop games from hanging response.resultCode = AAC::ResultCode::Success; response.decodeResponse.channelCount = 2; @@ -643,10 +643,13 @@ namespace Audio { response.command = request.command; response.mode = request.mode; - // We've already got an AAC decoder but it's currently disabled until mixing & output is properly implemented - // TODO: Uncomment this when the time comes - // aacDecoder->decode(response, request, [this](u32 paddr) { return getPointerPhys(paddr); }); + // TODO: Make this a toggle in config.toml. Currently we have it off by default until we finish the DSP mixer. + constexpr bool enableAAC = false; + if (enableAAC) { + aacDecoder->decode(response, request, [this](u32 paddr) { return getPointerPhys(paddr); }); + } break; + } case AAC::Command::Init: case AAC::Command::Shutdown: From c97a174cd12dd77180a63f7d86d10a7c2e5df806 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sat, 19 Oct 2024 18:16:33 +0300 Subject: [PATCH 218/251] Silence invalid offsetof warning --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index fe72f3b6..641a0055 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,7 +26,7 @@ if(APPLE) endif() if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") - set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format-nonliteral -Wno-format-security") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format-nonliteral -Wno-format-security -Wno-invalid-offsetof") endif() if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU") From af1fe1399617b6209faaa9164bda277032640d99 Mon Sep 17 00:00:00 2001 From: wheremyfoodat <44909372+wheremyfoodat@users.noreply.github.com> Date: Sun, 20 Oct 2024 16:41:41 +0300 Subject: [PATCH 219/251] Make Android builds official & update readme (#612) --- docs/img/KirbyAndroid.png | Bin 0 -> 580748 bytes readme.md | 12 ++++++------ 2 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 docs/img/KirbyAndroid.png diff --git a/docs/img/KirbyAndroid.png b/docs/img/KirbyAndroid.png new file mode 100644 index 0000000000000000000000000000000000000000..05e8b466b2a9bca97911662191c7771e27857475 GIT binary patch literal 580748 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Å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%0De^#sq_s7Rhq_5b zTD8~mTkYZPmtLl7M}^LjVLcMjx$COE?d0|e|3mC8N<`dNe(CoTwd!j69}!g19lQH> zYLiCrDS0vw(UtVUB zd8^{u=9oIZr)Ghci^@)#y6R1uc52bmx;43*x8yp1O@3qYz+$fEq1b@Jos+t*-hUO} zSEhf-ZcmMb_3A$^^nU+ssEad-}pO3cj>N&?v)|4tgVjFt5TfU-KVMDso z`5j(uXA%Mg8Pl^i%WOVuSo}fsMd`7-_Ma+CW0N1P<=(b#QgWcWe@DN3-gS9D&x`Lh z-wi&eePk-lF1m7K#}%8UGj3cy&i~hAf%oj!W|HFDK7ZNy)cI_r$$c5QciA#REPvA# zxtcED=`S>8yD=-%=k+!Nw}hGp+-$G@%`FuCq&{DHwfy`iI~QJfzwPS#>^o}}-w3Xs z(_XpV^?maH4LUv3TT{P$Uba7;tLL-$fmI)a?dwfWFBnpY0YOC28 z+;F=6`Y&7YC&hjCYA3wvrm!k7vHVFoZu`q|BEzOn|F5l$F8lp0*ZijFfop2}lB0xA zzwu?g|1I{iSBr=HgJZ{Ly;$MolBPke0)ON=|37WBJZlm$@vi!tdFiS? z5@)}u*b2=nn6^!RyP_3ux)*y-NU=@KgU0_=X{<}^U){X>=V1%vpfd5e{b{KVw>Y$S z9Dj7azkJ>;@%Z}8c-vpMcQl@)LMeC}Hxuj!2H zFCP}xe*QUqnZuh~x0c1e&X0(%S!k^J=E1*O?RmCa8xKj@)+b$dzWRO3hOKJH9aF+M z|7u81wdZ1Fn|@EV;S^JQcSsS}apw;96BA|C*F-)4Ya!p^mUb;$FR!yRhLzZ{jq($I}8>^AL7nkm)G5~Vvjwi&9|c! zkBc|>*SjZjeLSDM=-5HYUypjY^fZ`u#F%>uKJ{~ro^_z zd@&Kw?bXR&>1%zK^`QOk5DTpiYwZP>Pv)$#x%NKu?7w-kqHkCPbkmZTS{Gc~Aw5C# z!PHy4wRM&g_s?<@nRsbIoyXJJD|s*X1y;4lX-(0%|N9+7ZF5qiRq@06*fSehyJ;>|dvJuj@_AmG-}FcEW@$^UK#eg{lYMX>4uw zOk13Jy{*dr<8S5=_GJbZejRs}IIeH*JKG`?d;I3FSDhjAw<<9(?y<5y78Y~nbn9L% zk*B&Ej9p$&SDrM6tP|X`P4s8a@vfz(?YKxpVzn0S9U;J^?TJ3v$wU)&k`^77=ll*X0?t^;$x=u%i(2HqbL;pIT`I9?^QNuw*&P)2|!aH8P zbpdlX_NRS!_$+T~*Qc4nbNS5*xf_hv!ZajLHTXv7=if-#U6AS!rp+wH;TbxQ<98t2 znFYdgMAEv992yt$teA9FjN8$^n?Y`~z`3c6+=-Dt=V>#Co&FD+L;15mFJj`K>4$>; zeeFuD{L}sNdFk?c$saNCe`i-|m4-i(_R>!__dByfE&2APfC7nCCA{l6-qaEhUK(OkY-mq#`FEuDXFZhl_o`8obcL_7QMW2wip z=WhGD%eOZ;!b9{)^K!>0i|>E?>sg;(`+fI;O`obS-`8iIe}69T>SO-@|GF>cNx03M zsd9J5lY{CKTN#*_Y}~v`_DG*J)4Q;1n`%u3;#bzP35DrMzcGB1u}Sx5=%H0l`2`tT zgj;xvHJsj*9?~i=^INliEqmhs)&(*hO!en}v9FKc|Bq?c@2T0_c5ckf{%d-_^q>h_ zW2H@5$AM>@l2yu64?)h3IT+_5?-J3Baa%flWC^Kd!W$+AqR z%*)OdeC~|(wVok2ebv+-O;rvI_eI?K{lBL8*Z(`C(tmQcJv*CprqLyvKl453)jt!q z-D$k@>-oYZhd-#Vd3SvOeC}*HMj!vro#mfbXfFC~%l%<9$23mmMu~7A?lhOMJFMGp z@w*@u%K|J=(acdu4^Es_;?= z)!p`|`!>kAHWWR6ahvO>rtTcc`*Sqo;5yZ*FbgEBiXPYkh9+ zj!h}W#iz?FeM9qS+z~!-tm}7tP3rvrpRSkvsJX2#mFu52ZKdAjhbPMR@5z6hem>{N zo6f0R{h$9<_pbaJ8M|=J^tJPD33ExZss!`6mQ0ptsy^5H>>tx|-K~39u*|q-f9vrM znJD+#2=!3euCis$UjvQ)?y0=`JA`?AQ>e_N%gHa8tu9z+G)!Hw_0PAZ4{vKQztB&% zciQiGw_^T6yDwh#Nw)EOSN{_`=ry&Q?YLL0<}Gnqzr*JkL>TW#9ypcVvOeb8sZ&$- zY+RqtyDu`+)|s*4g1wR!6Yr_W*jN4szbvl%VfrQ1>dP*6!Ee8<7fxS)C!)6dZfpeO zk^b)-KMTU2UO0NFyOw96&Iy68bx-7`GTNNqeY|d_?)HCv=l|L2uF(r-d(6?|dUuP7 zx#J%;hnoUyX&*Th_CJ`i@28KeL+*Ds)+>4G45h~{RJSd+;OqFAdnCEe z6pjQQfBur8>GmdTZKWfhW|<#6GF87r?qz(c8JDT@#r>}|LVv&L_}AV%QT4LV!f>c?s$d%=e>C0jN75c z47bO7p9JIz%XF~kueNhZ_F}dU68KikBQTw#IKTC>ya{Kn(_|U-me>`OMC6}y=sazg z|LU=G)w$D7?+s!l7uc+xaiS_?=bw~dNpq2T({p0w0tE#!0{?d0+SR3~!SX~`siB%V zchBw@(UKEb|B6k26vkCQY0DJ%j}w+;PmX$FzhYLIK_+Lgbw)|d3ww>IZRVM`5-)B^ zJnGjl@6)})r;d7Z@g`AR>sRjc{2SV4v>>%N?RfarmEu8M9?N}}OwQQrs&2eh_tu@j zojzBOX-`SsQo8WfvC~eR{NjJRznzdMQM9hD4FBpc|L&>t@6h|l0<&Z`hh)@jQ?EL4 z_4-q-{CCpIPm7x&d299SPpG;%c?##ef=WgOJ$laqN|nHT3&h` zdBD(VWkqt|g7}{gE!NGvbZo`Z6i2%9Tv z)zvStYqq~Lb<*SV8DEP%G|g`5G6 zH?FF&<@Zi|wM;YkuB>B@kKV1Q?7LGL?RWi7{yKB!*4J|x_r$N_36S5&WpFK;<@Brf zF_If3rcHehF52gVOZnGrq9^M$b)LqrmzZ{Z|BSl0@?X;rwlMC$__H8ktsXb`m%EZH0{^Uh=iZT!!2008 z@g0*V{$wj-eN$FsD#O2at)YmuSY*e=3qL%=ex}_yc1Y)Je0E%=kdl1tmwEplKKcEl zXJ^)5{ont5>-US)^p%^fk+T2#*?lojMPgF)^MkF8rbXLqXWoywUy^9<^g3-Sh4FA2>_o#dAvHZV(A~!|F|n z*>byo{w?_l4x8*P^z2r!#Ztdq$pJ(HGkx zef*|D<2jRz1g74bAq;|&*(r~_ujo#2DEB%YVxr;pH1)w&AIs~^0;x-vDcN2x@r`^j z|MuOQgK}pcu&@5ly*_@o5yP+W-HXnNge*My*U@l;diD9%^E|0ne;wPU?Hy3^&1!Fy z#xoM`n@=Odl=<;IygwrZ93*Ni`54g5UUY`HKv1*orJ;nByAB4X*bPPLq z%0kL{M?$=7SEsep-4FB9rV=&^&{Iu zOV;1nwr0lGt9-^&*}F0`BvVTtI2vz_J>UJ@yTov6Y|o6jj29-X3dmHpUnD%$SX{R_ z+`}TO(`Z(uJm<_G>*QKqX>879`PIk$^U=|2*2g|yw|<^sey!#^^U*V2Wm6P_%d`q) zCR{qf$0lqhSgH(2*i zyUnX!p#BU0ulH}wZQ8g0;jjB^y;a<{>&+Cqn}(Jf`iznso3y#Eg#QXSeExrUr^9Eb zUw2MlUVSZpUG%@2B>O#|7mH72tXXVj7cIG<$~v6w%C_mvpdA9v^F`&Jy*+*Ee~hHS z@9)={3l?ve3{&~Pbif;9s+vN9^S@!Syzr6Z`e5{QnEwpZ}%1_Uf*qJ`RPl^>_Ke5|f@)7H@jdsbFjT*)k?sW@A`&bt-+SdNM0y-||#JjGF#!rbR zf?xRC8;|X1(0x8*Wk6Wnx*ckfGQusBQpGCOm&rNhCZA$7U+OJ?GT^+*>yIvmTdrTY z`Cw_@G4tatwc1Ued7i%ztqm2eT%g=O|Aq9;%5PWybIeLjJ2Xw`RuI$e1#f&6 zkDqT@+~Kj~mcCb~WvYXP)rxcby(c>LvhTXQ`Gt&vEk+ak#IiYC^zSXLDHJQ7C{`DL zU^@SM?)_y8WmlW?MIKeWcl=z}$%;ir-nqYj_WgZ!G5lNi{C{`qK7WqCm5_h?)Xf7@ z%?nG91>X*m-Exf8oOd=a|Jq2u9N*;aaoSt?PBBcH5w~H@LaU^eOAe{r?RfU$v7mP0 zywZrJ2M+}FiLhOLcy?Rjz#d=*yq{BtGTvXPG$Tn^0nDuy54IK$pvOluRc5Rqk}29_L0H^ z+w)s$@BH~xp~%I4f2ZT_Bl~~1B`oOu<@x@U(uMueQn%Z{Mq*R!)vzD*=4zl?`|)hxwqwX zv+@4@PK)`M29;cKkSx%+-B@Am)lhrd)!a3Cfr*3tg~>01N}iioiT+*4@*wivnz&~( z7q9)ZLh}#z)m@dZzph#FL{H>z!4#1$hV>=xQ>XsoujA<7vAyM!a(-HS5%-qoEAk6j z4FcN~)>{=R?~%F_`|)TwOZ$guvT3cK;#cY%o0Zdd)SoT%_BYq;>YWu{7mJo2*pPE8 zar5HupTaxu7wAmz>i^Ahip`oi>Xhv9$%>oYBF?O;&WPNiSoKl)w8^@>6LUlF^=ffc z#ok<)|6uEzzQy@z(Y4NBX9pfnni?ZBtF!;o{;K~g0v2D8pWM`JQ^tNj|cD)lX*ky^(qo>7SpU7q`KB`ubbTE?o<>T%dA*KmG0Q zpor4B9p+WmDvWVExvt7{#wQDBc$fK0*3a`xI;QVXRnX{vSpQMK*_!`Zr}sRy-1D^l z1NROmuf|J38shF#C%>N~kpJMuZm*ZFhTQ4L=B%ncXS6i7v?8_a?ys|^()Op{UcPO= zC1plt&!VFCd!nr3EczjAFSc`Cu-9dJZr%+#bzXpQbKG&|uf_8hvG;T_xLM|&?XG@1 zE%wff+t2|0g2?;TR^j~>Pdz(oN-qAku%FZZqi5%;KlzbA_r82T zooR9#>xQg9SNK19Kbd)j{hNJ6D#OdvL(;9cmxwb~?D-=0khST~^Yim>Z%#iSBN(vi zp!-6$>38q#D17Ym=hJEZSWbr-A}Jz0%{MkrQUy!5mT;kYlu1+9>ipI*Mfh=LI9o?Z zAlr{0^TH)Gn4eq-nSX3ugWI~3uMM`pe0W1^=HH9OzUedHoLaATHfGK9wnbgq&T*PHI(X~W9iM_uWZ9lyekxNd zY8H>Xnc&@`cQ$#ATv-k`9yqlT&VkYbN!p}`v2V3A8v|= zy{h#-tjS_&K8bgC^}|i3-JPn77pDHau}e$8k3A}TrQDPpmIs+-o|X+_D~(?X%o69- z%=HT|`Sy!%d%&Mx;mVdWTiMLCL)+KX&GYGv=8fWAs+4hX#wVbUYQ@!IrpsHpjd#$*P82`nM#!7|&FEvrlgm@13}O z@)ygs6T=q1X9{3Ep6(V!9Mc$41eR;R>RK_B1rt0-p>={>HhwuD)$YycyxjyJ=!Hbga&ZXVD@%{bYoH^6J%smxzr{&UjtFEQ|F2<)e zBz+V%{nI9z6l3=OJ1a-|v0!VR00xsc3g_Q!IFl1%I(6~7A0Fqs{QN6ZzuHefUbHYT zdZ){dxlxYkD~i^NR4hH%;vLylIR2 zOtMez?KN_Ee?>0hFaJ!}RE9(K^KO1TX3aG@-9ST3WZQ)Am9M+vCtMfd-O{4LBxGw| z!`@IKzMCQC((R%zyzUPd97z!5*v)CQ)cv4Uz!Kw(_3QYzruAQbe4OD++*-G34Z#;x zk`wHM<1-&`O5XP9d&oC)bMv?Fj%sgHjJvmZ?LwY_g*%NLL^LxvEG;ax63Q|@_)+zVS4jo0gD8C)z1pam1}n@Ey&#Y z==q^CzB7;hiA~vTd@D0L`G47Ey=mcVrmwyE;pL5e|IaOVlr#vH)u?r`4caR>n>sFr+TvxN7nmYu{TH3e7$|t)heD5F%C8*^+ReVluJPXI z^sm|}A~`v^zw6@#soCYt*Js6_JM1~HN~qxT$Cr}t8!VnzL`Hh?nk*tX`;s0$JlrmKe>RVX?Fsd%{SDGbJwo!Ps7_vg>8)TT+mAL+zm^*x zG}Ae{%UgVJ9y9)0*?#-aMW=kLEw>xX7*4hR+3@K9?EHNje}8{}+y1Be|0DlC>Hoh} z_v5hqt-aOX|M0PX@>{HQ-{hF|XK7dW=gwnRzvLATCC7Ut z<}a=ks=saPW42%ST>XBYXZx*r<3;C)T|ceRyi+-3M@j6*O1YYI43FI`BBq3GyzTrp z(?UCUjokR|IYuv_V4fd|L@=C|NoX>>n`zDT5jLw&o(79vRy}fs8Hyllf*s5lHMYJ~HMf;yWIqi@2riX6H`&A#BsB}xS__Vsr^24TcgI{a( z@Yva$S-aZ6$}??lh>bb-@y(1J@`48Yvl`5uv@eKW$V$ETc2?;X(=Agt!&|O2sm8C> zUSxAFcgbOg4L1&V#f5Y%3ic`QPTO#vS#jSF+xO1z+M)^7*B}W!w4R zBvq0UrP+j8NH@W7en3!aH&iS z=au%IvQ9%GEP~^__t!=h(|fs&g$LLkR_~j<_FrL2)GVX0Cwp#9ldpc3?Cv}7@ejec zrcUdhbC(By;`?-N|7_`Eqis)Kxc#VJwjucF#U~d2JGbt(txwy|a4BO~&J;-v&lx7k z`hhGH`fn_IvD)!{%t@h_9ADQo@vyyr^?jmzqv6~I*JuB#^Z8qB9shgtqpn=rYyNAF zJ4k;Pc=bP|;i7BD%l~}}Q}(~`d--V(M_@>B#~TBelg|I0Yb*Y$Cgv4R`M&v54d3$u zF#6Eu_+~f5ul*MHayHNQf6Vj5!6trJL11!U-|t-W=vn_W_JkPiTOu6K8qPfJH)}{d zZ&g^|aaMNr>TO&1<%yfh%klE%_!r0W?*6kbpwE`c@AdZQcg6PFztm5vv1>P%miqr^ z{@-q8tuxZ?->%pHz3x6WBJ0wwjoT7$@4l_aGjsluJ$w!pr@6BAcCg;~)*mo+i;;%x zn>Vs^U-lK0Raf6`z2_tHqwgehlN`(5J^hCY?wp7gSvQB(fa6i4p@3Ryljn{jCj*^Y zSSR%L_VRu|?b2By#GWd3_@VWqBfKfx?=1INHHfX4zD=#e=jHKAi&rPrrxjTXG`GkF ze0U(fYf;}q*;kTPk8HbC|G3;*{BGs>iv=G~sbw^N2v9ltjm3Adh{74S`m^s=LWl6Ene^C zzQ8(V<+4+GtE#`f$? zn~${lZuT_a=-C;tZA-R{?B|5z*XlptSZ5=DbE?+XjqA_wb9qj)OHw~Nb(6=>tyUN2 z-dUvJ@RMPM|8nI!Vh&C_xD&2~|5$W;n$j7qi;vFr$=X$|5odnA`L%0iz(>to-PI-s zG>!G9JK>wkM+{dibk_xH2#vfVXV^~*BqH(lFWK09`|!Y#j|i-*^&m3p(P@3xf6 zvPTaeebri>X0Ygd zeY~lV*I;#8(oeVR-^{MB`SGA{>%TAV*M3g6-}=}4`rFFY`HQdZdK$yAYTL~`yVc&0 zo;H-RY`A;1HFZh+&eDZzF3s?}>i>{x6bN%*L{ZT zW@Q`yyHdFdTZ>=o7(}&-9QH2$+t;ov+A7{!agKZ$ze-O1 z=ao-}e|PT*Twr{qN+Yx*{NQc{(+h{6T#A$3pTWM+C~N^Yf3h;mBJlkfW?$y5Ae1}H-9%Dh>)iDBZ7$2t?{!h8R#jb45^3NT*M-Okb z3f!QpF1okEvc@X-XK#BE|0#j8{ulCRj{Tg)ll``nz0}p)*6{YTGir+_2O56+pfR~@ zTi)cqr+$?k-1q(5@^Ach|J=X*t*%cp*T289_uaI=8vHd=KR>UXS;O3D|2?3`#hF>! zZ(ipz_C@oZkJbphsXE?wV`j#S1>bj0SAOhurl6i7H{AYW!lG|>g0WAhzVLW|LZMFL z^TT^L&e^X2;_q9rYu{Jy;;D7}H~7{hDMiVz{(mB}S9Mm}nf5~xn|5w5c75vb>*@BH z)zvGHOkUm6y-{>q*U{8{PWvajh-Gx`lePL$e)7L&PyRz$E&WTY<6KLX?yV|W!1Lg- zSL1_cCCdU9)Gps$_NY|&m0r)vmC4TouiO-RVAtxff1&LMU5}YA#{2qBgao(OeAroE zE;hmZSX9qq3G2yr3T>t{V!u~-RF{0;bu%yt4VXUl zl-{$W@vC{t_4r~r4;j|Y{MFrej6XMVeL%lr(f^BYtH1r5>izBG*VkpS$-CAF$;!Qp zQmR|^EOBw)54}CXPfxU7{owmzyN^MrKBtU-s`~yb(Kf3%CzouMdet+>K`FuBufAg4 z?KgXDpKW!G3vs{4T`F4U-*GeaP3%_rvi&AG8d37$Z{`I4{uKJ}k?o?#-bdTLoYVII zIKO{O{Wt%A)wB6--n}L+sht}d{Fj|u>e=PxyUN#2Em~}M?7dC|Y_NAmt=2naTSa+#j?1{VA;meG2TRGI~ zu3FSo_N_m2Gx^`*+cr#=Uj7Fo-%n>uUg5eS>*xH#Nde{?rF0(7KmDR#_I-DP=Kt!u z2l_6uZqWHZ)ponj-y2oS)BE4oC;#@Jcf+~YreO46&z&aGQMtM`nJT>?rctok-sv}QGfY$?JmiT_wTN& zo0}J-9LnEbp&@DhG=BYSLoOrjV{;PUh};QzXH@y_Y}22IIuUO5-mkxlf;Ng({<64S z#dzVrWAh24mE|cjE+~Kbd^3Ii-q_gOW{@bO^utR57k^4%P+RaHn9xiU*cevTAmDAZ+!Gy8b!T64_ zz-tlnR}-qbBgDF;weLG=U(lEy>1=(3wRcl;!U9j5ck%Uqd*5D}9$S1)+ULiI2N$*< zP+$4N{fkp@Nmk>+PwlgPHkQrY(|GaPvi-@IPI$$#)b0tEzFu$sS90~G_fI76?c8vE z{ws^hpc}^%TRt?)AK_d0*oyNaZ}G+7kz!VA2J@DlpFO#(PiDcMejgV7qUV=Rh9*oY zVpI|if7;r)di}G!AA#I5&wKM4jB>IZHiVs9Q*5y9+P8(J?82{4{tu~hvht8-^2l2J zF6XE75C5pIpLZ$y|2Y1{Q17J;GpE{=pD!a0Ro}R!(e&_;kKhbnK6bB)g`QSTij7Q; z7py~XJz|}(Fi!YL>%V(@ch24*_T_gum+^$?zkU+0j>`Ux{SdYL{KM^yFXwD{_GJ5g zwVpHj6%I3&%XCiL(XE`lUFUV#n(bTj_WyWfT)l03_U*~G($gN@KHOlM zj_#S+;(bXuNG{HJ%50xLgF_{be0$^*7vB5WbMofZ3-ZVRs=P|B*}CV{{wG1F{BCPK zU0D0Mtu+7MrmWn%ZeEvLMEA%%UhvC%I!Ezz#~W(?yQUUgjL!J)C!42Kn4{&J`*Fcn zzsuWxX^Agg-D(iLH~f~(6#q|07PQ5z%>O#|p3QVAk*oq8^RU!*GIc%j^f#byUj z`y_v~iKg_?wHtn1>yHVXv2Rff-(|Z+dk++M96ld^d`X_*jn~o@`je6$eUZP%`eDM7 zP^ugOx+*8 zcj1DMpY0nwcW74b6_U0U0;$^ebW=_QZ&8hcyf3N-h zS^f|6?=Oq(H{K3^e=90Gwl%XX_N&w-;cSstj*1OJUbSVG0f8Aj*CkHXD%szW7kDA~ z>-6Tk6YA*}PF(_dV;UG%p7W5UsoaZ05xn(I1ku7)mNki9D4mW_!Z z+gIOL8kXz1f8INxss8bS$@TACK7R#|Cn{>V9(ko&U zTlAO9XZ#7;amij|rpW9^j+(4*9g8)-uaFB`a$t@h^NBMR)9hcwXYy`YlK;gg&iUrN zUL9romtx1C8S3cYmrC9tsAhRwg>{y#=+ti}xvL~3*iA#-W`1&s=8A}lP2eu?rGoWK6lQv=WnN-o-^6&W#~fXCFerK zm3x2R`m|e5>iw!U!50ISo)kIti6n(DZ1<_{w%Rjy#lOl&9VV==3)$lpKAv8f#&k}( z!mjBTb9=`6*J=0Or7rxk?BLYXQ)JU)`X$Pm6P%oR3N1gKdFfL5?1bmBB|YWWmQRma zxaaSWowWtuYiIigXm8O8-M=O>>PG4VLl=ehzt$Hx*IQ~zyl5Zlc$?W%luhkjG)&-Gzv z;vdwdA71sep0E4i3hlbgh&%tLU%YShZ|9W%d&?qp|G!#j>gG`^o@pnOnRoeP=$FTb zC)!qjIh%4U38yW4od%#Ej3erj`#@(`Z6aHHayyS4k?9tx8ZUcKJ(SSypl z;x)JWy%_dAmzEM~ke^_q&vc4eA#dN(LLsq2p|UGGjxhC|VoRtCV)-Q3)*!h*a@kD# zc2UQx(vqgB=ZvR&DnD8NFW}jNEsYz4;;-l*s_f%>q#>V?7AMFc;J0LU>K=(@=gTj+ zE;;PcyrcZrYrp%f>65z)w>{bK8x^rvvu)1B4nd>tpKgp2lP|bTZU~t>V~(w^o#NE} z3NGsNZCV9Y7Msa&GY@cjJtWy+R1KLr=FU)=+NEQhqOEG|E~VuUSD-I`;74fpLx&Z zF1G$wIuw%WB55GayHej5U7HK{aU(It?m^^WMC!)Z}8yzV5;N&%X(Pw)OZMeu`%`70MA-U~h z?#-iH*X4fOUDsH&>iZr332AF`!fy%2>=c+b)sW*)AWOn2-!y>>r@lUC_v7Uzl}3^R37E_F&!<*@^q*9T&Om zJ`n%)RJEAgn?Djvre}6n~ zzw!IJz4_5+y{~!hUe|HY>GdPe2g({Mcf12P{<`00HgltL!jea_r;i70F$vq5-NdS! z^~T!o`(N)2rd!4uK03eeNnoav;DVD^O$2LK**gZ9oW3^Y*}i?T+Y7g*ElxVy_IMxf zF(Y@|-TCG^^EzfY+RYkvAjvX?7Ebdcwpkj~c(`5z9RWmi_3Qs9KQfc)|PfU@E(I-P@7oYEz zg}->Czc*$~N-L=QBYU6GuitbL%bfaUA}f;b3#uE3Wcld7fBK)p^kx3azZ$x6_l<5Y zIJ8x~^V8{Cj>U6hqc6{S`svuNSONZAZP}R_3tLxy{C{KWvM)zi9!*)kYvtRn>r-R2 zxF*S$x9^gSTUB=To$W-y4ac8ntbd(-&#&P|tfWuH2Oi$EE7yNoKFL#)`hQ7HL->?) z<>EZkcPeksu1SqP%DVM%lw`s0+SyNE>5KS9t(13be3vSm^ep~{XhZBzp`tzav^Mz6 z*s2xqoV#&X6yv$6jNPBb|L)bP34dwNow2byA@c8BZRUSF5<7L?M<00fZGX?dX&=uo z{quh7;jEwiry35v+qdh^%)P~rcl~;|(zfoy1=hDZ|ANj}Jh?sf#wz3T!pU_(?+#g9 zbBH_ADRg4OhRq(9^H>B^Jqyp6Uagp(|H5B#y-P>pzh~zA3nN91G{x2l@ojQHus^U~ z=I6^yjx)1AzhhvJ-ePK(sgd`-UsSC)Bi_cN{rx{p=8r+Y3V$9CPP8^mX@cXYKxq<=1P`1`x1iqS#*igB-@nTHk2i|Mi2f7kNfzO^U8d|mwZe^KW@ z6~6m;ct-W!eG)o6p(bn6pP&6Rr;N$SGkJGe$_;~Zi{ulGj{ewwu`uF?EoBVG5d=1a{bo3DF6Ip}Q6Iat9W@IJEEt^Lg* zgPAhN8(w}r_}#lD@^r$>a}0{9XR3BC;E{;>m2*Dow{o_4uy=Gv$t#`nnweCl zKeBaEnH|&<{6d5ADsOS(z4@&zF9qi`a`@DyH?SGxq~~;OQ#FLSRhFL&dAeYs!b6Uvy1n;SgewZKoqVdHQuzBOA=}r|dNy-?Pb(CJ z1ZXgGCEk1;@&1$u&Sll`uW?{ee*xtv7@7kO!Gu;1Z7eE!4a@Y%cz;-@M&dCb&kiI6=j!I-$> z^4?q0C5<^#xNZLOR2*uw%ZX22zw3x^>iP`l*$aDDDFrYXJaqUG-^r&jF{pRjmFjul zm)mZC9M;k2@Mh`+ZXa)!zbkjJTU$<=w!6?~dVJn06aR>nep^C*P5IdIiotH7!OQ5N zuks)6EJ(3gr!Nxt=9Py_&xf2>eEc>1yt_E_-88&!?%@2t@Y9bO{#Wd4xc}{3VH^DF zLg9iMg`V^p^M(uMY!wT+7U({ocsx-|Rr~AL-d{?3(qBzh?YVWfuk_Nszdx$&x5?Li zQUBIlZof7A_4T96Z}0oGV|K|}?fTljAB7?=(~%v_HAG(qC-< zo`|PH8bLF!nsXfesIt27&E)T!jf|_ctWM-k)0AI%DC}#WycefgTu#}QxH#)_ z+fi0)nf#lL9}2eQvH#rl*2KJ~e33TO97*L9OYif0F=j9u)OiwC;&6gKh&s~vWHu%BmP?0NTIu2VZcNGR;f z5Yj)fLwlL(p_(Wg-c$3g@7nX@qxGihzq>Veef@s$@AiPHv8JZS8{z|=OUdt$xnJI+ zVREr9{N;|y!qe|EYRXfS5C8JL*s8TGQ1p{_S840IOHaEP{_e4#b~pUa%g9_AyY&H{ z`)2=naN*As?{aIlH^=Lgt1Qpl-s|I1|3P$l;MDaQ>Th^BP83_zbrnr6-xm-Su|V|W zql(MN8?5=~-()Ypce%mqv-?*6UuVVKZv^OF37;!+ORG%&JeM$=gEsj1w@=OKx=pEX z|KC1TV_kC*xmOSd>J{AIY4o69`bBwI^U=5Ul7Ihh)Mm~x`(%0d-??>PZ^>4FpBtUG z|NqhH+pfl6{{F$eD(&z0ecCq|+oI>5+jTor{Y2FRbDcjf1^sh4e+m~ZtvF+{N%qZ> z)2Xv&-DfOZ<0`?h$ytQqPOkV=#+K$gE3WJiwp{$)U`49eeYVC7rX`Bu`~CO4I%N9J zPxtB^N3MljRr7(lK6`(^+pYITHQE0E&+~87 z?SIc)|Kizf?eKLwgV!l0y$<;&feF#e}UTazX?i@)`GvyB^+=xNXgy2=nZN9%$L{=JWTT#3YUP<}q4(Z!=LwTb zYGq+s!g9yUj}(6m?(<#wm+e=ol6&-w-+Fc0IZHqORw&8;`MYf&=Xr(qKiKyB)+)ai zEOIlkcyHFk(o1H(JG|E!@X%j zaOSs7jw>1`@RWaQoIkD|&hoN^?yC*mgan`K_)^wp#x|tU&N~h?~6U9F2HjF$bk{LZ;SZ7&F-;6x|&csLLX#x9lfeTK#&;L!@959h>p5uM} z%|Z1c(~`HwCr<5VKf%w%5Zc@S^Txvk`?~&Y{-n3eS=r=jxIyOmi)(WKy<3s%9 zS=9a5<`MDvMy9Fh^BF(xW+bY!6sa!boAvu-$;>GRwp9>^R!VRT#Kz z&*Ay&?UrAib6A^M@35VW%&&=oS6F>Sel4&IU^w%hsbOMZ`jy<3p0@9 zKu|VAX$!}K_WIvLUq-Ry~@J%Q+H=fw9lB7K4Zh38HXO~%V-PETe$7<b#noTw|9x@0;AO@99$m?+8Aa>-P5AsagNt-2VEi z{E2bf@<^Tk@lw;?@@SZJyJobmXr0Trc5kEUyf+cW`59;Tg#2=nG&bqTeAIb$KF6s! zY0vIeE_`kIhHpmXRMx~>ksimZX8jh_`h9s-!9rbY<{dseB;mK2>Rng=OtezjNgFrc>)yXx)3cYzNF}vi+9vc{V5T*;@^}_Ya$faUf8>T zYW=PG84H7!Rz#luGWBlosd=GKr2hC_4SS{bg2~4x%zyR1$h+qY?Ok=B%%8%0+WY2F z?N3^7SR~Y6dx>*(xRl1e=L)Dw+xKa|Zt#{i6@!Nn>Q_^P_6y(LY`mbvRsT!J4c~Wh z>?-VunF0z)A4NUCFvn?aiS#e%;NI`0e=|U3Q?P|&=LD4_x=o+T)V|;9lW$#M6?Mk` z;ZF72MyZ@kN8GaZ+I{I4QIu#u*8FAWRQpZZe?J)6FW&R*b>+-n&LfxO{J!MsSxLQ& ze5<4z_3hu<_c!-yzrVS6_4gd(n;X_;8eNImZf31p)Vp=ruX$JBE}C*l|Gq2Z0<*l| zVJ3VbLDi>{16Y0>-BV@WsjL?9e5tA7(KImX~sYES9!)4os@Lv=5LSQGBR4_ zPO0q5@I7z*vG!2OJgez#96MYO*j_SHtDJkSwzWw_%V%bd;o}u*x8}qpzw+F_pvub0 zu+Qa-@RYw@(sBAr{-wW~Ec|i7$%hMLUzERjw$!^Ua`KOTuMYKpxorL_@Q;L3{>LLr z0}k^sZq1+68T&=-*3{pJCLY$zTg=gXUi!qxAu?Y^h>_gsT!ZR;T?GY{CM$k&(uXV{e{-27*Zx|QM}`D%vnQf!e52%sQ0a>V&@q?RwRo{I-{h+r0 zeIYidzs~W2>pEqvTJGK3HJ8!XcZsLP^WDupXOb2lJZC1=vg?Oqa@zuNMxB{`yoP2i z8S{^)I;1wV*!LSSOlVpuq9o~kbruUW0J}f?@&{oK|UTy*F41=${o7>6) z?k!L_z+2VDGbjC3YgJQS(qHH79m$`3Bx*MEEbyFRR`;`Si-WkvmIHCyANv@~^XG2Z zpPtOJGwc8C*R{LL-mcbbP~n*sdHv0*`jpna z>wkPt-#cC2{&|f4pScEaj4qTXaq99jKV*F`y6^bZ4J+2h@v8m*7hB;TQPVEFP$TWg zjw1VA`ZfA0IR$lB?JHd`wAA1Gx%pB5`RlVM9Nf|)t_T4^tXpzI4s=uLGb^DpT`)vmAG5PHdSBuyHj%N zllp(dI(@g6=&iA?wK?CnR9)LE{9N+O^II{THns{|WHS4Uq!!FYk< zpT|b_TB~oxv>eF%)*&PMNV{jDW~l6;g0Q7UZvwS$om5-2@%iquivNwg@;2LU9t?Qv z_g-r1dYf2Tr(G+&Uog#LFn-$Wy*aR{G~}3W<%4+*Gas#KdBc>UqV?Hj&&l)2&v%Ho ztf`v5f=}R#nFSw5D+5rw5aC6UWKwdE85)- z&3x$lc*oLCwok%WBVT@1Q`6Yz+Wq0ZsNI%FB|=fHF#=)jyGozVUG#X-!v!$`-m?^c zgzq@C$0s+&v~?wKYF|t9q*%t><0rRBim-J}pUarKIQ8S3kkM_$J^E}YfK=G^tu(R#|vQz@rT~{3JkMkk7iV)?hj za~kRBQ!5%IE=>m9eEl$I@fWga?rbEfCc%{?DPj{cEQYPx){e8Qh%i*^W_&h)j^ zb>8h~zuMN8R_isSr!%a6CwZ9hOR!+_e$)H)t%hrlsYsZb|VZ3K+~@x(wET*($wnTF zCXS^{_Z{;yx*z9>FPI~8?fU0u&)R&iX#V|megBsDzmMLh%HOm$U$4D)R)pA2Zl1(V z`$WBYLc`vlh_TQ+=C!!(r<`HZ*#k_%ednu=8`nJ*yO(`v*?SKDluvA%%hmL&Kgb_? zGP5-K+>fdMfB4L~&l$9=>$Qb^DZ9&`O$p(rKbLcCaXvYJM&drEj@(+gq}yM=MT;Ff zRm1Y%cnU+&?9RA|neR3BmsHhl=~lb$vthBqJ=ULMEI-l{cij1s^Toh~@p#A+FKK3p zA6=KOUnmy36Z7GD-K+c;Z*2cP-qb!%>64FL=vRH8XQ6-i{?)$S##M3SNO-&WN0aTB zH{8sOa#o&L6m`QQJo?`K%@+^udwSS;_Nukw%@gG{H(h*nJ1qaQnbq_)_wQ_(_BBN0 z(f^avINmg6{9JTs*Alb6@0y+-et6`YC|jyR(0auWuPiF~(?l0NX;xiv z=YPPe2i~G)|16HJ4DR6jW;&rpsc!4uZy+oj*1HN zRCi@+#!5bNnfj6E3wJ~y+mv4wDlUu$+ceYX?^bePPMR62R5w#;g{T>q_`JkL0v#T4 zv5E>V2HZv$j>S%q)`&d6@r}|ktxS~_+DE1?TXQD=ic{dv@{`2@2YzYQ{B%9&@Z+8u z$K;~0bBFd#ST_3(-=T*Rr*=M0VPkWww-bG3Ie(LO^Ww&U(=7FK%Kxc1r~Ya3w>Phj@&5jB?)zJRyFbRiA3Qr=_WF@-@cK;A>2<4Z zvkfwIFRJejS+??pp6p7$tNuz~{Q2(cOlCGuKa`oQVRfZCT*9IILYlxcxr=vXta2x8 z-&ND(VCL{5yMN+cn|+JjYZ+>5j$E)0s4EEC$N%*+lT&)Hb=VEvizj_A9_GDxHB{PH z|MrEcOs}^^Z^>DI=8eNw>ws9EO*4C=mi9)s`dVcT)kBc(~np+<1t z8P=;8B)wLpnQshp|J<+Ft!Fa5Axp!Y)i&7H^1Zmh(lV_;`v)Oz_Y6bbE<`Rk;NkA5 zcwp(X)lN@j*L_%)w_@?GJtwwz9WtCYyPwN8*GRNJ_{-a5$4e{sEZ|sh|I_3rPqd%q z%RTuD9l|*Ga4XZ_;Ls`CuDy=jee=_ACazxniI)xPIvnOd{VcF>*V}~Y)|=c7P6A60WVOsbmGzxMXKTi5T`yuD@A z^7e=8iM|O_*_abQ^1gf~EoE4jQDL&?C#NIJn~$?o1^%9^mp`rLC&Qu6)m5l2dr|f% zOH*pg#V1pb^~+0{_$<8Ol4PfA=yOMj*|L3eV^LGwjx#wy8BEKJ9>v%8pc z$+C@I!Cz*lU3*!klbtrLzo`1#pOv+rJ|6#8GrPNYe&p)ASAMUvS{)<2Y30{nw)SGq zx(+WDcv#ofyF8t2)vA5ZyX9B#ti$qmZr(Kf%W$0K;j=?!ms;=sT~IQ^dY<~xz6qXR zbSEn}{W+o3dc>A#$NA)v*-8S-Ckwo~Sa@&#voBonA3yp1zu@)TxnYp)%Bx-|CDX#++VlkW&ftqY=h4D174B#Ld{#Q zyx%h?d$Lhi>brNv92fRIJN;W_m4J;7zl`~#RiDjV8QIppyc!u%WpTm4WI5yH9}G_~ z`WSt0FvwZ+%i+N6((5|<(%)X(T=#avDkjFf*TP1bYj57rGd)(OtNkX)Xk)4E_S1*T zk2y?kxqB&L?cFCYv|9DcUroDu=kphLvq!6ve14gnSzP?V@nT9$;~g!*A4*Y^X6vsE zvAClCrNzbn>$D}Z(e5{Ee$V@Qe`n&+W3%3xpFMUe={d{W|Lbn#neLUDs^NYlTlQ)G z-pgBh{chXsC_k&W=Iff%JFjzW7y7zW^EmUxb>6-5@3iy1{_1?cy5Z=Q&70b%oz_{V zJhLb{yM`Ri2P)XBHF-5GsD;$p7N;(nOB=D~OGONjzgI{#Ev zzTB%X;9=vx_^z{CEDMe|4%@3~nPtI~K35=TTqMp0f zi7yggs3RxF)$FA|)%E@QSN*Tmo^93L9d~N&(bmLIn>m>O+ne3(J6W;%bmWR7{&%>9 z9V{KDL?)e$yB_p4`c2wle%(uZSKM#DAO6T+X5p1L@f-4o~eR+@Xc?$h4-g#U-@ z|JHBl&#ym!ZE~r--;()TU8l$9_rD2vcw*hcOqmzuEHW$nzU$0?8*%Z*=FCmnHX9EA znfczIm+7DzN_jmg9Q*7vmyDrM$P)<~Z1U+}dfcaNVNs zZk4OtG3mPao>S3F#D7e9r4-Y1N|-6naG9Ot?JmUOP)&4#q&j~70gndx(^@_-JH63?Yji`p&6cmC3x zx>TP-Kwji;$kc#mJiDI-Ue}A;5G}5MGirUV?9=`0Z7$n~?)=48B0FVvk^B^sE4;Zb zWqS|A{{PY^a{1p>1NXgwPAlBM7qHxTT)d^d>PdNuy6~pum-q3!Vzg7dx3ucv3rRiQ zXB?9{D^&li4X}N_>ACRIfPc|v*Zt}={k^34?CrDZ`+xP`-BEY?W!>8CEB6Vyu30{> zN&M71ZJQ}yZncD5syO#RvY;rZTkL(w4m0rH<`VO5sx8&{zTMV8H zwtmliCpi7+`A3T`@9W;d^PxCur~91w;jVM)eu~N_DV=ry zFTX00`+IK(YeCek`x-eq>+BzFsd3NA_WShXAoK5k$K`L_`)vFB*J=H?b6=b1Y$$)Q zi92lfYEf;0t*$AiuO>_OU3e!bx@76E)4Ot+4C4+R~m0%6ugKsPD$LA3TxoHRMkpKNA(( z%Upe8fy9gzJx`?iTf^4Pxiw+>j)eL868wg7ljPsOt&M4lR95`p?ogGc_xyA9%o83H zW*_;))R=L}?)mTjN_#=Sebztc8HgQR_1^#S$P$aExP@ zfb02lA$RY-*=fa{H|M!rw8Ga@IV5yJo|;4_>p` zKfcxKF3dgs;54a((0x(!tDf8}=lIY!b<+nO0}s{a`=S$e{O#afdiaK`(k!rbm{s(&wePql0Crtg-`CE@pqmbu`e7fzE(fj!uQ%_dpKj_ z)$J={OWwXcahq4^W~b@Y>p7CSCLP;mqp!=KX~u|V9)PNhv@Ew8y>aHWn}TLz4+{!)JpAz zo0>PO{$`)ncWj@K>E-s9E}6BhAtt}; z)gr@WDdk(&~pZbZxU1XN1WOL4gc5zZOZs zsuITKr!EWKYgF`)k?U$=dYF4kP_@ut&I{}1wu);ULth`dF!Mz;%fr&2{Iyr7hx7jEKBMy%x^XqOwi_FR7103qFO_|E_i;Le^^hxZvep6zK|M6#@=2f0g z1ZRA%m~}f-;^*N=iOZEncaMLOU3KoW+C97Z{u7c#6J2#~6&%!04WBOmXZy-?2fhny zdTXZ6TDe|a&G+M}u*}Hy3wPOUX+JM_<2wJoZRx*nn_Y{T8YotNYs0Sl;Xlq#?O(Qi z>u$O1YJ;!w9sUiQYrI2<#+oq`ZxfMNR{}(FJ6JYvlf_#vc z-L=ffYq{O?JGZAf9yoeVFT7&@q$3m7Lk4NWxtD+C{9M6Tf9HS`-xJMO7w(*s>G*ii zCilYGV-tm9#cQKXrZYZN`0hNTKZzkSBvL;pp@vg|KV_o9Crz;ppY%>`l34tpKYwDQ zkXXYX;mVh*9AbLPCoVNey7C~bcGoeZd!E@gi*2%&EUU@TQR!LzyIDF{W7ov+pHF8R zcI|%bpZKFg)yDm1Fo$&uW6OrPoXMZR6<3%##O^m_IC%Moh5KALA;wEb<~}YFIlIlsC8TbUXW|{Gd;GtHK}gt|`qy3cFa_e*QTbyx(QIHOr@(oZqL!n@*N_ zPu`()y}TsG;O>C~iF@|FDtaebDU!a6^%-|?+M_P}nO*IC25UmCizywYvqex-(2C=cOt-g!^EFY*DP3nNN>%Yg&Ya&4i-up z)Av63$FTQ&sQ=E4qD31{+B4^?vaqFceCrQ0S#4u8H&pk;>=k`2KU+3ih%T=aNZV6z zIsBpj>J8dU7W@+lYc@X7tZ-K>0Bzt^GA6 z{^^C&jmlvHkBZ-TzvN=Pxct%ViFr#kL#vFIs>>}d>f!Qvu{OeH>ykYoVlh5vy=3|- zxn>Do+)iTZ%N_*mNTs9KIF4 z+G76cbhEXYhOso~p?iPjDL$RQHlepbXOilo+V$bDB3=gmbh4kKZ$Izi0v1&IfB(-*cE2AP&p&U_|Bz$7n0w>Oxy#)D8y+>El6&c#aakW9mv|7n&$1b( z=D%6(6u>kmfW;)lZNWSTku7~vvlk|`$p74!{wr>=?8PNNf}{Vg_#s&)_IvlHvnvC0 z*#60$c*BvQ|Ku&dtly=_o;ILy?Qr(xTX$C4wA<$;EPw7+@B7&5Yxt((SB>|-F4^}^ zWr4r~dFxk~@01!Ed`n<7IL=zw&aix~!Mg*p)&~xS`mIUGX*knWej#m!zRBNezl{mu z4pXN8st9>FXUX&XqBGflWb%A@DD{2H(yG#4P18dkeky!YI`&a7f8xs`m6G%x z=&LR{&d(GlV4auGywTvN!rU1g|K~nTnK}F83gu72o$}Q>{Nf^W4m&j%x;^{(KmEP` zbd{5v{`kF_vGUfdcN^2(p6m+`D=jT8H?7*m$iu)SBUn^bW#M#=JxJk+x!p+{7CDyw z13x(w6`8sU(gIpnty^QOY^+qX)i>0)_y7H!RcB0f&Yo4eY^YKF|`~2b6w!?RuXVaj*_v}7I=_2{bVspFV#| z?}}d9{MQ?Tt@Q_LR?ba+oAH%d_%xkyGsB)2&5jTb&}#iEndJ zcyLBZccP{4fpw7=?*uWM_w(qd`sHyi;pm%rXMOXVci**3Me>$@NIa$MVti-*c}~W3 zp{of=hg91Xd<3S-^7OYoe3YwY6tt-l>Y_X#WiTYX_>-mHad9|z0* zRXi>l(UHe3-Id12o>t!X>)Wll&bx~(wp?Urocd?e(S7=_H`wXfx6hQW;; z%Ks--&J1uY=JlAn_V;|VN6XjVZ`~4o%hE?8{fLRy|J|$R*G;|0D)au^%Vb-!ELeuRYt~R;p{I zQQW2eb}MbQZ7W2~`K8NuT=PxUQ}L157n3-tipOQ9r@(=&-gou*zxKUZVz=S)(-VHL zC(d1atWS7@-0d9G5WZt!r`B^bCG2~y9IYtuqNrD3Z<5yT^+~I4JN&vdXYLU%xBb@( zWxhPUEV=c`^cwBG3qGyNDsOa9Dz4f$@6>$D^B%6om&}*^4@he*R9&SI%9kCogELe4 zN_x^OJN<316*o9>IMlB2PgZ=$j6T&$m)rGMXF$n{|5mF1hP#s1Fm zs@JmL5xZiag`hxgW#&?@DZ-2~l}$coauIF!7~-Fp{$yFWQGjK^^Ir-r3^DA_)mf&< z`!xnmyXbBdq`Gk4i|v9;I)W@uYG<#`o!fbB{;ECuBF?;hS(9{Jiv8U5EW=;h0uKIs ze2J@hyV@%~b;lj2w9ke|O!@iJ{!~QC)8*ESa;EMVeH&!5#O37tpeBI>f_shLth)25 zh4FP&mcfGD>w41-RvW%KQX+2KTD3BN@rnfXBAt5icg-Hz50*JN$nNE4aFOU0aHuSI z+n*TvVSGKGP}CzjHkf^vnh)YY<^W0pm)jGe~+F+HV4bJAnl9G;!~Gu z{CO*NvCDo&&#EVSrh-pexDTy$%51QczrAyA*sS~&JUfg8Li82vwaT|@9?DV=KKk^f zn*Ea}{jy0td%5x-u;qc!Q-;^yc5i?G!Q-(4O?>lF;SAV6o!{@0p+V z7<@>4)*N$O;c*)R~g&4FCUZZof8Za(k17v{$nFLmROh36FCN-7<9EuUrkDLYHzq&dXMDOQTyyvLM4glUXSNo& z-rmo4?D0WAa|65UJp4zW^O);CnH#AxOGqi?(J7^LwnIMxq8Hd!%wPQe_OFDz+*ud5 z|9yAp^`UJBw>i9ZRMod;OSIJ)F}UsAa9X*4=CPid%=X3W?pnRLSurJ(sb&An4nLPG z=hv7AbO$;o=%)l8jtR3}d*eIDWZp&xgTq3?+&@LMp6=?LQvc$1=G``~qZ8+61#UDw z#IW<{Nui5ZHkeq4bbkwG+Vm>KV6#bOsML4H+5>uD_Bl*Gb>&!sW9FvidrC#rjYQL1 zH05WW5^;;zCC5=Eba!_EgIo3-9tJ^yj&tF2)(OmA&2w@395w@kIu4eQV@fe)6LuN6 zREBlFtUh{SO6<~2EW*C(Eu7N77VX(kX}R_2rj^F3QqMB9q8mT8C>&USOHgmRQMkdi z$|)rY{m*yW>YA8^CtJtM-r7^0;aYck+TYuYx~*TPIHXTtVZ3=G%;weYryC!}{Cs|_ z{%d&n!cW)UanE4Hh+2R^!#0!249sq?tS1lmh$qeWSHam_OJh|=1Bq$T4jeGX64A+ z{5kRO8GC(t{l}C2Z|7FOzqR(wI^LA|tLObHz5hwndfC()eUHp9PJ6-Yw%@Hf@S^Ip zG9{OKC7(kK_c$FI9HP^<7YW&vE-A`tIv&fO^;^pQ^PN_vCG+oo+arBoPtQ-*ZvFTM z(=G1ut9&e~q(gV)9$XQB)!Aaz|I~SrOE-vfRj+!rW5V6mQ_n;0&s*X1Y;Uo4wfMFZ z%fr4*(U-gZ%$DIS>y6&IDcio7U)bkWP5V!*lzVbrzGL~FVvo4m_6cn~ zzH4`_k~ht`_tIg1kj%1g9GBSFT)ulHe@5YLe%{*H#+OIL{;H@<`jWfk>H9+`ZijmO z-^zVk;3roV+hhfYRM)*?TG}cuH%|6+ZIFG~F#m_Ju1aX?_Lvp18^?kAW0e;c z3W~(AS9CW8vgm}G8GM>MbG;2~aQxCoO%1BGD;Jd{HoT}^w&>^Go5H_~iqg_gJr4Ql zu3-?qSHb5){)r|5HZO)-Cr-;-Z#jM|w}RQcfLWh!TQ6tso<{rJgtK+u&&i5&O_}xn zqn?bk?VOAEdiEVVCoc5uYW{72pk^OvjstiJY+Ely{JSDb9ZN5&s{xxOuxCwSQ`d15)3o7xwi zGu1bdkl26YxcPc3md&%aOj7^kmVDUfo}fU12}i978-x7=-ZyNgu8W7i>J*S`EXm_w zeqpjuy2ZOh=f>h=P2WQo$nV;H<*BWw`_PaE!)u{>Da+Du~x!*g)=Xuxcrxi zUGG{qdt)mD!|rXCUvhu1E|3q{zT3c0A*OY;Rj`f0v?q}s{}Q}*IV6Nyl(kwqHP)m) z4!qvS_uEV=)$B$0qzNs&E|=e2dbje7)vDQ1!t17To)%}UOA)@O(pVMSKbzWXHEPA0lzv(H_Tg=`^V|CNR!h)kYPIEQ3 zI0drR9N%u@eeqY##w!-9cOG?3y!`Xh6_d4E#@5k&tfJu86OF$Td=KolT`Hv5e9ANRht9IP>w)jSWM^L2zZvp#V!Hd%iDsTZ zHKyHSWn5KWdwi&2^8!!SC!0s`zWLk!{&xJ|2gkqt z<*&dT*3=w)cr%64{^^v_@;bW{h_Y*rdU@F13}qNxmPnq z7x9Y=t=wPi^~|OJl;A;6;~AB4moq1AdC(=?`egP`r`ky>3d}*KKuw zFO;mjVzge=FZ6gZ@0zO{(!WN0t*TJ`dT83;;OG*+oNJcbDr?hPf62d?Z?13R6R}l5 zR@|%Dbj!O>CyEmHTLsR(heOK0~-)n1WpSy$I?D9RfNAdB3BG;J>^>*!C()_`^@j%u( zyEfqlr!QNd#VOoZc{pKT;X;Nr#S6LPg6f|)abFEqvwD11z`wu7C~x8CPa><1t~=Yu z!7^)$kyzDowdk2w?tG}MeW114!CrEKMhwT)&Ud0KEuXkCSk}6aVWN49Y>9+`eWlaVkICiCyBmvw%pW_c_^}40eZ27{eSYn;r=9Xv zmI79Xd!%GvpJt7JYRbLayU~GxS;XYL?d1z6bLm+}4i_*g$etSjzP!l5Li(gkmV zycN}EO`09@rLI8r=yycF7((=hFlUZlj|7w96Y#wAv!wOh8FvN)6b{Q$!ku7wL9tvj~=2otyc!Hg$w z8T=aO*jsLM-~YGQ_Sc_sssHEp^1f@%un$+vbV*e@wfITO{d4N;|}`2fXj75fXGx|b?#pN=Veugl-~r;{y?BZ@FUX+o!y%-dw*i!bL;WI;fwT0<}Z*@+t zUpCC?o_0w|%XZP4j8MCmTU__0{WTAdo5 z$f%70zau){bxkZiHL-NH^VEyE>$7H@kzKoKhH%N&Pp7WTe>zM5N!9^_#jyf!oYZeE zyESz+=i+@H|F?a6+U;~zJ4^a-p1eR(^yJdYCAFW5tRW-7iir zaD95`vWxwqNg)d-=mlg>UAsJ%V@dm~xwo#Bym)Y^@in7n>YtqNPyYRQZ(KFCGow4Z zzC%iJdR45wEW_veo58nsr1>Xvv$yuT zwC2p5cDH;)%HxqZEfg_fqK_ycaVs7`8QZ z^odpdJgQ%p*xnvxa=Oo`ZQq9H(x;~7$E7AbbeYA^Cv-Pv&b*xt4X>LFieEMEIkM=~ z*9#N$rKOFzFCMGAtgGv~(f?V(mdz7*YaiUYv*sCxy482_#_Wej=nwgw+m)H!Z2Ezhv;m_O9xQ-cG-g*}rLRG*&x%`(A`v8uU|V`19aCsJx@?X1kZA1Ch9dbexxCD!G<(nlX&NaAao zyIlOyF`15?f?{hf*RZZSP_Fdyv&pts8|U87y&v=F(z4~kvsc=AU1b$HVyklRjn?$X zwOi$bDi1GvGKInCtwrmjCJpW63dOx8MZDB_f%2QJjJ8sWP0z_%Wu`wHZCr_5PtgAPKL=!OOIC8 zul3*NxVw8tqx6nO=?cLM?>=vT2+HQg1#S-`k{yJ5n!_);89%MdYlxRjw*PE2VN-+z0{d>Cpr~cdJ_J3dg5`4H?!GBA& z>VT$*dH=n~#e@B=+YIp0jz8}lJBYd*S1xK5T4+jpn zJ?uWkSN`fT+d8&a%nL8RxBEYL{w9Z6r$Qa=f8RVGvp^*1%4>#FM|ED^fah~QI2`3m zoMa zN$blVFBuk}Im!H$UpJutOX5X-w!SRi)+g_J+{$Kb$_`s~-~3&bZ`!U^$v@wggkJlR zDbdq%`tLQl@WWHM*cPixx^?_Xe7^Tdo4ms8gCD)$y|G(jzvgT2d#5)gYnSBQY}EZf z#W~D-{iKIpISEAug})14Yc5z9@`X>U`t{efL6LLLEiYU4@xjTTEC2c=pGn{UYw6$f z|9=|qUTallaOUm0vpFW!NsCV=@7`;%dGXUTe6LR}xDaTzA<%9^9sffmo|IF%)90V& zobWvI&^{`=0Z`%x?X?`Qn&7k~TTO#gmA&{EUqXkh9rpEA*H6LzoaOe`_* zGTE5$J?r?dx%0kG<)61N)br--x%ajHIPb14Ip0?myH4hN?9D0bS2_H5@wPcL%XVwg z*BfGU)sH2wO?`gnM(y8}Fa4*?eUv|K`Skl*<&vrF1==^Ki)J>?+tKCZ8E|*%BL5j* zHgw7H_g}uzt*|!C{?t{O(@TnCre>QbNGbn#v`TkBU-xCR3p*ZF-l|u=7_@AP_tSZ` zWxJ-mnttQpsjuS3TYI}JGbjH%v`k9B%d6Is$ENmLWbKoWyCa_-UiWd$YR>Zd7hleO zeK;j}?Y7r-UnWj^rDd>zJ6zv?nMx?5r>XW;{TQykCvGzD__G8n#WHL7cW=ml-MIQr z^1sbzc{9GV=3cSiE?=0G$o^(_eaKyN5i!<9zoJ&^a_?Tw7quk?3JOE(Ok z8-L**d)bc}y`PPQD2o9rK<)jlA{syr{!m z?yZZBFN$q{cYaFisdMcAcV_y3sb9*SpzRk^aLZ}Rspd;}eO|t}=q9))L~3^8pYv;@ z#JK*O{+HY3G^6DKugcq)iA$M6tRJ+nZ{c_x|Fo_CEBA%_=d0iUd^vR{N9scV${*X$ z{oWQd|8nU4>Z-zbhurFy_$D7R2$y%A?(%(ASz*~Mx!&j(zc)>JwMA2Eo$U?$S%aJCCK`oRrR5x-VT^r*gluK;bsQh~t+X zUO4+6n(zI8_Wj>KlrwI(OK0itep<@;_s4AI(<{GR;NO1#{F?QP^iLH(bcq+4mf7IB zWl2E@^Y>@YQ&)b{j#^N^v{(P?H}0vYol5RY_vM!D+faCM>;9@A6aRj%|9AdP{Qm#S z)q6wY|NpqMY`f6mSldNKN>Xn`r1+;yTlXU3*V142cO9Nu>LsmiHO0^V;={)$FRAUl zZmWN8OZXkHrps)9bzUUz%m`AQcqfM6BsHo(r8D{y``)Y9!m~`?d7p27SvprcuU;+k zjtl{!-%ZxPyqW*+vj5xqKhNv){|d|P3ZJ3s#MryLGL4%xnB(V>-z*os{AV1? z;eRIWeC2&8L+rbz)NQ-#Z;RV~?tOh=-Uuljvr|WlG|J0ZJQ*~r@V*KBCv**7#{eRzw z_1x9B%Qx56MsDce_ig9<{QLT1>rUIx{QpJ$oB#iB`fsQ2|FQP(EBX4w`~QC^@3yes z8ejWuX6=`Qzsn4-#sn98{9xMptYPW;wM(vVk9k?Spt0Aqis6b#+A(vvXHN^0ZaqF& zuw-la`N~<~(^<^-|C-ts-n)F!#2sH*>Qqh6uGM(I|Np+vMFI)|=RRzm;eW87<-vQI zf|}n_1?&DjKeu=Hx8L{w-;=Zbb?M&k2h6`eJoYzPd)k!U<1_oc$I9t1eYdCmUBvX^ z=aHad)w3cs?-tf9**fu=@htw2w#8iq<<`%1&-gzPuNS|hUfz1PCiLAI-2(M>dyZdS zkr}sCza?{DXJq}X>N7U-C*MWZ$Z_10-SGBz?w`%ue%sx+{%?0?W2New-~TEZtT^|n zNH1Gx{E3%$-;I0kvz=0=v78e#_BY^Ve0X3x<3WKwzxiG~PAo~~7QGXh4(pd0OsoGL zT`G{Z!9+yL;J!c{hrlXZ?zzty)j~^lY&7Oo;1xX1`-bgRvXM7;mS18(o591yg7M-j zilr_;KHfi<&+h-9;^n-L4)D%WjC&z2wRobV{gQU8nu`v%lAK&>mNb?$@+iy}{$ap; z^30=o+ZO0;zgTa(hsXHKeY3na*$-=XXutl#!*eHZ`^DqN4}L6Y=Ms*d{Qg})?SYDn zz0SH{+GS_+TbaEszqBZv@$A-!`BM+IzH-aGEg2g0moMYxbB=&SwY6F^^*0Kflb*Bn zOW%*yiB}b_uW7g-`m3mi>IBFa~0lgoq9^Aa+=o-r%T)0)b?j*TnaP4Us?H6LW8-d^dcuuz%PzB z!O_!-t$EjluX;M|)8QQ}-#dLhHpy<0P0gWer&^xRHd~yr)7bhgo1)>bd*Y1-s+BV$ zF1u~z43YRB<9;M(^2?M9rAs}9&z$8_;qGEjvv*>c5pv^v_pN2!mzI4F%JqM{Cct`@ z{+|?vL*gtkl0H!v8M3B*jF9k2WD!`@FO_D{WA;?CvE^3m+cT*x0^B#3+g*~}eeQxU zkH5iz)1Dr;yUcEs@pJ6nCM9vfEdFU-Y{!?l*6PO4#w-S|9c!Ym&UrPbPB^iLE9U9a zpI2X8IQBhmdo}<4ZQt+LT)$WQVcXxY{r`{c`~KJe?ZIYq6ZYnhD=I~6)t;{Z)n57D zdd)(1KgJc;Tp2HQw^iO}w#qwYwRF0b*Oq&Fn@{lnyWVl+)JwjD!X_)eSifC4)jX1a z|J06k0+;4bkNW7)yvO=>Vo&NE@!XFLulGb*y;Ur+nt$$&e)(!;5od!f0r5wy8M=dN zxBr>w=U4k>%ZrzpdWUVM6y90?XQA&Tfv2LoVty{(>iyX8{XDUy%%=Zip7Q=Sll$S8 zTIni(Vz={@x!)zP_;N>tsAk^J73wFF;6n87JCHhA0s@xqV zi^J0#)r@Obe9sJ;Wf3fMVe%`NzirniPk6n%;yCj`ZYQVr(`8)6bvM7Cx4b{}ZjtD% zEfF`4);w#v`0?VxMXL2*9(#VZEuSI%yKmp|vloAVN%8Mq{7j$G-0|}J^$T<3Uaw#E zz2mizhWf3KQR|C}W-XeVuX!ERcHhw`{o`?i1pE3!@2xhJrA3NZe{EjR9IyH8eQrnP z!_Q26BoC#|J!-{fr}Rhphnd5NjGBAe$3>e{S=R3lMI5-TGXO+}}*k-8jnY@Fu${JZ|H1#!Ct(w9{ssR99F3_Tr-Qas7Qi4qbMTdTFr8 zXVFBrkjpa-FRDIKJn9JC62E` zch-D59B*X*I`MMd?A`ZIuePt+I5p(>lR0dzmOqrIeR{GxX6uFTv1hXWi#+)7=eybT z<4R>bC-};)?$OKTD&9DM^Lc&4tzUF^&wVISl6A4;&Acbx+E4nc*NO)@p6L!<%q6HN z;uIpVduB*M(461zema?)+%0KvX^PW%;Vk!;AOE=QTcY(iyEy-<4_i$@7@LCo&eJMW z3odYhg*oUWBZB|4@Xsp!Z379)YD|8yqtfx2MK9sF~-~0XT<#PLt{P%0l z|NA%j|E+iT>l4`7+gJJbx?eqXQ9kBu_Wg;m%!w#%~>dZ zV~Xcext06-ex)1`*KlIt;i&tQ+v(PDD|X}QdHW*2ddx0J+qExrpJv{#(2R*m%U-wf zEfjLUcq#Ke-`$;;Z>$M9rNzP&J zo%7XWGZ%i;NXl3#81W^nHTX?^M*U++%Rf@qWNW z?rmRI@6q2al_baIySVB6g~bMM4mNyv{P>C9jm^fBD$83$3oCwST{`^uIP?4 zyu6%#m-+DBvx_wDy({=Nb)M|%xMg$ZOq9P>v;X?_Kk|+Z&q|9{xjxYL+kJxj*U}A9 zWmldrpX^y@<(V7rd3D9b&CNL(?5pSXzKZ#^PqvTG>Gk=)^@Z2n{8z)1@#3S_9oFA>)^q=UnH~ST zV&ey!FZ(0s&HiEZaMz#7o8$lAvioD{llKQsI}^}v5ox*u=v#)to=3b9RyB4_5ZuyZ)^Kee$Xz_;Ynx3qJ++VS3d;@#e*JhS;u4Fh=}ltamwAl{NYBR z*DQnP7Uv(=8Cw{2d>k(w()(<2$?@KL>3{OpU#`q7Tr2YRaH-{+JMN3TB;Afz$}ae` zklm`_h4KuJk~0dWZSqpmzZUR*x%i^&U@7}%{qxWKCSTxvD=MY#cS+=9{-uB!f!SNF z(?#+W>;fJaKHR|l+hJFdX1wFmoYjBTvaU>=^=n=(M~l>lHMLs*&2s9tpPnn%7pEwY z;XmQy;Tx>NR^nx*Kaa`Rg-IEttIxk_vik2uo8J6=7OjS_d%PFM*Dki())L7Uyz-e> z)2TmG4y@WVnJ3X9bB+XuPGW>hZOGUOo_;qjg&GSSq zc9ZBlGNjo=&NXmuHFAgBAdbT zO8ecLJJ+7;TU%Y1^Y`ePV_LT-e!3lOP+NQdHQy95mSfCIec$qk&AF8q|Bq>on#lU& zDx!P2(^L4nPu`WP{xf0!#1aEueSCY(<_Oe`kG_ z+n0KH_xhW{EsRkCvD4-sD)mX_SS+HZFoXMMB+ui!=Ph3BHqR@~x$ypGKrQ>^gxX0j z=61@2^IjF-_9lbpO~*Maaj(?@zn*60P4#hY+u%^Ra?NxR%gS#xr|#%{NqBRpA)Hg6Gc$=gt|(kuO!1qtg-ro(C zNE5ynGNVv5ATd~+ZOxs3PS2b~nIBJUZ?H^z^ee5&>PpF^*nbA!ch9`leCkAb61Od{ z*Yx)i;wc6zT37K+4ZU>u%2KxBJtx*u8O)n; zJHFiLe^c<*J3}{$^=f8l8UrYuUSilIhWL{BIu4ZT~i1{_lml zKQH5N^WXb<xx3OrikCG)zI?$u;R~E{jyI^Un^FLC^cC5mt-tD9J*^!7kg9s z>c=7XK0OToe!_i`#8thw2fj+J4xK*dgTsy%1BR_Z^*4immb!H7$vm$9xp-fubIPIP z`S0y7MLe&5m)iC(>+0LZPc1kZU5bM*YBe>qzTj2UU(W6kDCDA*(Yn`Vf=|?vqFb$o zvt9Xl((dQ>8CG7EU=(BMA*xMthDPv!z^`>uHQxO8yr*rlak9&Q(Lq{(mhk<~SV zVvogBoL_vgJK`YkD1D3d%=|qyty7MMzi>=!slKh7QF8xJ`h0_iN>PD_!g119W7pkf zzLnWo(h@6uH?CHqUwb!$!`2o{7EhDg^Q_oUDTGJvX1K}9IEUfslJ--JTij-RzsSDq zb@2jGy_2h6WM8qr(0(xI{S>ahC23D@x+Hs_&y$sj3a_?0QNjODygKL3bw0SoTi##$ z@%dNHueq7mF$V4rcf9gBv_P4s!ft`gBAr{mYG2;p|L@ji`!|iXUpMRjdp)f_OFaDW zy$?4Xw=jRJtyks=u)lQSap(^2Z#(0~zg@n*@7u?uhr7P~UvxTOK4yA-(ZAjIZ@A0v zTVFZtS(3wQjD52n8`dBtq}S^AcKc4&po!3#(IKTMzY^y@v|7vFCBY}mBiU+%V3_KJ4? zxb3&g-)a1xnP0bJhOlvec-{}q@9Tce-*7vS|3#=m^rd%gPnLJbz37^=N93=5Ym2S4 zn$3T~O?M8rzn0!Sy{o<%17&<2JN^Lhk)Zpwpss7XD`4-LP@rfC> z`-D%}Jao4R$&=0ukvZwe!Js^IGDD$_`W;R2ONY)Chuq=NpRxXW<2HkaiiD)!i??TK zwu-G;@{ny^+a|5Kk^Vaa4vLi(H4;DV{ z{UJJ~cAaBu<5#UWtIj+UIcyffCersP=zG7^F9YGrdEe8o`hK5t&?A-c!laop)?WVq z_M9oWVxRs>k>^6Y!Oz!qY0t&?Z!szln^DhynRVGJn_y|XygQkOf${!q@FJG%;%b>3Ker)yV4?iM|v z-h1+NJX`uF=hGL=a=E7LjPjiP*0yMODT0{vK@a4=a(@z4EfByecjWtH5iYRMZ{D49Q* zy{UC;%Z1}_rLD`e|1dbeoNVC0y!8@W^Te;RhZ+{{lhwR4@6C;ahNmx`Yp zKHm5H^7{GNHQ%L8dbRa&rsvl!QOoA)-McoW{#1MHoxOWj7w((h!Z=HV=T}y{&z zJsd(O*PJx@bkcOy=<0Vb{}r&)eL|~1g65U)c4^wCosP?j0wr~qYNf9FeC9OI#A7EvU0Bu{ zv?Cx|%j@ONX|Aa&lcryj+&Y!3C4V|s_wPM_ZJBisUA!+{Ry*A^{Qa}6h?f?WAm z;dfkL?^by6M(aVAVC-aGNoB=)YO#?{T|mub%q^|lw>AaGao!t|58<+Z0xJ@p(elyBi>UAv`2 zO^}U4@JQgPISoz%4{|qTwx3~qIak0{t!3kNCh5=G=twO4+r^9TYaJBly{QD zWc#UYk3M{ibm{Rr#xeJgs$%A0-~CK4tQR)VaISAv6G^+V{ce+A^12$)Rjen(@8=%x z`FQQM*~4EV9?dh0lQ~~s(mS(4Q}X;S_N4Qglf4}s=CFAbtDUO1xH8AWL0N7jUQxyCQzSt@<%MC^lx|0PilTAcZR0(M=P z+J1e;^@qaZ|H5DXT9o20bD_+O`}+EyEA=f4H+4;$=vVV`?yDyKSBtYbnLWOh28eFH zY8PO-_p058U1v^6CmwkB<@cRz^*4I|A*0{*cRyW!arMQv!}~s5?mX(!efz@mp6EBs zt8IVYp8kKv_I)Nh(ta~H$!&jVS0QJAz47mtt?O&|L{!ZCwmWiJZq3bt*ncM{*|qmi_o+QvvNtKfZZYp7qkpOj-*+C{S*E#w-&o71^xA@{&r;Qo zAOE&vXDrjb3mFzyqEaW-eUWEi`2WSz#WCbzPo2Rnt*j4|9;7ZjbUx{@x=-x0Bt!XR zu?y?^*Y{T5U$tq|axdxRUx&@_ZP{J^uIkZEi{mkup7WeCU!X4$_}Bik(6iT_?oHNa z8Y@L^NOW4Rt(REtnSE;U@2U3N5;yEC`ro4XUBkZrdb4lga*3cfnssRo*QM%i`(w}5@7RJEvl1&Eh zG>P2rjBSp1$%dCHOw4RrFPS`dlB+-!p1!1GJ_}A^L*K5(n8`blBO;EyH@#r(tc)Bsh6Ku*T0-0WzXoU!?Sty z>I6)*HzA)U^GAyz=`|9HoC7u;kMX&9)rrH;?@$bH~ z*{im7)tuGRd%nC@;VDSd^cGC`e<@bx}X(-;;@F_x;&B^?1MT?D!iC^l}#L zF}x^IcIBD1!Hb)^MghCryz+%uChWGmK6Q!CvMt*F=LDu}FMaG|c{BNW59gbL$uny< z>oc=`cY4}mpkSNOZQzi%NYhW9C*|PcC7)|D#1AvCerLPo-t6}`KKx^@uDRg-QTV%* z=q7Q6&LF!f>92M&Z{Do)CznM|bziB$w(53o-@2P$PkD0Is_%NDcq1=7b>{S0Yvx%P zZ~GR^_&n?fC*!)zhp(m`@;7E#Ec|To=EQ}WPbd8sJ*2n%+ND!fdbjj)Hnv{zg=edjZE^@MF)&y^S@))zm#?0UaE_5A5uZ~tcTxwEj% zYN@_;;k56;=~}@d5*y##*gIMMm|g0TIJvHrJ z!FuYy#G zac$nI^8F#dTW7wzf70*EBKxVjJW)%nAAh`Z=GiKBiGRkXX)o4YP%2oq=>OFc)mc6z z?HdwiYVOx#36OVeOE|X6!DHe@@r&O&XXtsId{)Bwnlb3T)ZdT&c3g_0dwG}`?y*0( z_;4Xd;e_PpYZ5MQ{3ZD5$Zhde-CniI-PYnf!V_Q3kCQ%DHjCLfF37xSvaI#Zaz+z2 z-pc*YO5SnqX_*FU-%rUeYuwKJOZNN^wo~&iaXs66y+3CU%hPA`e{J8Cd-SfK_Y&nd zw>S#kPWh+p^R>D~)vC{ze@ghQ|Hl1`ZLS)xcy}@=R7=$Rs(<~Gk}sk0R|?~2WTee} zX|22Kjf1UCxaHT%$;;BOmgLR+waPrMI^q4^|NHLB-cg^(>m(oK;=VE_C1BDD$750< z&!W#Q5$~$HqISZ)$bVG_OK74(f`YBww%Jdg{d&T_#&!9-`1`*z8D7X$^UEKc_Gw*y zknW@To0c3EUcYZueyq1dL2SpbyS544Llo+*A{eOR1$OuqEE=BL*V;h(3tF8m@ZAHA;f^zHY1Qr+9#<;`x-g?tEDu zx$5`vxs_{}lu{P?Z4_X;|ML0l?dv$hFaG%aO>uWZ{_mu7`C zUwHJ}zU1AVvEw@9AyuD4%NFgJc0%%O)So@CWIwKE;3%vpn00Pau!R?M)v8nw?d5?X=jQWarH+xpy<0k(_TT=g!W$ zrKh5nIxqd*>N-K7_{P?&+y1H^+OdFnKaYIzjfvA@Jw)F+iM^P;;>^}7R}!}iW~`nq z@ok-!1WQ-fsg1d10ZP+O`DmVQm02G3SC=hzc7L%p|L?Nb)$ecr*lVs^|F%!py>`Fe ziWsZ#OPd7vE|hoFRm?EsWKXD2eRJtK$6v+JU&1S2++M+DcHgkKYk|Z4M0TY$43ZPK zx|Nq~oAGK|?8_C6GtA#6+$@?qG3W1dtC@+*ey(JD6ga2$4K<`1*>ZCLsy%lCuU&gP>vNmpyMez(@&pBTF~KkMseZ{IWL>V6i_{`YNq zm|a~;tRHJgYwr}c8-F)A)FeGRBmAqDcafLa6-MWlY(`=bmB+?l|5(cjDq57C#u0r5`%pdb#e)kCcx0mm{V!yi(-p=KFHXTk(E6?X$dq3FvN{q$xsA+n?O`iQKQ4E>+Ecod0xoYS6>6)ZkE;F_UG+l# z*TR}PYE#3@zU!}AcXrB`?oG+dkDl^NskDi|b$Mn%(2bumZn@!^O^nu?QpKO-w5ol% z+c-URVSf7OQ)-tFscmjOD|Y8)-0hMgolAl9_wEqz6`OTO&-CHD8$0fAZFWp^-db9^ zQ7U*wg?7633y!K;5s@)JWEgrZo2Kvk#dEAfLcO8v|7QNf^($see7nzPq5JMY?S<&f zqLp<`m(|W0l~}d>yONgRa?Vxxbl=xq$<}XVZ{59^yZgHJCdI3@b!nE|hK%2z>nGf} zt`&cATI#IdUtO|ec%P^To^urzU)1?*O2d4f8B3x&uBexXn78*zdcZ2 zSoc}h`ki;BzP%B+6>{;}l--x^1xmkKp_?hX`rrGQ+S#Yvw?tm%Up4=W_BtE?Dg6$$ zO~wsV?2g-R=oPzFcKv9rLoI7t{i|NHXP>RF3h&KbalZTE{ME}2OkMhVr2?nHq4ihX zMP6xlxfV+~{O!29XMNF%oey$ff9Y>&za}ZOvWs7b`8WHe{xb*WS@HLt*S?x+E7VtQ zCSR6g7j(nDR{Y;9=A$c*&U&$M_aiNg`Km**>UB=usX47LmkYG5a=#dA ztQWYxZ_D0XB~6+qd6IJRb_?mW@}dZ_m$t9p`7=u|KgSFsV}5U zW4o$0*1M&V=Zocoto9PIAw70$6Z9qRC*y~0F7(bdYcH}aj4_3WmX zeTNRu*_YmW_rruG$IsuAvp#EB!{m8UpwiXYJTXatZX;q-J5UE7ACO>>KsZcH@#%eVEw1bBw!yur*~9F-29i` zReJ-7A-f!;Ry(Opit(k0__C?{>0R#8t_uQ;*=6<_+w<#q7)^a@(ev*RFov zC-$Xzo$Q{k%X;N)HvGAkyZZMmkBqM|A>J2z*lsTR#$NS#DVrB7)3Voj9P()=F14~G ztjiCm4ywM9=_HhwcYnv$zfFo943RtRPk*{_D3J;G>f2vN=rD8#6&z(er>6=&TiWgr!dT0N^z1p?qcEvF^zj{Ue z?Y2LCDSW!C^VRB4u|H=9Jt=A6TQ-wxdf1XFD{WqE554Cr{k;B?X6LVtssn3vIS$D9 ze0nn9XnV-5f4!$7U#SXAe0tkj?r(_UiIflzc*_HIW zuC&&#zEow@9^g8&eBJuZow~b&mkPQ_7=%Y_q=b9wOMSny z!e@$4nWTEL7W1WNPWmm3USBu0-PHa(Nto%RjBtF&zS63z#|^w@c8Q%nQ=W9*%Xii4 zzPYQb{KaLh1B1>#OA`-LcRem-Po84Xbl9_6Tk`^~)=L-H))krTqN7;t85u z>%+vAriTq)Y!d1-4vj~4&dkN*_5JKBGK-E1LyVR9_n;hC+6CavBn_A73MnFPxA`)%=ly$R1^ckr7C2hD8U!uRvJu$KI>5S2 ztG{hm_nGL|`en*+~Ug@*s zyx_~UQmSeS7q6A{Oqveg&lKdU)Xa<@6KVFvfQn`x!WCHwA*zb&B;p7`Ihi5{ASWR!`QEJ`j#gx z0_MuxDk#wAtlq((h1F9y5^bfS7+z7`T{~4d}-#2 za|(Bb?0(thwku2iX3#?qSB(oRzkc+py~V|-zWr&}vN;F+?H9jyO#hZF&cqOM=8E~d zhsVsjX8&l_@-K&z8~kfdH>Jk{a>@2c1(Kq;GFL7-~0bppSStm=e(miu}VdJ zcf^ypRs)&sH`$Lh{m^<4r?+1!!)W8gN9`{z?*I4ewI1hU!#7Pgj~@AZfM?mo3pbSf zr1*HErF=c^oRIgqedDBT(5@9Vmv|D6+1=^u>)UfsNbVzpqR9?E5rbV0W8O-CE1y%Vm-ERa3VrpBWp@bwv6D|gUp;hz z^reGagO)UIsLWsB)cf2J|FX6N|(oW4x)UoB~5MLgD!$A7h;g7M^d&?6Z2UX73(bb+%XMUaM zLs@gX_fE|`)~nYS?Z199cfpV9PtTv~UY)qd^Jv{2d5JUMLIZy)Se*Iddu{W)FeXW! zing=M82;uZA8mc5&ZbdsIWg_e4*N@rs&8KIPrUHygyCiRpCxUkTOaO|I;!rl>AOOT z;41q*k6$ai|7g^Nypp&2ys24J?LR~1het>Fwt6o99Z*}Wbjn|?BhP~OVz^yGtjaR4 zL{sZO;>N4RijP+r7N*rNE!#PxP$=P5Bf#T+@bslGUTm@7^0+zL ztolRSP3!kjYG1c3H+t~Au)|K2v#S0=(EV#ts}ufxpYo(;>Bh9sTOP4b-Y%Ya*DA#R z%FKyMODh#!PrPQ0Rhy&r_}7`Vr=35o8Rv3q%czH)lzrm(fc5I4U7D{i{W;6sdOp-) zh0T<`r%qH_r7C$%mhFj}+9`bUR37i~PxBYDM6R$c+_lTC~P0mVv-hE5& z!}b0$>3;XU!~T8B-PTvXn9|Ia zTP0L&mYx-R+CYE#sq>4H?Q5O~|K9&+_WyJL&c^=@-hFxE?IJni24GO4kPAD2WeFI~&UDY8W+^ zT=7($60#ycdS&c{Z(r6Py7axJ(|+Cgrj`X?vsjbE3%?(q#?QaT-X`S#WGAmf-b-8G zPj1UF4RhE${i}@0_RfF4%(?a#0;CW1?rHe_tbCsS(tum0N#FLJKHc#%+9+QsZ;$n* zI(LTsje>IDVnjtH{uM~~T{T}-d?ti%$yM)rzx}SRywtzJO}XPvby4`;Rhy=3<-BnX zdc_~QO(oDVtuD2pj_dgEFEPKIStb{`&oB;|zi^*K)0%7hI}Ixo+yHeShbqL$}&iUHy6|Rl104vPoQPoOtv;?vP9^ z(QsyyUED@o&!jfol>2wVBl<4u56%U-?CWmVKkoDT%_u3){BgH*+424B|KDJLYm$Gs z)>BQF>(yhvKHGEM0f9TNl{wcA?lUnE-^=%A_58gnEFXmZW?RpF z`%uo~?zY@QyY5cIKZ`z8-ukY0$Nm3>hzobj9S`l``>|u2>fQ6*%B2s$JEijfuil@q ze&*kI=KD9A%iC>udDJCax%ZD$e(m?&mrl(Pue#J%z41a+;Nw;6)g$ar@O|0Zc$_(rtds9ukGayi zZ=!8+r0S-hZ7=zX#g4B1@#N%P8=>0Q#hlw8EzTEL^7(Xk+4OBUPp9xRy?t*UxOB_c z-3My#zdvpL^_1yPG~1F}&dAm_GM)$)yz?S`uGAGCR+|*#4bq zhZV=0-uDr9yK?7VTzK=tg|NfQtKHeR$@H!bnmaw}O!%VirD6~EEskCs8(3J)#2K8SbYgYU>Dmzl_I^mS0W5Zb& zVS~6p{Uxi;>G{m<)`W(!tWh~CJ;_Srx z8q9%BzNh-mu1ycM+p=qU+2sD*ZKvLrY}}$Q&TTy}v`neWKKzn}=z*6FnOC=F>V{N5 z$je?Q?8Isr=&<90Xz2--xpmV#*L)X`JMxRYIBdpuw^=!d_Pl#3J+*07xNmUEmMu-% zP8YIm+%&YjF zmtd?Le(+>XQn2WZ{iPS5{}Q=)d((QS%Qu##{ropu{$_mrkLTZ>zpqW==YN00;j2Nw z{*y1ecg^Y+V_Lq5^F_8+RhK!_xw+r6F3MdGSL9gXy1KOe?&sEDK^fu?g`61fF+Etq zc_#7A+)F9b%Wm0Ewx6+hW$TQu{JEAE8?>amUR9Ur{jv(ZUuC2F_T!Qb##eubeA)4W zZ%x*V&1c-hO4!V(=P>@>j>N zJlCo?6*6_P{ua3)t6!_4-><*+(xXDmY{d{K@NEv$gDiz-2^-TK&Trqo^<4MtzymXmAGqPJz@RT| zo8490Ab;t2gr~%dPUUMV9L%eV7S23)DoHV+MDYGzj|I%j|1D5`q*`~W?ZM0N{lBIv ze`%X2nnN<);K z?9D&lCi?dAy-B&T-PqUYT)XY;Z^g5}7e%MU?r&;}t$cAXC{^-AChvq;`^`*g6Ca)b zt(Cv7C8$0yV?s#iX5o*ooxV)IxbD$gCW)=*cC1bOliU`_Vr$AHS{9zOszm`h9p zo#$syT2Z%DOqMr(cY3*f&HcRkFY`*5*uOPCrfKQ7zjKN?_=EA^6~t+pcy|5mQ0yH_0*;J$@8_AbFl1~rWz~wCCI^jaC|@ z?kwKTY+SakMlryHsY&$uwiSXG)qlS`>t)$`Z%#*lVnGK=obx!8KLD#cj( zM&;C-lQUEM&!v6)QIqJezI5)HX*p`!J(vEwx}J|I)chXX5}{?vPiKc@=zk4d6+CtF z`&)@hHx-x`>Bz5M;eM+6eg#9=S>M0gi`JW&Me5`RuF2`^S@}dn>`e5%oEIzSg{pg5 z|NWcZuVB{tIlm>RWSB;4g!Oz{~1+A793>Mt8X7T5OtkYSVV9TMl2% zb*jn&df&{i`y>AC{-3Y*+v`6r-oI`8`+d*&@AJ8A|LO2F#yS1MZI{$l+fGL_a4_6C zvzx*5XpaC_qwp8k`lssoF41pz*mrI_d4IV;65m1D$Hj-d7dkLJwSF(V;=khUW*XT6NonJnH;J|^-@`m2%Fv$*(Fvsb@m8ZT>a;ca0wsJUqywa&<1F?iOCkiySu zZS&(^@Yu%Qy}0lz-}|Wd54L@E-@fMb;cY)Jv@%upPwqIo$NA-=*a8!oB{_>?_M9~- zalgxMyZ!6zvfm1a_DTiYHujk7oyffT)>%BqM%_(zqeJNTS1`%T5aBX$2?JpcAvU;gIL zCo)~0)-NR%TU=abCAIS14mbWU+4o<(UXpJ1c=wN`YHsyO-+t@3ep}HKpZ7eOai#L+ z6`X&2#UIZ4sqgZ>cJrpU$rBqk_DuBBJ@Nl{#nl%FYd7pYr*CMcS`(}?_4+~k(vEL+ zqW2DTx8|DsEd9Wimmr>#efhpx-Sx(e?_PY%{rz|Gc~kYXbLSuNtMz;0`cL9Kb8-#8 zXd;8aXN_mBJ=r-SPp@(P=)19b^88YVqc#BtI2+3E*IqxHBcdwvW5Jn~_bMLq`n!La z##E@D+akSv-W%p?GEbUK8W;+XRJB~*tgv16&hF~l?_^|G#Ru+*+AOPe_Ket^gX_P3l8`G3_X3u=Jd>en^HUvde+AGXYQN)LSyY-%cpPhK25Ih z;fmkPbBD2Cc)#4bGymR}mHh9mVh(41`ooK{#rJV@M5c^@cG>yOHxBSmmE%?GP&~BA z@K`5-gJ_W;dNQ=wKFj7ismo- zl%2Vke4ZA%gX7Ane`bGJ{i!ne{-UsSzEh;eD>O``gP-6+a0;f*Wcf^ zx!T=qXK{ux?>27Ue^(0( zw}@qPO+(Iuorbzk^=Dq|Gq~Ck^Z8Lv&J01L;MEp=8{X}8}xukvZbI+%Trl@Ruap|0Q z3uDOUpv9ibb_s7Z&RmUI^{8p)$`bd;Mc-d@O6|S9(nq8Cz4#Kgq%uW@4*r#V200&YV0n^XjoRo6_HdcN6tc1c@V)c3a)7q>2db86mFbH+b) z-)^2dm(s4#@Vc=pblr@EF10V8f@togT7npgs##}!* zRcmg}nknC=g!%SWW<-4S6a8Ou@#FXWZm*Xi(#K`jgw;NkZ#-}y^Y}~mue?=Ni}YA~ z_;Y#Gw6t!#t7h7EbFV?h(o*4h>i?b|yfLj^%l>Qao2YlwX3DN{`*!Z&>u>4n_vhFD z{r3Is{QCF$Z`Qx7KQGtQ_r&?H!@3Q>7wzbKky-k1N6bpyBGn67i>h4f^`_XCaMW7% zF171=y}jad;=*lb4qy3aQEGX4hIQ%IRWrDk$bMh>K4^CCtMtwD`c}^~*gn~4`mCM* zB1-F&tSoe&p1T^ds(Kwy)VH>6r`p5PYPAiN=PZ5u({SAtK0&MBJ63H7{99#wao+w$ zr50aq-d|T^weCC4ySd{2X6CPNbtEDszU@f!e7kSWZnO1aW`R|DEt8X;s;8)VJznze zW=KiIGj^}*jW-``D|O$lD}7f;E_08?hmDI?o0NZ!+hy0sC>^k~V%Lk~(WycLTO_~i z^jx0%{k4JlJ7dkkt@_Dt(V^-M(4&k6!bB@Jw z>Z~)%c(SetEc+>%Bs0~xpQZA@Mfh9|-@{AiPFMf-THk)N{`|T;@y*6d-CoPp9*k)C zz!B?T`!6+O`h(=1&%#!m`@`-Und#PgVfoD1OutJ{6CWMxX=wPeOTnR7`V;$Sdltrb zZZ}S~@2<`YIX1<3mCqlir{%Eu&kBV?Xcy*9U4o zO}iHA@84H@vgG?e+vtoxPql95ewHqhX=1oCqn%;n&E=P73B@T(I~E?6b7Xk9@cAwI zyiSMIw+zAib$K3ieYqZ2efICR!^id2nK`8IUH?4AKJkJ6AxD3ik8htmI^(r=Df`}i zcXyShI4?bV{Y=#N?^Om*Oz%(Lzh$$%;UmWi{TZfTCv?eQI>E*fu;n5L%Z_6mXESD< z`;}!C`+iNnRP~p}mYH%=X?>mYwafD#@iL|v>bWTVDYcrn^tkVy#=8rW>Pi`ycoP|p z$nhR}e^^NIee;SB3MCvIwr3)w&g9p9mQFvS6nL1gK&kmwoS?#9hM#?(IF@~!Zxz5c z#dC^ann2V-yWcWXr%THkWc@h)m!GlFr{|ZFtxm#9_czYF&YWzh|8u;4%Xz!sXIhyj zPG4~RHN!)8-N|g9eE%q9az^bJdU>ThVfW^9#=4fRxz4xcv4Q)t!>5`VWfu80 zhaX&6u%A&O=l0gZ+0{wA)MoAKQP*87$nhoBSm4{%0Jr{z=G}i{r&b>OD)jTjvcnri z%irC4^>(|??{o_9FlPNDQH^t>T3oj@U za`?NJtyY|6LA=Woi7OHP@f?tZqv9RQ&aP*JYtP*?`2mt+yt;u{Nk}?RnF(H7vvOdd};eQ&+DnSY+cfb)O2$3Jc8(jvN6e!j(B9MBlaj7vQ=Q+j`jcLtUys$ux<+ zJCEbJ1UY_OwdVEP#a=DfVLQI@dtSlq`0e z1)A3ByR25#37V|bd)%dYe)=z$4A;4fFFW{WN*!tlm25bD)#K-z?svZzv7WW|VquCE zXILZoKf%^#RaK)`X0+G&-)ucszdT-eiM@I13$d-dUkn=q!+*btGx-s_Lb{OEHYor1 zf-^-|ebbChH{WzB4Jduv+syv0T>k&mzlZPtegAgy?))3S7hiv~uIl2(WuG}x{)Bjz z=XK9}kz2a3NYv`FJtvz%y91Y%%MX^^qR^S&V=o9DYGB$GG&5Z5chS2JyS=g#o-s3C z)N|a&zIxR*_sh5T&6ruM`KvlAt!&?$&P$dnjW#W^{+8XG_UgAKZ@|-6&4N;iVgKVR zA2&bb-W>L~uc&aP+U~wfjCViEm|ZWi{JT>3v)HKsZTVZOPP;ZXL5PaEgwXtXRnwkS0}=d=x{uGQX!Y`Lw+YkwHD zgdW#f5hC?#{eh|L#ChKq-S7LtAO856XM^t@_S?6DFKoZOgPU=N_Cc9^|V9I#;h7W!qT&!z2GO0v8X*9dM7Xxu3f?c%<34eQkx zQ=hAZB=Q{o^5^M(Py_eT4zBIBmyb#BkUKJ?So9v~0MT;sZChWiZ+^S6Si0^yqvQ7k zn@_79)Hfe$o7H}B3A@?n>t;V^=$k(K@j>h1&jaO&vIma&|LE%p{E?Wfp*7n!;miG9 zrP;Q}KJupa#y?64<_Kq1G&ry>%WTcV>7{HoDivNI%)dKcQM}JIQT}buKV{DIR|>xSOw}*^nQGawFVSqG#GfnHJwJ}9e~fHxx>Dk-WzsD5FXD2E9xGE( z%guf%C&uH)&i?d#$6ouZ&%j2;zx~nIk`wX|O$y%}lD@&Kbt;@&pvUe>-5i&PDW{&#uYYI4xp(`^Z&nRYOqQpI9FIMy z6C1W~F^~2A#An?nWmkN<)jB2df!XeabBptw@02Z+cdYhn3FNX%zVDR{ZPxx=G0EVK zQZ5Hek?YN47P1ZVRMsBbzD7#^#-|V4u1Y<5dw2Vq-+TGj{{G9qM(kVNpW4&^KM9}y z_lEg(&6mEb`Fr!O{m zBEj^%V1e!NVkaLPrsjV_hwV98C&kXHIMR2kfcKTXQ6SH=g^mUUc7T&e(qG zw4=Mr@-I9NF_<{-s?k#2pMlj*4;Gk|Y8{#onX9vxd4hpBZ?)~poEwGRKP=RwY|jQ| za2xE~BR=uwdbvlvj9)j_`ZkGmwZFE=*m*i!TlyJi%rw@uF*T^{UY4+B24&%UJqPFl|x1hJA6O5X*rV z;;lPw2mKRZI`HJVmgb*=phFW^w>??sRpjX4qoQfQiM{yj`zPf;d)2mmzU9|-rZ2V5 zCVHnpgD|7`=`+t(H^v_}TEDwasv)~FOm^M1OF#6M?3?x`GfLIw6*Z zEi*5$+cLb~w^?#M6Xmx^}Fy%~@O_!e7RhQD5JlA~zl76B6ypQEKh9|rIc;ckC{IJu%kT$K# zOYI4rlNpaT87s`uzJoUKb8*z?`KeY`T z_p5H0FjKqIfrZiL<|YN6cX88YjVEkjw&`8&X~1A?(ZcY>_Vg9`%pOUPW`kD?u8V%l zS@LgAY^2QPWxB!7IIPmTFR3}bC|+jw%IWFpmovJxiWwuVBo|Z~<%_RayWn`gM_|3kg96xoDCb5GJ3qth|x5))rXa8C|?RV)y*=uGw$9MC3 ze$zWMB~ZZjDL1oqZ((O%*43Y?tLDhKYU>HyI3xdzZRc|T6^}%!IL*)eU()8#FmILh z2_5?vw+ohhJ-#6MaADqI=Bo!iGJ{n=%-B`M)f%woHPJ7_Wv347T4ckZrRN} zwMD!ObbP$^-TB0!~Y9{{4&=~di0P>LE_)kXA5`yytV7+vE#8XZ+e{KY}^so zvEh*km%VMK{{5ZBr|(t#ZG1Xwau{ok``YC>Ti6VfjUZjpVPBz|e*`$GvY;*UP~?swlh^^sSHZm@%;Dn zZFZFw4t^GN_sF*0?+@jj5=>dZ_}A~bSsC zjlL(^(<2 zdfSSE_l6xk!`i;@_mnlJsa~2Y2OD19|9p(^HJjmQw}t=_<@+0&1WHe=UEI0(ib{k) z>;$=&2|7#vO{j~^T-h3W_=IlO{hNP}zC1H8W<~rOt8*1=cP)DBcV8@K;vvl@tKh#Y z=B_mfd|J)=ii!1NFr#wR(?2|WE!5X96kVCTb!wr!)QP!%373k35>|Q^gr3RW7;)3* zU_;#rm%%Wvgfy5M$<;n%NKFK2IL z)|f7#Z6?U^)s;C}AlbD?k>RCE{G3xaxucdu)Mp&i`;zBhn{@uz+=+Yb_N2b&lfMz& z{(bH0L$N-7d-p0Wm_1F{I(p7+rRHk_Dl7FK?OJ(k&YMvGRr>GKLtZ&}&Av6=_-o)y z?ZABo-!Jd*zm|=zd7Tt9+M+|hdw*m8P75$)^DQ#+(_Ps9iFLlYWN|bi zaOM58Z$D&YEH1qN$?-ZHyU~5An-_Dx`nD^-Ua9Xcpm6Gw!PJn(B6X>+795v%_lhi^ z^upuQqWrJFxJ94s|Nj5t@)P{>46oc{Kh`9*zY5bTe#K(E_U-D@sRGka@5__7`Tcf# zzj=Y}jwKV{R)Fr^xF37t+Oqw=f7USBxC$*8F`xFPQfhZW41m(kRwry8LZq z4fl_Ay<97oE-TgE%op-ZV~!L<(3~@kvn^*XQ~HtU?DdF0_S{vc0)fP(ss&u^SFi8n zz5ZJX&7I$_T;5=uTS}HU1I-I=WS=`w=H^aFBYZwhb^7Ede!uH zX6b!(_uuXNy>0K$kL};ieYfApF28sCvGzmP?e|@=|MS5%nz!2Q^_8-)j1PLx_WAEV zWgXUe=9$5>?+4QEE$9rmZL(Q<{@2a#ilfVpJbER{^KaYif3dervu^8MNSIWeHMQR@ zw^<|Rr?bV3Te3Z-zZk^1QjEOszg`-g&~@POW4@oPOh;P1f~rF2t`QXy{Nd9oyWhxS zvg*|Nn-gppmvrxwY%}JW74mQ1HMd;lH>)<>Z`n4t#y+|1mU7(Dnc9D2)oNd93xpkz z(@Kqhe(Lp$OYcKH?475ZsBmmjiCV<}$hJcyeRp2)y<0C<2FE4-SGQ6KUzl|J)9$os zfk_Ns7jIj{a-oS~VxO`TGFkPqCkR9pHMt)FvzX=_#(tH1!?vyH!da z^71?g%9{8q_L86O(HKL{{sn7qxt+3@oHk?qJ@eCYjR7a~>RvRIOvwu7I@Ky_z?6A+ z=AX|mR$Vу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 z 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%0DeJbZIP?5XPvG72+@Cz& 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

%ABOUT_PANDA3DS%
%SUPPORT%
@@ -26,6 +29,7 @@ AboutWindow::AboutWindow(QWidget* parent) : QDialog(parent) { %AUTHORS%