From 60f331b43b162ea290eca9747d22249f40140520 Mon Sep 17 00:00:00 2001 From: Wunk Date: Sun, 5 Apr 2026 14:01:05 -0700 Subject: [PATCH 1/6] cmake: Allow Catch test discovery (#1997) Allows individual unit-tests to be discovered, tested, and debugged by IDEs without having to run _all_ of the unit-tests just to debug one specific test. --- externals/CMakeLists.txt | 1 + src/tests/CMakeLists.txt | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/externals/CMakeLists.txt b/externals/CMakeLists.txt index ffcae42f6..50706a057 100644 --- a/externals/CMakeLists.txt +++ b/externals/CMakeLists.txt @@ -60,6 +60,7 @@ if (ENABLE_TESTS) add_subdirectory(catch2) endif() target_link_libraries(catch2 INTERFACE Catch2::Catch2WithMain) + include(Catch) endif() # Crypto++ diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt index d35e6ba21..027609c26 100644 --- a/src/tests/CMakeLists.txt +++ b/src/tests/CMakeLists.txt @@ -36,6 +36,10 @@ if (ENABLE_LIBRETRO) endif() add_test(NAME tests COMMAND tests) +if(NOT ANDROID) + catch_discover_tests(tests) +endif() + if (CITRA_USE_PRECOMPILED_HEADERS) target_precompile_headers(tests PRIVATE precompiled_headers.h) From 4cbd75b41345dbc6a1ce3fbef78e7b0d3b427933 Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Thu, 2 Apr 2026 22:18:44 -0700 Subject: [PATCH 2/6] shader_jit: Optimize GeometryEmitter `SETEMIT` state The `SETEMIT`/`SETE` instruction only actually encodes 4 bits of possible state, but this is currently expanded into three separate bytes of data(four with padding) and requires three separate byte-writes for the x64 and a64 JITs to write into. These 4 bits from the instruction can instead be compacted into a singular 1-byte write from the JIT by encoding these 4 bits of state into a singular byte at JIT-time, and unpacking this data is instead done by `GeometryEmitter::Emit`. This also allows the serializer to use a singular byte for all 3 fields now as well. --- src/video_core/pica/shader_unit.cpp | 10 ++++----- src/video_core/pica/shader_unit.h | 21 ++++++++++++------- src/video_core/shader/shader_interpreter.cpp | 6 +++--- .../shader/shader_jit_a64_compiler.cpp | 13 ++++++------ .../shader/shader_jit_x64_compiler.cpp | 9 +++++--- 5 files changed, 34 insertions(+), 25 deletions(-) diff --git a/src/video_core/pica/shader_unit.cpp b/src/video_core/pica/shader_unit.cpp index 5d81f857a..725dd9ebb 100644 --- a/src/video_core/pica/shader_unit.cpp +++ b/src/video_core/pica/shader_unit.cpp @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -29,15 +29,15 @@ void ShaderUnit::WriteOutput(const ShaderRegs& config, AttributeBuffer& buffer) } void GeometryEmitter::Emit(std::span, 16> output_regs) { - ASSERT(vertex_id < 3); + ASSERT(emit_state.vertex_id < 3); u32 output_index{}; for (u32 reg : Common::BitSet(output_mask)) { - buffer[vertex_id][output_index++] = output_regs[reg]; + buffer[emit_state.vertex_id][output_index++] = output_regs[reg]; } - if (prim_emit) { - if (winding) { + if (emit_state.prim_emit) { + if (emit_state.winding) { handlers->winding_setter(); } for (std::size_t i = 0; i < buffer.size(); ++i) { diff --git a/src/video_core/pica/shader_unit.h b/src/video_core/pica/shader_unit.h index 2f9c1843f..80eea8d23 100644 --- a/src/video_core/pica/shader_unit.h +++ b/src/video_core/pica/shader_unit.h @@ -1,4 +1,4 @@ -// Copyright 2023 Citra Emulator Project +// Copyright Citra Emulator Project / Azahar Emulator Project // Licensed under GPLv2 or any later version // Refer to the license.txt file included. @@ -75,11 +75,18 @@ struct GeometryEmitter { void Emit(std::span, 16> output_regs); public: - std::array buffer; - u8 vertex_id; - bool prim_emit; - bool winding; + union EmitState { + struct { + bool winding : 1; + bool prim_emit : 1; + u8 vertex_id : 2; + }; + u8 raw; + } emit_state; + static_assert(sizeof(emit_state) == 1); + u32 output_mask; + std::array buffer; Handlers* handlers; private: @@ -87,9 +94,7 @@ private: template void serialize(Archive& ar, const u32 file_version) { ar & buffer; - ar & vertex_id; - ar & prim_emit; - ar & winding; + ar & emit_state.raw; ar & output_mask; } }; diff --git a/src/video_core/shader/shader_interpreter.cpp b/src/video_core/shader/shader_interpreter.cpp index cb06a62bc..6d373e28f 100644 --- a/src/video_core/shader/shader_interpreter.cpp +++ b/src/video_core/shader/shader_interpreter.cpp @@ -671,9 +671,9 @@ static void RunInterpreter(const ShaderSetup& setup, ShaderUnit& state, case OpCode::Id::SETEMIT: { auto* emitter = state.emitter_ptr; ASSERT_MSG(emitter, "Execute SETEMIT on VS"); - emitter->vertex_id = instr.setemit.vertex_id; - emitter->prim_emit = instr.setemit.prim_emit != 0; - emitter->winding = instr.setemit.winding != 0; + emitter->emit_state.vertex_id = instr.setemit.vertex_id; + emitter->emit_state.prim_emit = instr.setemit.prim_emit != 0; + emitter->emit_state.winding = instr.setemit.winding != 0; break; } diff --git a/src/video_core/shader/shader_jit_a64_compiler.cpp b/src/video_core/shader/shader_jit_a64_compiler.cpp index 636d9cb57..90c8425bc 100644 --- a/src/video_core/shader/shader_jit_a64_compiler.cpp +++ b/src/video_core/shader/shader_jit_a64_compiler.cpp @@ -865,12 +865,13 @@ void JitShader::Compile_SETE(Instruction instr) { l(have_emitter); - MOV(XSCRATCH1.toW(), instr.setemit.vertex_id); - STRB(XSCRATCH1.toW(), XSCRATCH0, u32(offsetof(GeometryEmitter, vertex_id))); - MOV(XSCRATCH1.toW(), instr.setemit.prim_emit); - STRB(XSCRATCH1.toW(), XSCRATCH0, u32(offsetof(GeometryEmitter, prim_emit))); - MOV(XSCRATCH1.toW(), instr.setemit.winding); - STRB(XSCRATCH1.toW(), XSCRATCH0, u32(offsetof(GeometryEmitter, winding))); + const GeometryEmitter::EmitState new_state{ + .winding = instr.setemit.winding != 0, + .prim_emit = instr.setemit.prim_emit != 0, + .vertex_id = static_cast(instr.setemit.vertex_id), + }; + MOV(XSCRATCH1.toW(), new_state.raw); + STRB(XSCRATCH1.toW(), XSCRATCH0, u32(offsetof(GeometryEmitter, emit_state))); l(end); } diff --git a/src/video_core/shader/shader_jit_x64_compiler.cpp b/src/video_core/shader/shader_jit_x64_compiler.cpp index 84cdc09bc..1c1ff92b2 100644 --- a/src/video_core/shader/shader_jit_x64_compiler.cpp +++ b/src/video_core/shader/shader_jit_x64_compiler.cpp @@ -905,9 +905,12 @@ void JitShader::Compile_SETE(Instruction instr) { jmp(end); L(have_emitter); - mov(byte[rax + offsetof(GeometryEmitter, vertex_id)], instr.setemit.vertex_id); - mov(byte[rax + offsetof(GeometryEmitter, prim_emit)], instr.setemit.prim_emit); - mov(byte[rax + offsetof(GeometryEmitter, winding)], instr.setemit.winding); + const GeometryEmitter::EmitState new_state{ + .winding = instr.setemit.winding != 0, + .prim_emit = instr.setemit.prim_emit != 0, + .vertex_id = static_cast(instr.setemit.vertex_id), + }; + mov(byte[rax + offsetof(GeometryEmitter, emit_state)], new_state.raw); L(end); } From 06a535f50e8ab7d60a82be299c79efe1e8c8a2db Mon Sep 17 00:00:00 2001 From: Wunkolo Date: Sat, 4 Apr 2026 17:03:52 -0700 Subject: [PATCH 3/6] shader_jit: Add `SETEMIT` unit test --- src/tests/video_core/shader.cpp | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/tests/video_core/shader.cpp b/src/tests/video_core/shader.cpp index db868a260..4ea976668 100644 --- a/src/tests/video_core/shader.cpp +++ b/src/tests/video_core/shader.cpp @@ -481,6 +481,39 @@ SHADER_TEST_CASE("RSQ", "[video_core][shader]") { REQUIRE(shader.Run({0.0625f}).x == Catch::Approx(4.0f).margin(0.004f)); } +SHADER_TEST_CASE("SETEMIT", "[video_core][shader]") { + Pica::GeometryEmitter geometry_emitter; + + for (u8 winding = 0; winding <= 1; ++winding) { + for (u8 prim_emit = 0; prim_emit <= 1; ++prim_emit) { + for (u8 vertex_id = 0; vertex_id <= 3; ++vertex_id) { + auto shader_setup = CompileShaderSetup({ + {OpCode::Id::NOP}, // setemit + {OpCode::Id::END}, + }); + + // nihstro does not support the SETEMIT instructions, so the instruction-binary must + // be manually + // inserted here: + nihstro::Instruction SETEMIT = {}; + SETEMIT.opcode = nihstro::OpCode(nihstro::OpCode::Id::SETEMIT); + SETEMIT.setemit.winding.Assign(winding); + SETEMIT.setemit.prim_emit.Assign(prim_emit); + SETEMIT.setemit.vertex_id.Assign(vertex_id); + shader_setup->UpdateProgramCode(0, SETEMIT.hex); + + auto shader = TestType(std::move(shader_setup)); + Pica::ShaderUnit shader_unit(&geometry_emitter); + shader.Run(shader_unit, 1.0f); + + REQUIRE(geometry_emitter.emit_state.winding == winding); + REQUIRE(geometry_emitter.emit_state.prim_emit == prim_emit); + REQUIRE(geometry_emitter.emit_state.vertex_id == vertex_id); + } + } + } +} + SHADER_TEST_CASE("Uniform Read", "[video_core][shader]") { const auto sh_input = SourceRegister::MakeInput(0); const auto sh_c0 = SourceRegister::MakeFloat(0); From df05b5f3db8b2465485c7ec34b39eb8625e8bbf2 Mon Sep 17 00:00:00 2001 From: SiniKraft <65973766+SiniKraft@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:48:32 +0200 Subject: [PATCH 4/6] android : Fix emulation exit showing an Invalid Rom Format error --- .../app/src/main/java/org/citra/citra_emu/NativeLibrary.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt index 9d2015baa..f8cb55874 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/NativeLibrary.kt @@ -504,8 +504,9 @@ object NativeLibrary { const val ErrorSystemFiles = 8 const val ErrorSavestate = 9 const val ErrorArticDisconnected = 10 - const val ShutdownRequested = 11 - const val ErrorUnknown = 12 + const val ErrorN3DSApplication = 11 + const val ShutdownRequested = 12 + const val ErrorUnknown = 13 fun newInstance(resultCode: Int): EmulationErrorDialogFragment { val args = Bundle() From 000530c028bef21e4d78aad1268105e512f66126 Mon Sep 17 00:00:00 2001 From: SiniKraft <65973766+SiniKraft@users.noreply.github.com> Date: Sat, 4 Apr 2026 20:32:55 +0200 Subject: [PATCH 5/6] android : Fix navigation bar overlapping the Show Home Menu apps button --- src/android/app/src/main/res/layout/fragment_system_files.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/android/app/src/main/res/layout/fragment_system_files.xml b/src/android/app/src/main/res/layout/fragment_system_files.xml index bae1cda1c..650766748 100644 --- a/src/android/app/src/main/res/layout/fragment_system_files.xml +++ b/src/android/app/src/main/res/layout/fragment_system_files.xml @@ -5,13 +5,13 @@ android:id="@+id/coordinator_about" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:background="?attr/colorSurface"> + android:layout_height="wrap_content"> Date: Mon, 6 Apr 2026 16:27:40 +0100 Subject: [PATCH 6/6] Default to Vulkan renderer on Android --- .../citra_emu/features/settings/model/IntSetting.kt | 2 +- src/android/app/src/main/jni/default_ini.h | 2 +- src/citra_libretro/environment.cpp | 11 +++++++++-- src/common/settings.h | 6 +++++- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt index 2c8cbf2a1..eb1a880a5 100644 --- a/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt +++ b/src/android/app/src/main/java/org/citra/citra_emu/features/settings/model/IntSetting.kt @@ -17,7 +17,7 @@ enum class IntSetting( CAMERA_INNER_FLIP(SettingKeys.camera_inner_flip(), Settings.SECTION_CAMERA, 0), CAMERA_OUTER_LEFT_FLIP(SettingKeys.camera_outer_left_flip(), Settings.SECTION_CAMERA, 0), CAMERA_OUTER_RIGHT_FLIP(SettingKeys.camera_outer_right_flip(), Settings.SECTION_CAMERA, 0), - GRAPHICS_API(SettingKeys.graphics_api(), Settings.SECTION_RENDERER, 1), + GRAPHICS_API(SettingKeys.graphics_api(), Settings.SECTION_RENDERER, 2), RESOLUTION_FACTOR(SettingKeys.resolution_factor(), Settings.SECTION_RENDERER, 1), STEREOSCOPIC_3D_MODE(SettingKeys.render_3d(), Settings.SECTION_RENDERER, 2), STEREOSCOPIC_3D_DEPTH(SettingKeys.factor_3d(), Settings.SECTION_RENDERER, 0), diff --git a/src/android/app/src/main/jni/default_ini.h b/src/android/app/src/main/jni/default_ini.h index 5e2eab1d1..ce93d0e6f 100644 --- a/src/android/app/src/main/jni/default_ini.h +++ b/src/android/app/src/main/jni/default_ini.h @@ -90,7 +90,7 @@ static const char* android_config_default_file_content = (BOOST_HANA_STRING(R"( [Renderer] # Whether to render using OpenGL -# 1: OpenGL ES (default), 2: Vulkan +# 1: OpenGL ES, 2: Vulkan (default) )") DECLARE_KEY(graphics_api) BOOST_HANA_STRING(R"( # Whether to compile shaders on multiple worker threads (Vulkan only) diff --git a/src/citra_libretro/environment.cpp b/src/citra_libretro/environment.cpp index 04c78356f..c3deddacc 100644 --- a/src/citra_libretro/environment.cpp +++ b/src/citra_libretro/environment.cpp @@ -61,7 +61,14 @@ bool GetMicrophoneInterface(struct retro_microphone_interface* mic_interface) { } Settings::GraphicsAPI GetPreferredRenderer() { - // try and maintain the current driver + // On Android, we really want to default to Vulkan if we can, so we'll ignore the frontend's + // recommendation if possible... +#if defined(ANDROID) && defined(ENABLE_VULKAN) + return Settings::GraphicsAPI::Vulkan; +#endif + // ...Otherwise negotiate with the RetroArch frontend as usual + + // Attempt to use the renderer recommended by the frontend if possible retro_hw_context_type context_type = RETRO_HW_CONTEXT_OPENGL; environ_cb(RETRO_ENVIRONMENT_GET_PREFERRED_HW_RENDER, &context_type); switch (context_type) { @@ -80,7 +87,7 @@ Settings::GraphicsAPI GetPreferredRenderer() { default: break; } - // we can't maintain the current driver, need to switch + // We can't get a recommendation from the frontend, so fall back to whatever's available #if defined(ENABLE_VULKAN) return Settings::GraphicsAPI::Vulkan; #elif defined(ENABLE_OPENGL) diff --git a/src/common/settings.h b/src/common/settings.h index 90922bcff..5eca53421 100644 --- a/src/common/settings.h +++ b/src/common/settings.h @@ -498,8 +498,11 @@ struct Values { Setting apply_region_free_patch{true, Keys::apply_region_free_patch}; // Renderer + // clang-format off SwitchableSetting graphics_api{ -#if defined(ENABLE_OPENGL) +#if defined(ANDROID) && defined(ENABLE_VULKAN) // Prefer Vulkan on Android, OpenGL on everything else + GraphicsAPI::Vulkan, +#elif defined(ENABLE_OPENGL) GraphicsAPI::OpenGL, #elif defined(ENABLE_VULKAN) GraphicsAPI::Vulkan, @@ -510,6 +513,7 @@ struct Values { #error "At least one renderer must be enabled." #endif GraphicsAPI::Software, GraphicsAPI::Vulkan, Keys::graphics_api}; + // clang-format on SwitchableSetting physical_device{0, Keys::physical_device}; Setting use_gles{false, Keys::use_gles}; Setting renderer_debug{false, Keys::renderer_debug};