aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornéo Luigi <85024393+neoluigi4123@users.noreply.github.com>2026-11-03 00:49:36 +0100
committernéo Luigi <85024393+neoluigi4123@users.noreply.github.com>2026-11-03 00:49:36 +0100
commitc84e5caf82a786f748c233e27b7f4ed9aaad9f44 (patch)
tree3ec9cab90d74292445d0a076370933bac946fc4d
parent2b413a46a2820f0a646baaa8dbd683009f096f32 (diff)
parentefa7c19e59839f0d464c2e9bd41f9ca8f867fef0 (diff)
Merge remote-tracking branch 'origin/nx2' into neo.ai/vibration-fix
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> # Conflicts: # src/core/hle/service/hid/hid_server.cpp # src/core/hle/service/hid/hid_server.h
-rw-r--r--.github/workflows/release-binaries.yml197
-rw-r--r--CMakeLists.txt25
-rw-r--r--NX2_NOTES.md4
-rw-r--r--cpmfile.json8
-rw-r--r--dist/Eglantine.icnsbin0 -> 60987 bytes
-rw-r--r--dist/eglantine.icnsbin0 -> 620877 bytes
-rw-r--r--dist/languages/ja_JP.ts2755
-rw-r--r--docs/performance/MKW-INTEGRATION.md72
-rw-r--r--docs/user/Basics.md2
-rw-r--r--docs/user/QuickStart.md2
-rw-r--r--docs/user/Troubleshoot.md2
-rw-r--r--externals/cmake-modules/DetectArchitecture.cmake13
-rw-r--r--externals/ffmpeg/CMakeLists.txt8
-rwxr-xr-xshell.nix18
-rw-r--r--src/CMakeLists.txt7
-rw-r--r--src/audio_core/audio_render_manager.cpp2
-rw-r--r--src/audio_core/common/feature_support.h28
-rw-r--r--src/audio_core/renderer/behavior/behavior_info.cpp8
-rw-r--r--src/audio_core/renderer/behavior/behavior_info.h1
-rw-r--r--src/audio_core/renderer/behavior/info_updater.cpp9
-rw-r--r--src/audio_core/renderer/sink/circular_buffer_sink_info.cpp11
-rw-r--r--src/common/CMakeLists.txt2
-rw-r--r--src/common/fiber.cpp13
-rw-r--r--src/common/game_presets.h37
-rw-r--r--src/common/host_memory.cpp104
-rw-r--r--src/common/host_memory.h23
-rw-r--r--src/common/settings.h12
-rw-r--r--src/common/slot_vector.h16
-rw-r--r--src/common/sparse_large_vector.cpp7
-rw-r--r--src/core/CMakeLists.txt12
-rw-r--r--src/core/arm/dynarmic/arm_dynarmic_32.cpp4
-rw-r--r--src/core/arm/dynarmic/arm_dynarmic_64.cpp4
-rw-r--r--src/core/core.cpp21
-rw-r--r--src/core/device_memory_manager.h7
-rw-r--r--src/core/device_memory_manager.inc49
-rw-r--r--src/core/file_sys/fsmitm_romfsbuild.cpp27
-rw-r--r--src/core/file_sys/nup_container.cpp842
-rw-r--r--src/core/file_sys/nup_container.h211
-rw-r--r--src/core/file_sys/nup_writer.cpp1552
-rw-r--r--src/core/file_sys/nup_writer.h76
-rw-r--r--src/core/file_sys/system_archive/system_archive.cpp19
-rw-r--r--src/core/file_sys/vfs/vfs_real.cpp37
-rw-r--r--src/core/file_sys/vfs/vfs_real.h8
-rw-r--r--src/core/hle/kernel/board/nintendo/nx/k_system_control.h5
-rw-r--r--src/core/hle/kernel/k_memory_manager.cpp24
-rw-r--r--src/core/hle/kernel/k_memory_manager.h6
-rw-r--r--src/core/hle/kernel/k_priority_queue.h20
-rw-r--r--src/core/hle/kernel/k_shared_memory.cpp5
-rw-r--r--src/core/hle/kernel/kernel.cpp4
-rw-r--r--src/core/hle/kernel/svc.cpp17
-rw-r--r--src/core/hle/service/acc/acc.cpp4
-rw-r--r--src/core/hle/service/am/frontend/applet_software_keyboard.cpp17
-rw-r--r--src/core/hle/service/am/frontend/applet_software_keyboard_types.h61
-rw-r--r--src/core/hle/service/am/frontend/applets.cpp2
-rw-r--r--src/core/hle/service/am/service/application_functions.cpp70
-rw-r--r--src/core/hle/service/am/service/application_functions.h4
-rw-r--r--src/core/hle/service/am/service/application_proxy_service.cpp1
-rw-r--r--src/core/hle/service/am/service/audio_controller.cpp2
-rw-r--r--src/core/hle/service/am/service/common_state_getter.cpp50
-rw-r--r--src/core/hle/service/am/service/display_controller.cpp37
-rw-r--r--src/core/hle/service/am/service/graphics_scale_controller.h6
-rw-r--r--src/core/hle/service/am/service/library_applet_accessor.cpp13
-rw-r--r--src/core/hle/service/am/service/library_applet_creator.cpp46
-rw-r--r--src/core/hle/service/am/service/self_controller.cpp41
-rw-r--r--src/core/hle/service/am/service/window_controller.cpp2
-rw-r--r--src/core/hle/service/aoc/addon_content_manager.cpp14
-rw-r--r--src/core/hle/service/audio/audio.cpp3
-rw-r--r--src/core/hle/service/audio/audio_device.cpp31
-rw-r--r--src/core/hle/service/audio/audio_device.h4
-rw-r--r--src/core/hle/service/audio/audio_renderer_manager.cpp2
-rw-r--r--src/core/hle/service/audio/nx2_audio_device_manager.h539
-rw-r--r--src/core/hle/service/caps/caps_su.cpp4
-rw-r--r--src/core/hle/service/caps/caps_u.cpp24
-rw-r--r--src/core/hle/service/hid/hid_server.cpp121
-rw-r--r--src/core/hle/service/hid/hid_server.h6
-rw-r--r--src/core/hle/service/kernel_helpers.cpp19
-rw-r--r--src/core/hle/service/kernel_helpers.h4
-rw-r--r--src/core/hle/service/lm/lm.cpp5
-rw-r--r--src/core/hle/service/mii/mii.cpp6
-rw-r--r--src/core/hle/service/ns/platform_service_manager.cpp65
-rw-r--r--src/core/hle/service/nvdrv/core/container.cpp2
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp88
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h4
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp38
-rw-r--r--src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp28
-rw-r--r--src/core/hle/service/nvdrv/devices/nx2_stub_device.h73
-rw-r--r--src/core/hle/service/nvdrv/nvdrv.cpp1
-rw-r--r--src/core/hle/service/nvdrv/nvdrv_interface.cpp6
-rw-r--r--src/core/hle/service/nvnflinger/pixel_format.h1
-rw-r--r--src/core/hle/service/nx2_gsv.h1287
-rw-r--r--src/core/hle/service/ro/ro.cpp23
-rw-r--r--src/core/hle/service/service.cpp109
-rw-r--r--src/core/hle/service/service.h24
-rw-r--r--src/core/hle/service/set/settings_server.cpp7
-rw-r--r--src/core/hle/service/sockets/bsd.cpp84
-rw-r--r--src/core/hle/service/sockets/nsd.cpp4
-rw-r--r--src/core/hle/service/ssl/ssl.cpp93
-rw-r--r--src/core/launch_params.cpp8
-rw-r--r--src/core/launch_params.h2
-rw-r--r--src/core/loader/deconstructed_rom_directory.cpp109
-rw-r--r--src/core/loader/deconstructed_rom_directory.h5
-rw-r--r--src/core/loader/loader.cpp35
-rw-r--r--src/core/loader/loader.h3
-rw-r--r--src/core/loader/nup.cpp280
-rw-r--r--src/core/loader/nup.h65
-rw-r--r--src/core/memory.cpp15
-rw-r--r--src/dynarmic/src/dynarmic/backend/block_range_information.cpp27
-rw-r--r--src/dynarmic/src/dynarmic/backend/block_range_information.h18
-rw-r--r--src/dynarmic/src/dynarmic/backend/x64/a32_emit_x64.cpp6
-rw-r--r--src/dynarmic/src/dynarmic/backend/x64/a32_emit_x64.h14
-rw-r--r--src/dynarmic/src/dynarmic/backend/x64/a64_emit_x64.cpp6
-rw-r--r--src/dynarmic/src/dynarmic/backend/x64/a64_emit_x64.h14
-rw-r--r--src/dynarmic/src/dynarmic/backend/x64/block_of_code.cpp34
-rw-r--r--src/dynarmic/src/dynarmic/backend/x64/emit_x64.cpp70
-rw-r--r--src/dynarmic/src/dynarmic/backend/x64/emit_x64.h12
-rw-r--r--src/dynarmic/src/dynarmic/backend/x64/emit_x64_memory.cpp.inc81
-rw-r--r--src/dynarmic/src/dynarmic/backend/x64/reg_alloc.cpp14
-rw-r--r--src/dynarmic/src/dynarmic/backend/x64/reg_alloc.h9
-rw-r--r--src/input_common/drivers/sdl_driver.cpp26
-rw-r--r--src/input_common/drivers/udp_client.cpp4
-rw-r--r--src/input_common/drivers/udp_client.h1
-rw-r--r--src/qt_common/config/uisettings.cpp3
-rw-r--r--src/qt_common/config/uisettings.h2
-rw-r--r--src/qt_common/game_list/discovery.h113
-rw-r--r--src/qt_common/game_list/model.cpp21
-rw-r--r--src/qt_common/game_list/model.h2
-rw-r--r--src/qt_common/game_list/worker.cpp165
-rw-r--r--src/qt_common/game_list/worker.h7
-rw-r--r--src/qt_common/qt_common.cpp3
-rw-r--r--src/qt_common/util/game.cpp11
-rw-r--r--src/shader_recompiler/CMakeLists.txt1
-rw-r--r--src/shader_recompiler/backend/glasm/emit_glasm.cpp3
-rw-r--r--src/shader_recompiler/backend/glasm/emit_glasm_instructions.h4
-rw-r--r--src/shader_recompiler/backend/glasm/emit_glasm_not_implemented.cpp16
-rw-r--r--src/shader_recompiler/backend/glasm/glasm_emit_context.cpp3
-rw-r--r--src/shader_recompiler/backend/glsl/emit_glsl_instructions.h4
-rw-r--r--src/shader_recompiler/backend/glsl/emit_glsl_not_implemented.cpp16
-rw-r--r--src/shader_recompiler/backend/glsl/glsl_emit_context.cpp3
-rw-r--r--src/shader_recompiler/backend/spirv/emit_spirv.cpp60
-rw-r--r--src/shader_recompiler/backend/spirv/emit_spirv_instructions.h6
-rw-r--r--src/shader_recompiler/backend/spirv/emit_spirv_mesh.cpp227
-rw-r--r--src/shader_recompiler/backend/spirv/emit_spirv_special.cpp7
-rw-r--r--src/shader_recompiler/backend/spirv/spirv_emit_context.cpp127
-rw-r--r--src/shader_recompiler/backend/spirv/spirv_emit_context.h27
-rw-r--r--src/shader_recompiler/environment.h14
-rw-r--r--src/shader_recompiler/frontend/ampere/lower_to_maxwell.h1058
-rw-r--r--src/shader_recompiler/frontend/ir/ir_emitter.cpp16
-rw-r--r--src/shader_recompiler/frontend/ir/ir_emitter.h5
-rw-r--r--src/shader_recompiler/frontend/ir/microinstruction.cpp3
-rw-r--r--src/shader_recompiler/frontend/ir/opcodes.cpp4
-rw-r--r--src/shader_recompiler/frontend/ir/opcodes.h4
-rw-r--r--src/shader_recompiler/frontend/ir/opcodes.inc4
-rw-r--r--src/shader_recompiler/frontend/ir/program.h1
-rw-r--r--src/shader_recompiler/frontend/maxwell/maxwell.inc1
-rw-r--r--src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp29
-rw-r--r--src/shader_recompiler/frontend/maxwell/translate/impl/internal_stage_buffer_entry_read.cpp36
-rw-r--r--src/shader_recompiler/frontend/maxwell/translate_program.cpp62
-rw-r--r--src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp7
-rw-r--r--src/shader_recompiler/ir_opt/position_pass.cpp31
-rw-r--r--src/shader_recompiler/ir_opt/texture_pass.cpp485
-rw-r--r--src/shader_recompiler/runtime_info.h3
-rw-r--r--src/shader_recompiler/shader_info.h140
-rw-r--r--src/shader_recompiler/stage.h5
-rw-r--r--src/tests/CMakeLists.txt9
-rw-r--r--src/tests/common/game_presets.cpp111
-rw-r--r--src/tests/common/slot_vector.cpp54
-rw-r--r--src/tests/core/core_timing.cpp3
-rw-r--r--src/tests/core/nup_writer.cpp562
-rw-r--r--src/tests/core/priority_queue.cpp111
-rw-r--r--src/tests/input_common/calibration_configuration_job.cpp20
-rw-r--r--src/tests/video_core/ampere_returns.cpp27
-rw-r--r--src/tests/video_core/descriptor_table.cpp170
-rw-r--r--src/tests/video_core/memory_tracker.cpp38
-rw-r--r--src/tests/video_core/native_macro_fifo.cpp74
-rw-r--r--src/tests/video_core/shader_invalidation.cpp120
-rw-r--r--src/tests/video_core/uniform_stream_reuse.cpp116
-rw-r--r--src/tests/video_core/vertex_repack.cpp126
-rw-r--r--src/video_core/CMakeLists.txt1
-rw-r--r--src/video_core/buffer_cache/buffer_cache.h330
-rw-r--r--src/video_core/buffer_cache/buffer_cache_base.h64
-rw-r--r--src/video_core/buffer_cache/uniform_stream_reuse.h121
-rw-r--r--src/video_core/control/scheduler.cpp2
-rw-r--r--src/video_core/control/scheduler.h6
-rw-r--r--src/video_core/dirty_flags.cpp2
-rw-r--r--src/video_core/dma_pusher.cpp143
-rw-r--r--src/video_core/dma_pusher.h22
-rw-r--r--src/video_core/engines/draw_manager.cpp46
-rw-r--r--src/video_core/engines/fermi_2d.h3
-rw-r--r--src/video_core/engines/kepler_compute.cpp114
-rw-r--r--src/video_core/engines/kepler_compute.h8
-rw-r--r--src/video_core/engines/maxwell_3d.cpp38
-rw-r--r--src/video_core/engines/maxwell_3d.h47
-rw-r--r--src/video_core/engines/puller.cpp2
-rw-r--r--src/video_core/engines/sw_blitter/blitter.cpp104
-rw-r--r--src/video_core/fence_manager.h8
-rw-r--r--src/video_core/gpu.cpp16
-rw-r--r--src/video_core/gpu.h7
-rw-r--r--src/video_core/gpu_split.cpp30
-rw-r--r--src/video_core/gpu_split.h10
-rw-r--r--src/video_core/gpu_thread.cpp11
-rw-r--r--src/video_core/gpu_thread.h5
-rw-r--r--src/video_core/host1x/codecs/codec.cpp2
-rw-r--r--src/video_core/host1x/ffmpeg.cpp2
-rw-r--r--src/video_core/host1x/nvdec_common.h1
-rw-r--r--src/video_core/host1x/nx2_video_decoder.cpp4
-rw-r--r--src/video_core/host1x/syncpoint_manager.h4
-rw-r--r--src/video_core/host_shaders/CMakeLists.txt3
-rw-r--r--src/video_core/host_shaders/blit_color_srgb_to_linear.frag18
-rw-r--r--src/video_core/host_shaders/temporal_upscale.frag132
-rw-r--r--src/video_core/host_shaders/vulkan_vertex_repack.comp26
-rw-r--r--src/video_core/memory_manager.cpp24
-rw-r--r--src/video_core/nx2.h148
-rw-r--r--src/video_core/rasterizer_interface.h25
-rw-r--r--src/video_core/renderer_opengl/gl_compute_pipeline.cpp14
-rw-r--r--src/video_core/renderer_opengl/gl_graphics_pipeline.cpp12
-rw-r--r--src/video_core/renderer_opengl/gl_shader_cache.cpp5
-rw-r--r--src/video_core/renderer_opengl/present/layer.cpp5
-rw-r--r--src/video_core/renderer_vulkan/blit_image.cpp154
-rw-r--r--src/video_core/renderer_vulkan/blit_image.h16
-rw-r--r--src/video_core/renderer_vulkan/fixed_pipeline_state.cpp13
-rw-r--r--src/video_core/renderer_vulkan/fixed_pipeline_state.h1
-rw-r--r--src/video_core/renderer_vulkan/maxwell_to_vk.cpp4
-rw-r--r--src/video_core/renderer_vulkan/present/layer.cpp2
-rw-r--r--src/video_core/renderer_vulkan/vertex_repack.h114
-rw-r--r--src/video_core/renderer_vulkan/vk_buffer_cache.cpp60
-rw-r--r--src/video_core/renderer_vulkan/vk_buffer_cache.h44
-rw-r--r--src/video_core/renderer_vulkan/vk_compute_pass.cpp65
-rw-r--r--src/video_core/renderer_vulkan/vk_compute_pass.h15
-rw-r--r--src/video_core/renderer_vulkan/vk_compute_pipeline.cpp17
-rw-r--r--src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp55
-rw-r--r--src/video_core/renderer_vulkan/vk_graphics_pipeline.h21
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.cpp246
-rw-r--r--src/video_core/renderer_vulkan/vk_pipeline_cache.h21
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.cpp126
-rw-r--r--src/video_core/renderer_vulkan/vk_rasterizer.h12
-rw-r--r--src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp16
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.cpp791
-rw-r--r--src/video_core/renderer_vulkan/vk_texture_cache.h92
-rw-r--r--src/video_core/renderer_vulkan/vk_update_descriptor.cpp8
-rw-r--r--src/video_core/renderer_vulkan/vk_update_descriptor.h3
-rw-r--r--src/video_core/shader_cache.cpp61
-rw-r--r--src/video_core/shader_cache.h8
-rw-r--r--src/video_core/shader_environment.cpp166
-rw-r--r--src/video_core/shader_environment.h29
-rw-r--r--src/video_core/shader_handle_program.h39
-rw-r--r--src/video_core/surface.cpp2
-rw-r--r--src/video_core/texture_cache/descriptor_table.h24
-rw-r--r--src/video_core/texture_cache/format_lookup_table.cpp1
-rw-r--r--src/video_core/texture_cache/image_base.h7
-rw-r--r--src/video_core/texture_cache/texture_cache.h383
-rw-r--r--src/video_core/texture_cache/texture_cache_base.h53
-rw-r--r--src/video_core/texture_cache/types.h9
-rw-r--r--src/video_core/texture_cache/util.cpp204
-rw-r--r--src/video_core/texture_cache/util.h21
-rw-r--r--src/video_core/textures/astc.cpp58
-rw-r--r--src/video_core/textures/texture.h11
-rw-r--r--src/video_core/vulkan_common/vulkan_device.cpp72
-rw-r--r--src/video_core/vulkan_common/vulkan_device.h65
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.cpp86
-rw-r--r--src/video_core/vulkan_common/vulkan_memory_allocator.h22
-rw-r--r--src/video_core/vulkan_common/vulkan_wrapper.cpp14
-rw-r--r--src/video_core/vulkan_common/vulkan_wrapper.h14
-rw-r--r--src/yuzu/CMakeLists.txt1
-rw-r--r--src/yuzu/Info.plist2
-rw-r--r--src/yuzu/configuration/configure_graphics.cpp19
-rw-r--r--src/yuzu/configuration/shared_widget.cpp8
-rw-r--r--src/yuzu/game/game_list.cpp18
-rw-r--r--src/yuzu/game/game_list.h4
-rw-r--r--src/yuzu/main_window.cpp329
-rw-r--r--src/yuzu/main_window.h20
-rw-r--r--src/yuzu/nup_dialogs.cpp485
-rw-r--r--src/yuzu/nup_dialogs.h114
-rw-r--r--src/yuzu_cmd/emu_window/emu_window_sdl3.cpp14
-rw-r--r--src/yuzu_cmd/emu_window/emu_window_sdl3.h6
-rw-r--r--src/yuzu_cmd/yuzu.cpp53
-rw-r--r--tools/integration/game_discovery_test.cpp75
276 files changed, 17945 insertions, 2562 deletions
diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml
index 8835844060..36be3f1c64 100644
--- a/.github/workflows/release-binaries.yml
+++ b/.github/workflows/release-binaries.yml
@@ -30,7 +30,7 @@ permissions:
# clang and an older reader refuses it, so this has to stay at or above whatever produced
# eden.profdata; bump it when a refreshed profile needs a newer LLVM.
env:
- PGO_LLVM_VERSION: '23'
+ PGO_LLVM_VERSION: "23"
concurrency:
group: release-binaries-${{ github.ref }}
@@ -42,8 +42,9 @@ jobs:
runs-on: ${{ matrix.runner }}
env:
CCACHE_DIR: ${{ github.workspace }}/.ccache
- CCACHE_MAXSIZE: 1500M
+ CCACHE_MAXSIZE: 3000M
SCCACHE_DIR: ${{ github.workspace }}/.sccache
+ SCCACHE_CACHE_SIZE: 3G
strategy:
fail-fast: false
matrix:
@@ -67,10 +68,12 @@ jobs:
- platform: linux-aarch64-pgo
runner: ubuntu-24.04-arm
pgo: true
+ - platform: macos-arm64
+ runner: macos-15
steps:
- name: Check out source
- uses: actions/checkout@v4
+ uses: actions/checkout@v5
- name: Install Linux dependencies
if: startsWith(matrix.platform, 'linux')
@@ -85,13 +88,26 @@ jobs:
libswscale-dev pkg-config zlib1g-dev libva-dev libvdpau-dev \
spirv-tools spirv-headers libusb-1.0-0-dev \
libboost-dev libboost-fiber-dev libboost-context-dev \
- libopus-dev libasound2-dev ccache
+ libopus-dev libasound2-dev ccache \
+ libxcb-cursor0 libxcb-icccm4 libxcb-image0 \
+ libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-xinerama0 libxcb-xkb1
+
+ # The packages docs/Deps.md lists for macOS, plus ccache.
+ - name: Install macOS dependencies
+ if: startsWith(matrix.platform, 'macos')
+ shell: bash
+ env:
+ HOMEBREW_NO_INSTALL_CLEANUP: 1
+ run: |
+ brew install autoconf automake boost ffmpeg fmt glslang hidapi libtool libusb lz4 ninja \
+ nlohmann-json openssl pkg-config qt@6 sdl3 speexdsp zlib zstd cmake catch2 molten-vk \
+ vulkan-loader spirv-tools vulkan-utility-libraries ccache
- name: Install Qt on Linux x86_64
if: startsWith(matrix.platform, 'linux') && runner.arch != 'ARM64'
uses: jurplel/install-qt-action@v4
with:
- version: '6.8.3'
+ version: "6.8.3"
host: linux
target: desktop
arch: linux_gcc_64
@@ -102,7 +118,7 @@ jobs:
if: startsWith(matrix.platform, 'linux') && runner.arch == 'ARM64'
uses: jurplel/install-qt-action@v4
with:
- version: '6.8.3'
+ version: "6.8.3"
host: linux_arm64
target: desktop
arch: linux_gcc_arm64
@@ -146,7 +162,7 @@ jobs:
if: matrix.platform == 'windows-x86_64'
uses: jurplel/install-qt-action@v4
with:
- version: '6.8.3'
+ version: "6.8.3"
host: windows
target: desktop
arch: win64_msvc2022_64
@@ -183,6 +199,7 @@ jobs:
with:
msystem: UCRT64
update: true
+ cache: true
install: >-
git make autoconf libtool automake-wrapper patch jq pactoys ninja
pacboy: >-
@@ -194,7 +211,7 @@ jobs:
lerc:p graphite2:p libwebp:p libtiff:p brotli:p ccache:p
- name: Cache CPM downloads
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: .cache/cpm
key: cpm-${{ runner.os }}-${{ hashFiles('cpmfile.json', 'CMakeLists.txt', 'src/**/CMakeLists.txt') }}
@@ -204,7 +221,7 @@ jobs:
# The sha in the key makes every run save a fresh cache (actions/cache never saves over an
# exact hit), and restore-keys seeds it from the newest one of the same platform.
- name: Cache compiler objects
- uses: actions/cache@v4
+ uses: actions/cache@v5
with:
path: |
.ccache
@@ -266,6 +283,27 @@ jobs:
-DCMAKE_C_COMPILER_LAUNCHER=sccache
-DCMAKE_CXX_COMPILER_LAUNCHER=sccache
+ - name: Configure macOS
+ if: startsWith(matrix.platform, 'macos')
+ shell: bash
+ run: |
+ set -euo pipefail
+ cmake -S . -B build/release -G Ninja \
+ -DCMAKE_BUILD_TYPE=Release \
+ -DBUILD_TESTING=OFF \
+ -DYUZU_TESTS=OFF \
+ -DYUZU_CMD=ON \
+ -DYUZU_ROOM=OFF \
+ -DYUZU_ROOM_STANDALONE=OFF \
+ -DYUZU_USE_BUNDLED_QT=OFF \
+ "-DCMAKE_PREFIX_PATH=$(brew --prefix qt@6);$(brew --prefix zlib)" \
+ "-DOPENSSL_ROOT_DIR=$(brew --prefix openssl@3)" \
+ -DVulkanHeaders_FORCE_BUNDLED=ON \
+ -DVulkanUtilityLibraries_FORCE_BUNDLED=ON \
+ -DENABLE_UPDATE_CHECKER=OFF \
+ -DUSE_DISCORD_PRESENCE=OFF \
+ -DUSE_CCACHE=ON
+
# MinGW build: system Qt and libraries from MSYS2, clang plus Eden's PGO profile. The profile
# path is converted to its Windows form because clang runs with the MSYS mount underneath.
- name: Configure MinGW PGO
@@ -335,9 +373,6 @@ jobs:
set -euo pipefail
test -x build/release/bin/eglantine
test -x build/release/bin/eden-cli
- # Libraries Qt's xcb platform plugin needs, so linuxdeploy can resolve and bundle them.
- sudo apt-get install -y --no-install-recommends libxcb-cursor0 libxcb-icccm4 libxcb-image0 \
- libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-shape0 libxcb-xinerama0 libxcb-xkb1
tools="$RUNNER_TEMP/linuxdeploy"
mkdir -p "$tools"
arch=$(uname -m) # aarch64 or x86_64
@@ -411,11 +446,70 @@ jobs:
deps "$dll"
done
+ # A self-contained app bundle: macdeployqt copies Qt and every other Homebrew library both
+ # programs load into the bundle, then the leftover Homebrew search paths are removed so a
+ # machine with Homebrew still uses the bundled copies. Ad-hoc signed, not notarized.
+ - name: Package macOS app
+ if: startsWith(matrix.platform, 'macos')
+ shell: bash
+ run: |
+ set -euo pipefail
+ app=build/release/bin/eglantine.app
+ test -x "$app/Contents/MacOS/eglantine"
+ test -x build/release/bin/eden-cli
+ cp build/release/bin/eden-cli "$app/Contents/MacOS/eden-cli"
+ "$(brew --prefix qt@6)/bin/macdeployqt" "$app" \
+ -executable="$app/Contents/MacOS/eden-cli" -libpath="$(brew --prefix)/lib" \
+ -always-overwrite -verbose=1
+ # Qt's virtual keyboard input plugin pulls in the QML stack, which the emulator doesn't use.
+ rm -f "$app/Contents/PlugIns/platforminputcontexts/libqtvirtualkeyboardplugin.dylib"
+ binaries=$(find "$app/Contents" -type f \( -perm -u+x -o -name '*.dylib' \) \
+ -exec sh -c 'file -b "$1" | grep -q Mach-O' _ {} \; -print)
+ for binary in $binaries; do
+ otool -l "$binary" | awk '/cmd LC_RPATH/ {getline; getline; print $2}' |
+ { grep -E '^/(opt/homebrew|usr/local)/' || true; } | while read -r rpath; do
+ install_name_tool -delete_rpath "$rpath" "$binary" 2> /dev/null
+ done
+ done
+ # Libraries that load their dependencies through @rpath find them in the bundle.
+ for program in eglantine eden-cli; do
+ install_name_tool -add_rpath @executable_path/../Frameworks \
+ "$app/Contents/MacOS/$program" 2> /dev/null
+ done
+ leaks=$(for binary in $binaries; do
+ otool -L "$binary" | grep -E '^\s+/(opt/homebrew|usr/local)/'
+ otool -l "$binary" | awk '/cmd LC_RPATH/ {getline; getline; print $2}' |
+ grep -E '^/(opt/homebrew|usr/local)/'
+ done || true)
+ if [ -n "$leaks" ]; then
+ echo "References still pointing outside the bundle:"
+ echo "$leaks" | sort -u
+ exit 1
+ fi
+ # macdeployqt invalidates the signatures. The programs get the entitlements when the
+ # tree has them (the Hypervisor CPU backend needs com.apple.security.hypervisor).
+ codesign --force --deep --sign - "$app"
+ if [ -f dist/eglantine.entitlements ]; then
+ codesign --force --sign - --entitlements dist/eglantine.entitlements \
+ "$app/Contents/MacOS/eden-cli"
+ codesign --force --sign - --entitlements dist/eglantine.entitlements "$app"
+ fi
+ codesign --verify --deep --strict "$app"
+ ditto -c -k --keepParent "$app" "eglantine-${GITHUB_SHA}-${{ matrix.platform }}.zip"
+ ls -la "eglantine-${GITHUB_SHA}-${{ matrix.platform }}.zip"
+ # Also ship a .dmg (drag eglantine.app onto Applications).
+ dmgdir=$(mktemp -d)
+ ditto "$app" "$dmgdir/eglantine.app"
+ ln -s /Applications "$dmgdir/Applications"
+ hdiutil create -volname "Eglantine" -srcfolder "$dmgdir" -ov -format UDZO \
+ "eglantine-${GITHUB_SHA}-${{ matrix.platform }}.dmg"
+ ls -la "eglantine-${GITHUB_SHA}-${{ matrix.platform }}.dmg"
+
# The artifact is the bin folder itself: GitHub zips artifacts on download, and uploading a
# zip gave a zip inside a zip. The release job zips it once for the release.
- name: Upload Windows binary
if: matrix.platform == 'windows-x86_64'
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: eglantine-${{ matrix.platform }}-${{ github.sha }}
path: build/release/bin/
@@ -424,7 +518,7 @@ jobs:
- name: Upload MinGW binary
if: matrix.mingw
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: eglantine-${{ matrix.platform }}-${{ github.sha }}
path: build/release/bin/
@@ -434,7 +528,7 @@ jobs:
# Both Linux files go in one artifact: the tar.gz and the AppImage.
- name: Upload Linux binary
if: startsWith(matrix.platform, 'linux')
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: eglantine-${{ matrix.platform }}-${{ github.sha }}
path: |
@@ -444,6 +538,19 @@ jobs:
compression-level: 0
retention-days: 7
+ # Zipped with ditto beforehand: the bundle's framework symlinks don't survive artifact zipping.
+ - name: Upload macOS app
+ if: startsWith(matrix.platform, 'macos')
+ uses: actions/upload-artifact@v7
+ with:
+ name: eglantine-${{ matrix.platform }}-${{ github.sha }}
+ path: |
+ eglantine-${{ github.sha }}-${{ matrix.platform }}.zip
+ eglantine-${{ github.sha }}-${{ matrix.platform }}.dmg
+ if-no-files-found: error
+ compression-level: 0
+ retention-days: 7
+
# Publishes every binary as a GitHub pre-release once every platform built: on pushes to nx2
# and nx2-experimental, and on manual runs unless "Publish the binaries as a GitHub release" is
# unticked.
@@ -456,7 +563,7 @@ jobs:
contents: write
steps:
- name: Download binaries
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v7
with:
pattern: eglantine-*-${{ github.sha }}
path: artifacts
@@ -466,33 +573,71 @@ jobs:
run: |
set -euo pipefail
mkdir dist
+ # Asset names drop the commit sha: the release tag already identifies the build, and
+ # stable names let the notes table link to them.
+ strip_sha() { sed "s/-${GITHUB_SHA}//" <<< "$1"; }
for artifact in artifacts/*/; do
name=$(basename "$artifact")
if compgen -G "$artifact*.AppImage" > /dev/null; then
# Linux: ship the AppImage and the tar.gz as they are.
- cp "$artifact"/*.AppImage "$artifact"/*.tar.gz dist/
+ for file in "$artifact"*.AppImage "$artifact"*.tar.gz; do
+ cp "$file" "dist/$(strip_sha "$(basename "$file")")"
+ done
+ elif compgen -G "$artifact*.zip" > /dev/null; then
+ for file in "$artifact"*.zip "$artifact"*.dmg; do
+ [[ -e "$file" ]] && cp "$file" "dist/$(strip_sha "$(basename "$file")")"
+ done
else
- (cd "$artifact" && zip -qr "${GITHUB_WORKSPACE}/dist/${name}.zip" .)
+ (cd "$artifact" && zip -qr "${GITHUB_WORKSPACE}/dist/$(strip_sha "$name").zip" .)
fi
done
+ ls -la dist
- name: Create release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
- ls -la dist
- gh release create "build-${GITHUB_RUN_NUMBER}" dist/* \
+ set -euo pipefail
+ tag="build-${GITHUB_RUN_NUMBER}"
+ base="https://github.com/${GITHUB_REPOSITORY}/releases/download/${tag}"
+
+ cat > notes.md <<EOF
+ Automatic build of \`${GITHUB_REF_NAME}\` at ${GITHUB_SHA}.
+
+ ## Linux
+
+ Linux packages come as an AppImage (self-contained, needs glibc 2.39 or newer: Ubuntu 24.04, Fedora 40, Debian 13, Arch) and a tar.gz (uses your system libraries, so Qt 6 and the usual libraries must be installed).
+
+ | Build Type | Standard | PGO (Recommended) | Notes |
+ | --- | --- | --- | --- |
+ | amd64 | [AppImage](${base}/eglantine-linux-x86_64.AppImage) · [tar.gz](${base}/eglantine-linux-x86_64.tar.gz) | [AppImage](${base}/eglantine-linux-x86_64-pgo.AppImage) · [tar.gz](${base}/eglantine-linux-x86_64-pgo.tar.gz) | Make the AppImage executable (\`chmod +x\`) and run it |
+ | ARM (aarch64) | [AppImage](${base}/eglantine-linux-aarch64.AppImage) · [tar.gz](${base}/eglantine-linux-aarch64.tar.gz) | [AppImage](${base}/eglantine-linux-aarch64-pgo.AppImage) · [tar.gz](${base}/eglantine-linux-aarch64-pgo.tar.gz) | |
+
+ ## Windows
+
+ Windows packages are in-place zip files: extract and run \`eglantine.exe\`, or \`eden-cli.exe\` from a terminal.
+
+ | Build Type | Standard | PGO (Recommended) | Notes |
+ | --- | --- | --- | --- |
+ | amd64 / x86_64 (MSVC) | [MSVC zip](${base}/eglantine-windows-x86_64.zip) | | Use if you run into graphical issues on other builds |
+ | amd64 / x86_64 (MinGW) | | [PGO zip](${base}/eglantine-windows-x86_64-pgo.zip) | Built with MinGW clang; bundles its own DLLs, no Visual C++ redistributable needed |
+
+ ## macOS
+
+ | Build Type | Standard | PGO (Recommended) | Notes |
+ | --- | --- | --- | --- |
+ | arm64 (Apple Silicon) | [zip](${base}/eglantine-macos-arm64.zip) | | macOS 15 or newer. Not notarized: right-click and choose Open the first time, or run \`xattr -dr com.apple.quarantine eglantine.app\`. \`eden-cli\` is inside the app, in \`Contents/MacOS\` |
+
+ The PGO builds are the same programs compiled with profile-guided optimization (clang plus Eden's profile); prefer them when your CPU supports it.
+ EOF
+
+ gh release create "${tag}" dist/* \
--repo "${GITHUB_REPOSITORY}" \
--target "${GITHUB_SHA}" \
--prerelease \
--title "Eglantine ${GITHUB_REF_NAME} build ${GITHUB_RUN_NUMBER} (${GITHUB_SHA::7})" \
- --notes "Automatic build of ${GITHUB_REF_NAME} at ${GITHUB_SHA}.
-
- Windows: extract the zip and run eglantine.exe, or eden-cli.exe from a terminal.
- Linux (AppImage): make it executable (chmod +x) and run it; it bundles its libraries and needs glibc 2.39 or newer (Ubuntu 24.04, Fedora 40, Debian 13, Arch).
- Linux (tar.gz): extract it and run ./eglantine or ./eden-cli; it uses your system libraries, so Qt 6 and the usual libraries must be installed.
- The -pgo builds are the same programs compiled with profile-guided optimization (clang plus Eden's profile); prefer them when your CPU supports it. The Windows PGO build is a MinGW build, so it carries its own DLLs and needs no Visual C++ redistributable."
+ --notes-file notes.md
- name: Ping Discord
shell: bash
diff --git a/CMakeLists.txt b/CMakeLists.txt
index fb2b96ed05..04d0f17d74 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -82,6 +82,26 @@ cmake_dependent_option(YUZU_USE_BUNDLED_QT "Download bundled Qt binaries" "${MSV
option(ENABLE_DEBUG_TOOLS "Enable debugging tools (maxwell disassembler, SPIRV translator, etc)" OFF)
option(ENABLE_WERROR "Enable -Werror diagnostics" ON)
+# Handle specific -Werror downgrades safely across platforms
+if(ENABLE_WERROR)
+ if(CMAKE_COMPILER_IS_GNUCC OR CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
+ if(CMAKE_C_FLAGS MATCHES "-Werror=unused")
+ string(REPLACE "-Werror=unused" "-Wno-error=unused" CMAKE_C_FLAGS "${CMAKE_C_FLAGS}")
+ endif()
+ if(CMAKE_CXX_FLAGS MATCHES "-Werror=unused")
+ string(REPLACE "-Werror=unused" "-Wno-error=unused" CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS}")
+ endif()
+
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-error=unused -Wno-error=unused-function")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=unused -Wno-error=unused-function")
+ elseif(MSVC)
+ # Windows Specifically
+ set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /wd4100 /wd4101")
+ set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4100 /wd4101")
+ endif()
+endif()
+
+
# Lossless Scaling frame generation. Only Android.
option(ENABLE_LSFG "Enable frame generation (LSFG and FSR 3 Fluid Motion)" ON)
@@ -293,7 +313,7 @@ if (ANDROID AND YUZU_DOWNLOAD_ANDROID_VVL)
endif()
if(EXISTS ${PROJECT_SOURCE_DIR}/hooks/pre-commit AND NOT EXISTS ${PROJECT_SOURCE_DIR}/.git/hooks/pre-commit)
- if (EXISTS ${PROJECT_SOURCE_DIR}/.git/)
+ if (IS_DIRECTORY ${PROJECT_SOURCE_DIR}/.git)
message(STATUS "Copying pre-commit hook")
file(COPY hooks/pre-commit DESTINATION ${PROJECT_SOURCE_DIR}/.git/hooks)
endif()
@@ -443,6 +463,9 @@ if (lz4_ADDED)
endif()
# zstd
+# Always bundled: NUP archives are only byte-identical across platforms when every build
+# compresses with the same zstd version.
+set(zstd_FORCE_BUNDLED ON)
AddJsonPackage(zstd)
if (zstd_ADDED)
diff --git a/NX2_NOTES.md b/NX2_NOTES.md
index 08ce0123a1..eeebb90568 100644
--- a/NX2_NOTES.md
+++ b/NX2_NOTES.md
@@ -144,7 +144,7 @@ With the patched decoder, games 1/3/4 only leave ~60-110 rare SIMD encodings in
| 86 | title 1 after its title screen: black frames | tens of thousands of rejected shaders: IPA interpolation modes, integer carry/third source, uniform comparison, formatted surface load/store, uniform constant load, unsupported opcodes | open |
| 87 | title 1 loading crash: Vulkan out of memory | an 8192x8192x68-layer virtual texture (~23 GB) | images over 2^28 texels are created sparse with no memory (`Unbacked`: no uploads/downloads) (96a9b81); the game reaches its real title screen ("Press L + R"), ground black (that texture has no data) |
| 88 | title 1 shader coverage | checked against nvdisasm (`nx2work/nvd.py` disassembles one instruction at a time, since a single illegal word stops nvdisasm); calls: RET may restore the link through IMAD.MOV with code between, library functions may EXIT, `@Pg EXIT Pc`; PLOP3/UPLOP3 any table, UP2UR, ULOP3 predicate output; UIMAD.WIDE, uniform USHF/FMNMX/I2F/F2I, LDS `[Ra.Xn+URb]`, ATOMG/RED/STG forms, PRMT, FSWZADD, REDUX.MAX | 1351c73, 52cda01, 49396b7: 1699 of 1712 dumped programs lower; left: 12 task/mesh programs (ISBERD/ISBEWR, row 89) and one SURED |
-| 89 | task/mesh shaders | title 1 draws some geometry with NV-style task + mesh programs: the task program culls a batch and writes its payload with `ISBEWR.O.ATTR [Rn+0x10]` and the mesh count with `ISBEWR [0x4]`; mesh programs read it back with `ISBERD.ATTR.SKEW`. The most-drawn rejected program (0x114c459500) is a task program. Eden has no mesh pipeline | open: needs a task/mesh pipeline (VK_EXT_mesh_shader) or a compute + indirect-draw emulation |
+| 89 | task/mesh shaders | title 1 draws some geometry with NVN task + mesh programs. While SET_MESH_CONTROL (0x114c) is on: task program in the vertex slot, mesh program in the tessellation slot, header-only pass-through geometry program (its output map = per-primitive outputs); draw count = task workgroups. Task: `ISBERD.ATTR.SKEW [RZ]` = workgroup index, payload `ISBEWR.O.ATTR [rank*stride+0x10]`, mesh count `ISBEWR.O.ATTR [0x4]`. Mesh: input [0] = its index, payload from 0x10; vertices in groups of 32, one 128-byte line per output component (mesh header omap order), then the per-primitive groups; map: count `ISBEWR.O.32 [0x3]`, U8 indices from 4. Addresses are plain linear bytes | task/mesh stages for VK_EXT_mesh_shader: ISBE ops kept as a pseudo instruction through the lowering, emulated stage buffer (task payload / workgroup memory), every EXIT to one epilogue that emits EmitMeshTasks or SetMeshOutputs + the output copy; pipelines keyed by SET_MESH_SHADER_A/B and SET_MESH_INIT_SHADER, kept out of the disk cache. All captured programs translate to valid SPIR-V; in the attract demo they draw cleanly at ~3 ms a frame, and without them the demo stalls on the world map (`EDEN_NX2_NO_MESH`, `EDEN_NX2_NO_MESH_FILE` to compare) |
| 90 | each rejected program was lowered and logged again on every draw (1.7M log lines in 13 min) | no negative cache | the shader cache keeps the first exception per address until that memory is written; logs deduplicated (bcb4ff0) |
| 91 | a compute program with a jump table: "Failed to track indirect branch" | Eden's tracker wants IMNMX imm -> SHL imm -> LDC -> BRX; IMAD.SHL was lowered to an XMAD multiply | IMAD by a power of two lowers to SHL (49396b7+) |
| 92 | with rows 88's compute forms, title 1 stays on its loading map (file reads stop ~46 s in) | two compute programs failing in Eden's compiler: a jump table whose targets lie past the first EXIT, and a 128-bit store of RZ from unaligned scratch registers | programs with BRX end at the compiler's closing branch-to-self; R248-R251 for 128-bit zero stores (3df6a81) |
@@ -152,7 +152,7 @@ With the patched decoder, games 1/3/4 only leave ~60-110 rare SIMD encodings in
| 94 | near terrain black | 8192x8192x68 ASTC 8x8 virtual textures: 1.5 GB in guest memory, 23 GB each as Eden's RGBA8 decode | huge GPU-decoded ASTC arrays keep levels from the first that fits 512 MiB (here 3) on the host (60c4791) |
| 95 | wind/foliage shards | SM70+ MUFU.SIN/COS take turns (sin(2pi x)); Eden's Maxwell MUFU takes radians | scale by 2pi (245e6ad); jungle canopy not re-checked yet |
| 96 | title 1 aborts after ~1.5-4 min at the title or when a race starts (nn::diag::Abort from main+0x19e5df0) | the movie player (main+0x19e5860) lays out its decoder frames with nn::codec::CalculateDecoderImagePoolBuffer, which sends nx2:gsv 991; auto-stubbed, the reply kept the request words, so the frame size read as 0x167027f'00000001 and the budget check at main+0x19e5b58 failed. (Not the "video-frame-count" < 9 check: the game sets that from a two-slot registry, at most 2.) | gsv 991 answers a block-linear NV12 size (640x360 -> 0x64000) |
-| 97 | pop-in, objects flicker in and out, low-quality meshes | high-detail LODs drawn through task/mesh programs (bound in stage slot 1, header type VS, ISBERD/ISBEWR), which are rejected and skipped | open: task/mesh support |
+| 97 | pop-in, objects flicker in and out, low-quality meshes | high-detail LODs drawn through task/mesh programs (bound in stage slot 1, header type VS, ISBERD/ISBEWR), which are rejected and skipped | task/mesh support (row 89); not yet checked in a race |
| 98 | ~12 fps at the title | host GPU ~20% busy, emulated cores ~0.9 host cores, GPU thread 45-70% busy, much of it waiting on the host GPU: semaphore acquires wait for host fences under GPU accuracy High; ASTC uploads end in Finish() (a Flush() there crashed at boot); a 1 ms poll sleep while channels are suspended | gpu_accuracy=Low: ~16 fps; SDL main loop capped at 240 Hz (88f04ad). Open: ASTC upload without a full stall (and per-layer re-upload of the trimmed virtual textures), native MME interpreter cost, draw path |
| 99 | VRAM over the card (8.4-9.8 GB used on an 8 GB RTX 4070 Laptop), texture cache always collecting | ~700 ASTC 8x8 textures decoded to RGBA8 (16x their size, ~4.5 GB) | astc_recompression=Bc3 + accelerate_astc=CpuAsynchronous in the config; huge ASTC arrays stay on the GPU decoder as trimmed RGBA8 (ed0a3c9): 4.6-5.6 GB, ~10 -> 16 fps |
| 100 | 8 ImageTranscode threads at ~44% each (3.5 cores) | Eden's default pool is half the hardware threads | an eighth, at least 2 (`EDEN_NX2_TRANSCODE_THREADS` overrides): ~19 fps on the attract loop |
diff --git a/cpmfile.json b/cpmfile.json
index ecc7ef754b..341e5b2c9f 100644
--- a/cpmfile.json
+++ b/cpmfile.json
@@ -81,6 +81,13 @@
"repo": "crueter-ci/FFmpeg",
"version": "9.0.2-1790726865-946fcce07b"
},
+ "ffmpeg-ci-av1": {
+ "ci": true,
+ "name": "ffmpeg",
+ "package": "FFmpeg",
+ "repo": "Takoroka/ffmpeg-av1",
+ "version": "9.0.2-av1-1"
+ },
"fmt": {
"hash": "f0da82c545b01692e9fd30fdfb613dbb8dd9716983dcd0ff19ac2a8d36f74beb5540ef38072fdecc1e34191b3682a8542ecbf3a61ef287dbba0a2679d4e023f2",
"min_version": "8",
@@ -208,6 +215,7 @@
"version": "3.6.2"
},
"opus": {
+ "bundled": true,
"find_args": "MODULE",
"hash": "9506147b0de35befda8633ff272981cc2575c860874791bd455b752f797fd7dbd1079f0ba42ccdd7bb1fe6773fa5e84b3d75667c2883dd1fb2d0e4a5fa4f8387",
"min_version": "1.3",
diff --git a/dist/Eglantine.icns b/dist/Eglantine.icns
new file mode 100644
index 0000000000..7169466694
--- /dev/null
+++ b/dist/Eglantine.icns
Binary files differ
diff --git a/dist/eglantine.icns b/dist/eglantine.icns
new file mode 100644
index 0000000000..812334cd28
--- /dev/null
+++ b/dist/eglantine.icns
Binary files differ
diff --git a/dist/languages/ja_JP.ts b/dist/languages/ja_JP.ts
index 2341618395..12af3b1130 100644
--- a/dist/languages/ja_JP.ts
+++ b/dist/languages/ja_JP.ts
@@ -26,17 +26,24 @@ li.unchecked::marker { content: &quot;\2610&quot;; }
li.checked::marker { content: &quot;\2612&quot;; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; font-size:9pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:&apos;MS Shell Dlg 2&apos;; font-size:12pt;&quot;&gt;Eden is an experimental open-source emulator for the Nintendo Switch licensed under GPLv3.0+ which is based on the yuzu emulator which ended development back in March 2024. &lt;br /&gt;&lt;br /&gt;This software should not be used to play games you have not legally obtained.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
- <translation type="unfinished"/>
+ <translation>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
+&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;meta charset=&quot;utf-8&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
+p, li { white-space: pre-wrap; }
+hr { height: 1px; border-width: 0; }
+li.unchecked::marker { content: &quot;\2610&quot;; }
+li.checked::marker { content: &quot;\2612&quot;; }
+&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Noto Sans'; font-size:9pt; font-weight:400; font-style:normal;&quot;&gt;
+&lt;p style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;&lt;span style=&quot; font-family:'MS Shell Dlg 2'; font-size:12pt;&quot;&gt;Edenは、2024年3月に開発が終了したyuzuをベースとする、GPLv3.0+ライセンスのNintendo Switch向け実験的オープンソースエミュレーターです。&lt;br /&gt;&lt;br /&gt;正規の方法で入手していないゲームのプレイには使用しないでください。&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../../src/yuzu/aboutdialog.ui" line="131"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://eden-emulator.github.io/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;Website&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://git.eden-emu.dev&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;Source Code&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://git.eden-emu.dev/eden-emu/eden/activity/contributors&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;Contributors&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://discord.gg/HstXbPch7X&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;Discord&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://stt.gg/qKgFEAbH&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;Stoat&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://nitter.poast.org/edenemuofficial&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;Twitter&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://git.eden-emu.dev/eden-emu/eden/src/branch/master/LICENSE.txt&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;License&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
- <translation type="unfinished"/>
+ <translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;a href=&quot;https://eden-emulator.github.io/&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;ウェブサイト&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://git.eden-emu.dev&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;ソースコード&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://git.eden-emu.dev/eden-emu/eden/activity/contributors&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;コントリビューター&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://discord.gg/HstXbPch7X&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;Discord&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://stt.gg/qKgFEAbH&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;Stoat&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://nitter.poast.org/edenemuofficial&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;Twitter&lt;/span&gt;&lt;/a&gt; | &lt;a href=&quot;https://git.eden-emu.dev/eden-emu/eden/src/branch/master/LICENSE.txt&quot;&gt;&lt;span style=&quot; text-decoration: underline; color:#039be5;&quot;&gt;ライセンス&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../../src/yuzu/aboutdialog.ui" line="147"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:7pt;&quot;&gt;&amp;quot;Nintendo Switch&amp;quot; is a trademark of Nintendo. Eden is not affiliated with Nintendo in any way.&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
- <translation type="unfinished"/>
+ <translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:7pt;&quot;&gt;&amp;quot;Nintendo Switch&amp;quot;は任天堂の商標です。Edenは任天堂とは一切関係ありません。&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
</context>
<context>
@@ -54,17 +61,17 @@ li.checked::marker { content: &quot;\2612&quot;; }
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.cpp" line="45"/>
<source>Touch the top left corner &lt;br&gt;of your touchpad.</source>
- <translation>タッチパッドの左上を&lt;br&gt;タッチして下さい。</translation>
+ <translation>タッチパッドの左上を&lt;br&gt;タッチしてください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.cpp" line="48"/>
<source>Now touch the bottom right corner &lt;br&gt;of your touchpad.</source>
- <translation>次にタッチパッドの右下を&lt;br&gt;タッチして下さい。</translation>
+ <translation>次に、タッチパッドの右下を&lt;br&gt;タッチしてください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.cpp" line="51"/>
<source>Configuration completed!</source>
- <translation>設定完了!</translation>
+ <translation>設定が完了しました!</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.cpp" line="59"/>
@@ -77,17 +84,17 @@ li.checked::marker { content: &quot;\2612&quot;; }
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.ui" line="14"/>
<source>Room Window</source>
- <translation>ルームウインドウ</translation>
+ <translation>ルームウィンドウ</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.ui" line="40"/>
<source>Send Chat Message</source>
- <translation>チャットを送る</translation>
+ <translation>チャットメッセージを送信</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.ui" line="47"/>
<source>Send Message</source>
- <translation>メッセージを送る</translation>
+ <translation>メッセージを送信</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="182"/>
@@ -107,17 +114,17 @@ li.checked::marker { content: &quot;\2612&quot;; }
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="324"/>
<source>%1 has been kicked</source>
- <translation>%1 はキックされました</translation>
+ <translation>%1 がキックされました</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="327"/>
<source>%1 has been banned</source>
- <translation>%1 はBanされました</translation>
+ <translation>%1 がBANされました</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="330"/>
<source>%1 has been unbanned</source>
- <translation>%1 はBan解除されました</translation>
+ <translation>%1 のBANが解除されました</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="446"/>
@@ -133,7 +140,7 @@ li.checked::marker { content: &quot;\2612&quot;; }
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="470"/>
<source>When you block a player, you will no longer receive chat messages from them.&lt;br&gt;&lt;br&gt;Are you sure you would like to block %1?</source>
- <translation>ブロックすると、そのプレイヤーからのチャットメッセージが届かなくなります。&lt;br&gt;&lt;br&gt;%1を本当にブロックしますか?</translation>
+ <translation>プレイヤーをブロックすると、そのプレイヤーからのチャットメッセージを受信しなくなります。&lt;br&gt;&lt;br&gt;%1 を本当にブロックしますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="483"/>
@@ -143,7 +150,7 @@ li.checked::marker { content: &quot;\2612&quot;; }
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="484"/>
<source>Ban</source>
- <translation>Ban</translation>
+ <translation>BAN</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="488"/>
@@ -153,21 +160,21 @@ li.checked::marker { content: &quot;\2612&quot;; }
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="489"/>
<source>Are you sure you would like to &lt;b&gt;kick&lt;/b&gt; %1?</source>
- <translation>%1 を&lt;b&gt;キック&lt;/b&gt;しますがよろしいですか?</translation>
+ <translation>%1 を&lt;b&gt;キック&lt;/b&gt;しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="497"/>
<source>Ban Player</source>
- <translation>プレイヤーをBan</translation>
+ <translation>プレイヤーをBAN</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="498"/>
<source>Are you sure you would like to &lt;b&gt;kick and ban&lt;/b&gt; %1?
This would ban both their forum username and their IP address.</source>
- <translation>%1 の&lt;b&gt;キックとBan&lt;/b&gt;をしますがよろしいですか?
+ <translation>%1 を&lt;b&gt;キックしてBAN&lt;/b&gt;しますか?
-これにより、そのフォーラムのユーザー名とIPアドレスの両方がBanされます。</translation>
+フォーラムのユーザー名とIPアドレスの両方がBANされます。</translation>
</message>
</context>
<context>
@@ -175,22 +182,22 @@ This would ban both their forum username and their IP address.</source>
<message>
<location filename="../../src/yuzu/multiplayer/client_room.ui" line="14"/>
<source>Room Window</source>
- <translation>ルームウインドウ</translation>
+ <translation>ルームウィンドウ</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/client_room.ui" line="27"/>
<source>Room Description</source>
- <translation>ルーム説明</translation>
+ <translation>ルームの説明</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/client_room.ui" line="47"/>
<source>Moderation...</source>
- <translation type="unfinished"/>
+ <translation>モデレーション...</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/client_room.ui" line="57"/>
<source>Leave Room</source>
- <translation>ルームを離れる</translation>
+ <translation>ルームから退出</translation>
</message>
</context>
<context>
@@ -198,7 +205,7 @@ This would ban both their forum username and their IP address.</source>
<message>
<location filename="../../src/yuzu/multiplayer/client_room.cpp" line="80"/>
<source>Connected</source>
- <translation>接続の状態</translation>
+ <translation>接続済み</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/client_room.cpp" line="89"/>
@@ -208,7 +215,7 @@ This would ban both their forum username and their IP address.</source>
<message>
<location filename="../../src/yuzu/multiplayer/client_room.cpp" line="102"/>
<source>%1 - %2 (%3/%4 members) - connected</source>
- <translation>%1 - %2 (%3/%4 メンバー) - 接続済み</translation>
+ <translation>%1 - %2 (%3/%4 人) - 接続済み</translation>
</message>
</context>
<context>
@@ -221,17 +228,17 @@ This would ban both their forum username and their IP address.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="34"/>
<source>Amiibo editor</source>
- <translation type="unfinished"/>
+ <translation>amiiboエディター</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="35"/>
<source>Controller configuration</source>
- <translation type="unfinished"/>
+ <translation>コントローラーの設定</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="36"/>
<source>Data erase</source>
- <translation type="unfinished"/>
+ <translation>データの削除</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="37"/>
@@ -241,12 +248,12 @@ This would ban both their forum username and their IP address.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="38"/>
<source>Net connect</source>
- <translation type="unfinished"/>
+ <translation>ネットワーク接続</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="39"/>
<source>Player select</source>
- <translation type="unfinished"/>
+ <translation>プレイヤー選択</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="40"/>
@@ -256,57 +263,57 @@ This would ban both their forum username and their IP address.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="41"/>
<source>Mii Edit</source>
- <translation type="unfinished"/>
+ <translation>Miiの編集</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="42"/>
<source>Online web</source>
- <translation type="unfinished"/>
+ <translation>オンラインウェブ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="43"/>
<source>Shop</source>
- <translation type="unfinished"/>
+ <translation>ショップ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="44"/>
<source>Photo viewer</source>
- <translation type="unfinished"/>
+ <translation>フォトビューアー</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="45"/>
<source>Offline web</source>
- <translation type="unfinished"/>
+ <translation>オフラインウェブ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="46"/>
<source>Login share</source>
- <translation type="unfinished"/>
+ <translation>ログイン共有</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="47"/>
<source>Wifi web auth</source>
- <translation type="unfinished"/>
+ <translation>Wi-Fiウェブ認証</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="48"/>
<source>My page</source>
- <translation type="unfinished"/>
+ <translation>マイページ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="49"/>
<source>Enable Overlay Applet</source>
- <translation type="unfinished"/>
+ <translation>オーバーレイアプレットを有効化</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="50"/>
<source>Enables Horizon&apos;s built-in overlay applet. Press and hold the home button for 1 second to show it.</source>
- <translation type="unfinished"/>
+ <translation>Horizon内蔵のオーバーレイアプレットを有効にします。HOMEボタンを1秒間長押しすると表示されます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="54"/>
<source>Output Engine:</source>
- <translation>出力エンジン:</translation>
+ <translation>出力エンジン:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="55"/>
@@ -321,17 +328,17 @@ This would ban both their forum username and their IP address.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="57"/>
<source>Mute audio</source>
- <translation type="unfinished"/>
+ <translation>音声をミュート</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="58"/>
<source>Volume:</source>
- <translation>音量:</translation>
+ <translation>音量:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="60"/>
<source>Mute audio when in background</source>
- <translation>非アクティブ時にサウンドをミュート</translation>
+ <translation>バックグラウンド時に音声をミュート</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="63"/>
@@ -342,7 +349,8 @@ This would ban both their forum username and their IP address.</source>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="64"/>
<source>This option increases CPU emulation thread use from 1 to the maximum of 4.
This is mainly a debug option and shouldn&apos;t be disabled.</source>
- <translation type="unfinished"/>
+ <translation>CPUエミュレーションで使用するスレッド数を1から最大4に増やします。
+これは主にデバッグ用のオプションのため、通常は無効にしないでください。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="66"/>
@@ -353,183 +361,194 @@ This is mainly a debug option and shouldn&apos;t be disabled.</source>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="67"/>
<source>Increases the amount of emulated RAM.
Doesn&apos;t affect performance/stability but may allow HD texture mods to load.</source>
- <translation type="unfinished"/>
+ <translation>エミュレートするRAM容量を増やします。
+パフォーマンスや安定性には影響しませんが、高解像度テクスチャMODなどを読み込めるようになる場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="72"/>
<source>Limit Speed Percent</source>
- <translation>エミュレーション速度の制限</translation>
+ <translation>速度制限 (%)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="73"/>
<source>Controls the game's maximum rendering speed, but it's up to each game if it runs faster or not.
200% for a 30 FPS game is 60 FPS, and for a 60 FPS game it will be 120 FPS.
Disabling it means unlocking the framerate to the maximum your PC can reach.</source>
- <translation type="unfinished"/>
+ <translation>ゲームの最大描画速度を制御します。実際に動作が速くなるかどうかはゲームによって異なります。
+200%に設定すると、30 FPSのゲームは60 FPS、60 FPSのゲームは120 FPSになります。
+無効にするとフレームレートの上限が解除され、PCの性能に応じた最大速度で動作します。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="78"/>
<source>Turbo Speed</source>
- <translation type="unfinished"/>
+ <translation>ターボ速度</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="79"/>
<source>When the Turbo Speed hotkey is pressed, the speed will be limited to this percentage.</source>
- <translation type="unfinished"/>
+ <translation>ターボ速度のホットキーを押した際、速度の上限がこの割合に制限されます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="81"/>
<source>Slow Speed</source>
- <translation type="unfinished"/>
+ <translation>スロー速度</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="82"/>
<source>When the Slow Speed hotkey is pressed, the speed will be limited to this percentage.</source>
- <translation type="unfinished"/>
+ <translation>スロー速度のホットキーを押した際、速度の上限がこの割合に制限されます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="85"/>
<source>Synchronize Core Speed</source>
- <translation type="unfinished"/>
+ <translation>コア速度を同期</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="86"/>
<source>Synchronizes CPU core speed with the game's maximum rendering speed to boost FPS without affecting game speed (animations, physics, etc.).
Can help reduce stuttering at lower framerates.</source>
- <translation type="unfinished"/>
+ <translation>CPUコアの速度をゲームの最大描画速度と同期させ、ゲーム内の速度 (アニメーションや物理演算など) に影響を与えずにFPSを向上させます。
+低フレームレート時のカクつきを軽減できる場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="91"/>
<source>Accuracy:</source>
- <translation>エミュレーション精度:</translation>
+ <translation>精度:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="92"/>
<source>Change the accuracy of the emulated CPU (for debugging only).</source>
- <translation type="unfinished"/>
+ <translation>エミュレートするCPUの精度を変更します (デバッグ専用)。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="93"/>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="103"/>
<source>Backend:</source>
- <translation>バックエンド:</translation>
+ <translation>バックエンド:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="95"/>
<source>CPU Clocks</source>
- <translation type="unfinished"/>
+ <translation>CPUクロック</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="96"/>
<source>Raises the clock the emulated CPU reports, which removes some FPS limiters.
Weaker CPUs may see reduced performance, and certain games may behave improperly.</source>
- <translation type="unfinished"/>
+ <translation>エミュレートするCPUが報告するクロック周波数を引き上げ、一部のFPS制限を解除します。
+性能の低いCPUではパフォーマンスが低下したり、一部のゲームが正常に動作しなくなったりする場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="100"/>
<source>Custom CPU Ticks</source>
- <translation type="unfinished"/>
+ <translation>カスタムCPUティック</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="101"/>
<source>Set a custom value of CPU ticks. Higher values can increase performance, but may cause deadlocks. A range of 77-21000 is recommended.</source>
- <translation type="unfinished"/>
+ <translation>CPUティックのカスタム値を設定します。値を大きくするとパフォーマンスが向上する場合がありますが、フリーズ (デッドロック) の原因になることがあります。推奨範囲は77〜21000です。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="109"/>
<source>Enable Host MMU Emulation (fastmem)</source>
- <translation type="unfinished"/>
+ <translation>ホストMMUエミュレーションを有効化 (fastmem)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="110"/>
<source>This optimization speeds up memory accesses by the guest program.
Enabling it causes guest memory reads/writes to be done directly into memory and make use of Host's MMU.
Disabling this forces all memory accesses to use Software MMU Emulation.</source>
- <translation type="unfinished"/>
+ <translation>ゲストプログラムのメモリアクセスを高速化する最適化です。
+有効にすると、ホストのMMUを利用してゲストのメモリ読み書きを直接メモリ上で行います。
+無効にすると、すべてのメモリアクセスでソフトウェアMMUエミュレーションが使用されます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="115"/>
<source>Unfuse FMA (improve performance on CPUs without FMA)</source>
- <translation>FMAの融合を解除 (FMAに対応していないCPUのパフォーマンスを向上させる)</translation>
+ <translation>FMAを分離 (FMA非対応CPUのパフォーマンスを向上)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="116"/>
<source>This option improves speed by reducing accuracy of fused-multiply-add instructions on CPUs without native FMA support.</source>
- <translation>このオプションは, ネイティブのFMAサポートがないCPU上で, 融合積和(fused-multiply-add)命令の精度を下げて高速化します.</translation>
+ <translation>FMAにネイティブ対応していないCPUにおいて、融合積和演算 (FMA) 命令の精度を下げることで処理速度を向上させます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="119"/>
<source>Faster FRSQRTE and FRECPE</source>
- <translation>Faster FRSQRTE and FRECPE</translation>
+ <translation>FRSQRTEとFRECPEを高速化</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="120"/>
<source>This option improves the speed of some approximate floating-point functions by using less accurate native approximations.</source>
- <translation>このオプションは、より精度の低い近似値を使用することで、近似浮動小数点関数の速度を向上させます。</translation>
+ <translation>精度の低いネイティブ近似を使用することで、一部の近似浮動小数点関数の処理速度を向上させます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="123"/>
<source>Faster ASIMD instructions (32 bits only)</source>
- <translation>高速なASIMD命令 (32bitのみ)</translation>
+ <translation>ASIMD命令を高速化 (32ビットのみ)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="124"/>
<source>This option improves the speed of 32 bits ASIMD floating-point functions by running with incorrect rounding modes.</source>
- <translation>このオプションは、不正確な丸めモードで実行することにより、32ビットASIMD浮動小数点関数の速度を向上させます。</translation>
+ <translation>不正確な丸めモードで実行することで、32ビットASIMD浮動小数点関数の処理速度を向上させます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="126"/>
<source>Inaccurate NaN handling</source>
- <translation>不正確な非数値の取り扱い</translation>
+ <translation>不正確なNaN処理</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="127"/>
<source>This option improves speed by removing NaN checking.
Please note this also reduces accuracy of certain floating-point instructions.</source>
- <translation type="unfinished"/>
+ <translation>NaNチェックを省略することで処理速度を向上させます。
+一部の浮動小数点命令の精度が低下するためご注意ください。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="129"/>
<source>Disable address space checks</source>
- <translation>アドレス空間チェックの無効化</translation>
+ <translation>アドレス空間チェックを無効化</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="130"/>
<source>This option improves speed by eliminating a safety check before every memory operation.
Disabling it may allow arbitrary code execution.</source>
- <translation type="unfinished"/>
+ <translation>メモリアクセスごとの安全性チェックを省略することで処理速度を向上させます。
+無効にすると、任意のコードが実行されるリスクがあります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="133"/>
<source>Ignore global monitor</source>
- <translation type="unfinished"/>
+ <translation>グローバルモニターを無視</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="134"/>
<source>This option improves speed by relying only on the semantics of cmpxchg to ensure safety of exclusive access instructions.
Please note this may result in deadlocks and other race conditions.</source>
- <translation type="unfinished"/>
+ <translation>排他アクセス命令の安全性をcmpxchgの動作のみに依存させることで処理速度を向上させます。
+デッドロックや競合状態が発生する可能性があるためご注意ください。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="139"/>
<source>API:</source>
- <translation>API:</translation>
+ <translation>API:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="140"/>
<source>Changes the output graphics API.
Vulkan is recommended.</source>
- <translation type="unfinished"/>
+ <translation>出力するグラフィックスAPIを変更します。
+Vulkanを推奨します。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="141"/>
<source>Device:</source>
- <translation>使用デバイス:</translation>
+ <translation>デバイス:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="142"/>
<source>This setting selects the GPU to use (Vulkan only).</source>
- <translation type="unfinished"/>
+ <translation>使用するGPUを選択します (Vulkanのみ)。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="143"/>
@@ -541,7 +560,9 @@ Vulkan is recommended.</source>
<source>Forces to render at a different resolution.
Higher resolutions require more VRAM and bandwidth.
Options lower than 1X can cause artifacts.</source>
- <translation type="unfinished"/>
+ <translation>指定した解像度で強制的に描画します。
+解像度を上げると、より多くのVRAMと帯域幅が必要になります。
+1X未満に設定すると、表示が乱れる場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="147"/>
@@ -551,12 +572,12 @@ Options lower than 1X can cause artifacts.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="148"/>
<source>FSR Sharpness:</source>
- <translation>FSR シャープネス:</translation>
+ <translation>FSRシャープネス:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="149"/>
<source>Determines how sharpened the image will look using FSR&apos;s or SGSR&apos;s dynamic contrast.</source>
- <translation type="unfinished"/>
+ <translation>FSRまたはSGSRのダイナミックコントラストを使用して、映像をどの程度シャープにするかを設定します。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="150"/>
@@ -568,17 +589,19 @@ Options lower than 1X can cause artifacts.</source>
<source>The anti-aliasing method to use.
SMAA offers the best quality.
FXAA can produce a more stable picture in lower resolutions.</source>
- <translation type="unfinished"/>
+ <translation>使用するアンチエイリアス方式を選択します。
+SMAAは最も高品質です。
+FXAAは低解像度でもより安定した映像を得られる場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="155"/>
<source>Enable post-processing effects</source>
- <translation type="unfinished"/>
+ <translation>ポストプロセスエフェクトを有効化</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="156"/>
<source>Applies post-processing effects to the final image.</source>
- <translation type="unfinished"/>
+ <translation>最終的な映像にポストプロセスエフェクトを適用します。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="157"/>
@@ -590,58 +613,66 @@ FXAA can produce a more stable picture in lower resolutions.</source>
<source>The method used to render the window in fullscreen.
Borderless offers the best compatibility with the on-screen keyboard that some games request for input.
Exclusive fullscreen may offer better performance and better Freesync/Gsync support.</source>
- <translation type="unfinished"/>
+ <translation>フルスクリーン表示時の描画方式を選択します。
+「ボーダーレスウィンドウ」は、一部のゲームで要求されるソフトウェアキーボードとの互換性に優れています。
+「排他的フルスクリーン」は、パフォーマンスやFreeSync/G-Syncの動作が向上する場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="162"/>
<source>Aspect Ratio:</source>
- <translation>アスペクト比:</translation>
+ <translation>アスペクト比:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="163"/>
<source>Stretches the renderer to fit the specified aspect ratio.
Most games only support 16:9, so modifications are required to get other ratios.
Also controls the aspect ratio of captured screenshots.</source>
- <translation type="unfinished"/>
+ <translation>指定したアスペクト比に合わせて画面を引き伸ばします。
+ほとんどのゲームは16:9にのみ対応しているため、他の比率で正常に表示するにはMODが必要です。
+撮影するスクリーンショットのアスペクト比にも適用されます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="166"/>
<source>Use persistent pipeline cache</source>
- <translation type="unfinished"/>
+ <translation>永続パイプラインキャッシュを使用</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="167"/>
<source>Allows saving shaders to storage for faster loading on following game boots.
Disabling it is only intended for debugging.</source>
- <translation type="unfinished"/>
+ <translation>シェーダーをストレージに保存し、次回以降のゲーム起動時の読み込みを高速化します。
+無効にするのはデバッグ目的のみです。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="170"/>
<source>Use asynchronous GPU emulation</source>
- <translation type="unfinished"/>
+ <translation>非同期GPUエミュレーションを使用</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="171"/>
<source>Uses an extra CPU thread for rendering.
This option should always remain enabled.</source>
- <translation type="unfinished"/>
+ <translation>描画に追加のCPUスレッドを使用します。
+このオプションは常に有効にしておくことを推奨します。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="172"/>
<source>NVDEC emulation:</source>
- <translation>NVDEC エミュレーション:</translation>
+ <translation>NVDECエミュレーション:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="173"/>
<source>Specifies how videos should be decoded.
It can either use the CPU or the GPU for decoding, or perform no decoding at all (black screen on videos).
In most cases, GPU decoding provides the best performance.</source>
- <translation type="unfinished"/>
+ <translation>動画のデコード方法を指定します。
+CPUまたはGPUでデコードするか、デコードを行わない (動画再生時に黒画面になる) かを選択できます。
+ほとんどの場合、GPUデコードで最も高いパフォーマンスが得られます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="176"/>
<source>ASTC Decoding Method:</source>
- <translation>ASTC デコード方式:</translation>
+ <translation>ASTCデコード方式:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="177"/>
@@ -650,75 +681,81 @@ CPU: Use the CPU for decoding.
GPU: Use the GPU's compute shaders to decode ASTC textures (recommended).
CPU Asynchronously: Use the CPU to decode ASTC textures on demand. EliminatesASTC decoding
stuttering but may present artifacts.</source>
- <translation type="unfinished"/>
+ <translation>ASTCテクスチャのデコード方法を設定します。
+CPU: CPUを使用してデコードします。
+GPU: GPUのコンピュートシェーダーを使用してデコードします (推奨)。
+CPU (非同期): 必要に応じてCPUで非同期にデコードします。デコード時のカクつきは解消されますが、表示が乱れる場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="182"/>
<source>ASTC Recompression Method:</source>
- <translation>ASTC 再圧縮方式:</translation>
+ <translation>ASTC再圧縮方式:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="183"/>
<source>Most GPUs lack support for ASTC textures and must decompress to anintermediate format: RGBA8.
BC1/BC3: The intermediate format will be recompressed to BC1 or BC3 format,
saving VRAM but degrading image quality.</source>
- <translation type="unfinished"/>
+ <translation>ほとんどのGPUはASTCテクスチャにネイティブ対応しておらず、中間形式のRGBA8に展開する必要があります。
+BC1/BC3: 中間形式をBC1またはBC3形式に再圧縮します。
+VRAM使用量を節約できますが、画質が低下します。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="187"/>
<source>Frame Pacing Mode (Vulkan only)</source>
- <translation type="unfinished"/>
+ <translation>フレームペーシングモード (Vulkanのみ)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="188"/>
<source>Controls how the emulator manages frame pacing to reduce stuttering and make the frame rate smoother and more consistent.</source>
- <translation type="unfinished"/>
+ <translation>エミュレーターがフレームペーシングを管理する方法を設定し、カクつきを軽減してフレームレートをより滑らかで安定したものにします。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="190"/>
<source>VRAM Usage Mode:</source>
- <translation type="unfinished"/>
+ <translation>VRAM使用モード:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="191"/>
<source>Selects whether the emulator should prefer to conserve memory or make maximum usage of available video memory for performance.
Aggressive mode may impact performance of other applications such as recording software.</source>
- <translation type="unfinished"/>
+ <translation>メモリの節約を優先するか、パフォーマンス向上のために利用可能なビデオメモリを最大限活用するかを選択します。
+「積極的」に設定すると、録画ソフトなど他のアプリケーションの動作に影響を与える場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="194"/>
<source>Skip CPU Inner Invalidation</source>
- <translation type="unfinished"/>
+ <translation>CPU内部キャッシュの無効化をスキップ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="195"/>
<source>Skips certain cache invalidations during memory updates, reducing CPU usage and improving latency. This may cause soft-crashes.</source>
- <translation type="unfinished"/>
+ <translation>メモリ更新時の特定のキャッシュ無効化処理をスキップし、CPU使用率と遅延を軽減します。ソフトクラッシュが発生する場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="198"/>
<source>GPU Readback Before NCE Invalidation</source>
- <translation type="unfinished"/>
+ <translation>NCE無効化前にGPUリードバックを実行</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="199"/>
<source>Flushes GPU buffer data back to guest memory before NCE invalidates the whole guest page, preserving it while the CPU store proceeds.</source>
- <translation type="unfinished"/>
+ <translation>NCEがゲストのページ全体を無効化する前に、GPUバッファのデータをゲストメモリに書き戻し、CPUのストア処理が進む間もデータを保持します。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="202"/>
<source>Patch Runtime NROs for NCE</source>
- <translation type="unfinished"/>
+ <translation>NCE用にランタイムNROをパッチ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="203"/>
<source>Applies NCE code patches to runtime-loaded NRO modules.This is required for some games to run correctly.</source>
- <translation type="unfinished"/>
+ <translation>実行時に読み込まれるNROモジュールにNCEコードパッチを適用します。一部のゲームを正しく動作させるために必要です。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="205"/>
<source>VSync Mode:</source>
- <translation>垂直同期:</translation>
+ <translation>垂直同期モード:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="206"/>
@@ -726,10 +763,10 @@ Aggressive mode may impact performance of other applications such as recording s
FIFO Relaxed allows tearing as it recovers from a slow down.
Mailbox can have lower latency than FIFO and does not tear but may drop frames.
Immediate (no synchronization) presents whatever is available and can exhibit tearing.</source>
- <translation>FIFO (VSync) はフレーム落ちやティアリングがありませんがスクリーンのリフレッシュレートに制限されます.
-FIFO Relaxed は FIFO に似ていますが, 速度低下からの回復時にティアリングを許容します.
-Mailbox は FIFO よりも遅延が小さくティアリングがありませんがフレーム落ちの可能性があります.
-Immediate (no synchronization) は表示可能なものをすべて表示し, ティアリング発生の可能性があります.</translation>
+ <translation>FIFO (VSync): フレーム落ちやティアリングは発生しませんが、画面のリフレッシュレートに制限されます。
+FIFO Relaxed: 処理落ちからの復帰時にティアリングを許容します。
+Mailbox: FIFOより低遅延でティアリングも発生しませんが、フレームが間引かれる場合があります。
+Immediate (同期なし): 利用可能なフレームを即座に表示するため、ティアリングが発生する場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="218"/>
@@ -741,38 +778,41 @@ Immediate (no synchronization) は表示可能なものをすべて表示し, ãƒ
<source>Ensures data consistency between compute and memory operations.
This option fixes issues in games, but may degrade performance.
Unreal Engine 4 games often see the most significant changes thereof.</source>
- <translation type="unfinished"/>
+ <translation>コンピュート処理とメモリ操作の間のデータ整合性を確保します。
+ゲームの描画や動作の問題が修正されますが、パフォーマンスが低下する場合があります。
+特にUnreal Engine 4製のゲームで効果が見られます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="222"/>
<source>Enable asynchronous presentation (Vulkan only)</source>
- <translation>非同期プレゼンテーション (Vulkan のみ)</translation>
+ <translation>非同期プレゼンテーションを有効化 (Vulkanのみ)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="223"/>
<source>Slightly improves performance by moving presentation to a separate CPU thread.</source>
- <translation type="unfinished"/>
+ <translation>プレゼンテーションを別のCPUスレッドに移すことで、パフォーマンスをわずかに向上させます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="225"/>
<source>Force maximum clocks (Vulkan only)</source>
- <translation>最大クロック強制 (Vulkan のみ)</translation>
+ <translation>最大クロックを強制 (Vulkanのみ)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="226"/>
<source>Runs work in the background while waiting for graphics commands to keep the GPU from lowering its clock speed.</source>
- <translation>GPUのクロックスピードを下げないように、グラフィックコマンドを待っている間、バックグラウンドで作業を実行させます。</translation>
+ <translation>グラフィックスコマンドの待機中にバックグラウンドで処理を実行し、GPUのクロック速度が下がらないようにします。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="228"/>
<source>Anisotropic Filtering:</source>
- <translation>異方性フィルタリング:</translation>
+ <translation>異方性フィルタリング:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="229"/>
<source>Controls the quality of texture rendering at oblique angles.
Safe to set at 16x on most GPUs.</source>
- <translation type="unfinished"/>
+ <translation>斜めから見たテクスチャの描画品質を制御します。
+ほとんどのGPUでは16倍に設定しても問題ありません。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="231"/>
@@ -784,7 +824,9 @@ Safe to set at 16x on most GPUs.</source>
<source>Controls the GPU emulation mode.
Most games render fine with Fast, but Accurate is still required for some.
Particles tend to only render correctly with Accurate mode.</source>
- <translation type="unfinished"/>
+ <translation>GPUエミュレーションのモードを設定します。
+ほとんどのゲームは「高速」で問題なく描画されますが、一部のゲームでは「高精度」が必要です。
+パーティクルなどは「高精度」モードでないと正しく描画されない傾向があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="234"/>
@@ -796,12 +838,14 @@ Particles tend to only render correctly with Accurate mode.</source>
<source>Controls the DMA read mode.
Unsafe is faster, while Safe is more stable and can fix issues in some games.
Default follows the GPU Accuracy setting.</source>
- <translation type="unfinished"/>
+ <translation>DMAの読み取りモードを設定します。
+「高速 (非安全)」は高速ですが、「安全 (安定)」はより安定しており一部のゲームの問題を修正できます。
+「デフォルト」はGPUモードの設定に従います。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="236"/>
<source>GPU Fence Behavior:</source>
- <translation type="unfinished"/>
+ <translation>GPUフェンスの動作:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="237"/>
@@ -810,189 +854,205 @@ Immediate is the fastest option, but can introduce some issues.
Balanced offers better compatibility and may fix issues in some games.
Accurate further improves compatibility at the cost of some performance.
Default follows the GPU Mode setting.</source>
- <translation type="unfinished"/>
+ <translation>GPUフェンスの同期動作を設定します。
+「即時」は最速ですが、不具合が発生する場合があります。
+「バランス」は互換性が高く、一部のゲームの問題を修正できる場合があります。
+「高精度」はパフォーマンスをやや犠牲にして互換性をさらに高めます。
+「デフォルト」はGPUモードの設定に従います。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="238"/>
<source>Enable asynchronous shader compilation</source>
- <translation type="unfinished"/>
+ <translation>非同期シェーダーコンパイルを有効化</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="239"/>
<source>May reduce shader stutter.</source>
- <translation type="unfinished"/>
+ <translation>シェーダーによるカクつきを軽減できる場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="240"/>
<source>GPU Clocks</source>
- <translation type="unfinished"/>
+ <translation>GPUクロック</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="241"/>
<source>Makes the game believe GPU work finishes faster than it does, so it stops lowering resolution and render distance to fit the Switch&apos;s clocks.</source>
- <translation type="unfinished"/>
+ <translation>GPU処理が実際より速く完了したとゲーム側に認識させ、Switchのクロックに合わせて動的解像度や描画距離が下げられるのを防ぎます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="243"/>
<source>GPU Unswizzle</source>
- <translation type="unfinished"/>
+ <translation>GPUアンスウィズル</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="244"/>
<source>Accelerates BCn 3D texture decoding using GPU compute.
Disable if experiencing crashes or graphical glitches.</source>
- <translation type="unfinished"/>
+ <translation>GPUコンピュートを使用してBCn 3Dテクスチャのデコードを高速化します。
+クラッシュや画面の不具合が発生する場合は無効にしてください。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="246"/>
<source>GPU Unswizzle Max Texture Size</source>
- <translation type="unfinished"/>
+ <translation>GPUアンスウィズルの最大テクスチャサイズ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="247"/>
<source>Sets the maximum size (MiB) for GPU-based texture unswizzling.
While the GPU is faster for medium and large textures, the CPU may be more efficient for very small ones.
Adjust this to find the balance between GPU acceleration and CPU overhead.</source>
- <translation type="unfinished"/>
+ <translation>GPUによるテクスチャのアンスウィズルの最大サイズ (MiB) を設定します。
+中〜大サイズのテクスチャではGPUが高速ですが、非常に小さいテクスチャではCPUの方が効率的な場合があります。
+GPUによる高速化とCPUのオーバーヘッドのバランスに合わせて調整してください。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="251"/>
<source>GPU Unswizzle Stream Size</source>
- <translation type="unfinished"/>
+ <translation>GPUアンスウィズルのストリームサイズ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="252"/>
<source>Sets the maximum amount of texture data (in MiB) processed per frame.
Higher values can reduce stutter during texture loading but may impact frame consistency.</source>
- <translation type="unfinished"/>
+ <translation>1フレームあたりに処理するテクスチャデータの最大量 (MiB) を設定します。
+値を大きくするとテクスチャ読み込み時のカクつきを軽減できますが、フレームの安定性に影響する場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="255"/>
<source>GPU Unswizzle Chunk Size</source>
- <translation type="unfinished"/>
+ <translation>GPUアンスウィズルのチャンクサイズ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="256"/>
<source>Determines the number of depth slices processed in a single dispatch.
Increasing this can improve throughput on high-end GPUs but may cause TDR or driver timeouts on weaker hardware.</source>
- <translation type="unfinished"/>
+ <translation>1回のディスパッチで処理する深度スライスの数を決定します。
+値を大きくすると、ハイエンドGPUではスループットが向上する場合がありますが、性能の低いハードウェアではTDRやドライバーのタイムアウトが発生する可能性があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="260"/>
<source>Use Vulkan pipeline cache</source>
- <translation>Vulkan パイプラインキャッシュを使用</translation>
+ <translation>Vulkanパイプラインキャッシュを使用</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="261"/>
<source>Enables GPU vendor-specific pipeline cache.
This option can improve shader loading time significantly in cases where the Vulkan driver does not store pipeline cache files internally.</source>
- <translation type="unfinished"/>
+ <translation>GPUベンダー固有のパイプラインキャッシュを有効にします。
+Vulkanドライバーがパイプラインキャッシュファイルを内部に保存しない場合、シェーダーの読み込み時間を大幅に短縮できることがあります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="264"/>
<source>Enable Compute Pipelines (Intel Vulkan Only)</source>
- <translation>コンピュート・パイプラインの有効化(インテル Vulkan のみ)</translation>
+ <translation>コンピュートパイプラインを有効化 (Intel Vulkanのみ)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="265"/>
<source>Required by some games.
This setting only exists for Intel proprietary drivers and may crash if enabled.
Compute pipelines are always enabled on all other drivers.</source>
- <translation type="unfinished"/>
+ <translation>一部のゲームで必要になります。
+この設定はIntel製プロプライエタリドライバー環境でのみ表示され、有効にするとクラッシュする場合があります。
+その他のドライバーでは、コンピュートパイプラインは常に有効です。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="269"/>
<source>Enable Reactive Flushing</source>
- <translation type="unfinished"/>
+ <translation>リアクティブフラッシュを有効化</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="270"/>
<source>Uses reactive flushing instead of predictive flushing, allowing more accurate memory syncing.</source>
- <translation type="unfinished"/>
+ <translation>予測フラッシュの代わりにリアクティブフラッシュを使用し、より正確なメモリ同期を可能にします。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="272"/>
<source>Sync to framerate of video playback</source>
- <translation>ビデオ再生のフレームレートに同期する</translation>
+ <translation>動画再生のフレームレートに同期</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="273"/>
<source>Run the game at normal speed during video playback, even when the framerate is unlocked.</source>
- <translation type="unfinished"/>
+ <translation>フレームレートの制限を解除していても、動画再生中はゲームを通常の速度で実行します。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="275"/>
<source>Barrier feedback loops</source>
- <translation type="unfinished"/>
+ <translation>バリアフィードバックループ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="276"/>
<source>Improves rendering of transparency effects in specific games.</source>
- <translation>特定のゲームにおける透明エフェクトのレンダリングを改善します。</translation>
+ <translation>特定のゲームにおける半透明エフェクトの描画を改善します。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="277"/>
<source>Enable buffer history</source>
- <translation type="unfinished"/>
+ <translation>バッファ履歴を有効化</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="278"/>
<source>Enables access to previous buffer states.
This option may improve rendering quality and performance consistency in some games.</source>
- <translation type="unfinished"/>
+ <translation>以前のバッファの状態へのアクセスを可能にします。
+このオプションにより、一部のゲームで描画品質やパフォーマンスの安定性が向上する場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="280"/>
<source>Fix bloom effects</source>
- <translation type="unfinished"/>
+ <translation>ブルームエフェクトを修正</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="280"/>
<source>Removes bloom in Burnout.</source>
- <translation type="unfinished"/>
+ <translation>『Burnout』のブルーム効果を除去します。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="283"/>
<source>Extended Dynamic State</source>
- <translation type="unfinished"/>
+ <translation>拡張ダイナミックステート</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="284"/>
<source>Controls the number of features that can be used in Extended Dynamic State.
Higher states allow for more features and can increase performance, but may cause additional graphical issues.</source>
- <translation type="unfinished"/>
+ <translation>拡張ダイナミックステートで使用できる機能の数を制御します。
+段階を上げると利用できる機能が増えてパフォーマンスが向上する場合がありますが、描画の不具合が増える可能性があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="288"/>
<source>Vertex Input Dynamic State</source>
- <translation type="unfinished"/>
+ <translation>頂点入力ダイナミックステート</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="289"/>
<source>Enables vertex input dynamic state feature for better quality and performance.</source>
- <translation type="unfinished"/>
+ <translation>頂点入力ダイナミックステート機能を有効にし、品質とパフォーマンスを向上させます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="292"/>
<source>Sample Shading</source>
- <translation type="unfinished"/>
+ <translation>サンプルシェーディング</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="293"/>
<source>Allows the fragment shader to execute per sample in a multi-sampled fragment instead of once per fragment. Improves graphics quality at the cost of performance.
Higher values improve quality but degrade performance.</source>
- <translation type="unfinished"/>
+ <translation>マルチサンプリング時に、フラグメントシェーダーをピクセル単位ではなくサンプル単位で実行します。パフォーマンスと引き換えに描画品質が向上します。
+値を大きくすると品質は向上しますが、負荷が高くなります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="300"/>
<source>RNG Seed</source>
- <translation>乱数シード値の変更</translation>
+ <translation>RNGシード</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="301"/>
<source>Controls the seed of the random number generator.
Mainly used for speedrunning.</source>
- <translation type="unfinished"/>
+ <translation>乱数生成器 (RNG) のシード値を設定します。
+主にRTA (スピードラン) で使用されます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="303"/>
@@ -1002,33 +1062,34 @@ Mainly used for speedrunning.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="303"/>
<source>The name of the console.</source>
- <translation type="unfinished"/>
+ <translation>本体の名前です。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="304"/>
<source>Homebrew Args</source>
- <translation type="unfinished"/>
+ <translation>Homebrew引数</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="305"/>
<source>Command-line arguments passed to homebrew at launch (e.g. -noglsl).</source>
- <translation type="unfinished"/>
+ <translation>起動時にHomebrewへ渡すコマンドライン引数です (例: -noglsl)。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="306"/>
<source>Custom RTC Date:</source>
- <translation type="unfinished"/>
+ <translation>カスタムRTC日付:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="307"/>
<source>This option allows to change the clock of the console.
Can be used to manipulate time in games.</source>
- <translation type="unfinished"/>
+ <translation>本体の時計を変更できます。
+ゲーム内の時間を操作するために使用できます。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="311"/>
<source>The number of seconds from the current unix time</source>
- <translation type="unfinished"/>
+ <translation>現在のUNIX時間からの秒数</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="312"/>
@@ -1038,27 +1099,27 @@ Can be used to manipulate time in games.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="313"/>
<source>This option can be overridden when region setting is auto-select</source>
- <translation type="unfinished"/>
+ <translation>リージョン設定が「自動」の場合、この設定が上書きされることがあります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="314"/>
<source>Region:</source>
- <translation>地域:</translation>
+ <translation>リージョン:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="314"/>
<source>The region of the console.</source>
- <translation type="unfinished"/>
+ <translation>本体のリージョン (地域) です。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="315"/>
<source>Time Zone:</source>
- <translation>タイムゾーン:</translation>
+ <translation>タイムゾーン:</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="315"/>
<source>The time zone of the console.</source>
- <translation type="unfinished"/>
+ <translation>本体のタイムゾーンです。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="316"/>
@@ -1075,38 +1136,41 @@ Can be used to manipulate time in games.</source>
<source>Selects if the console is in Docked or Handheld mode.
Games will change their resolution, details and supported controllers and depending on this setting.
Setting to Handheld can help improve performance for low end systems.</source>
- <translation type="unfinished"/>
+ <translation>本体をドックモード (TVモード) と携帯モードのどちらで動作させるかを選択します。
+この設定に応じて、ゲームの解像度、描画品質、対応コントローラーなどが変化します。
+携帯モードに設定すると、低スペック環境でのパフォーマンスが向上する場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="338"/>
<source>Prompt for user profile on boot</source>
- <translation type="unfinished"/>
+ <translation>起動時にユーザープロファイルを選択</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="339"/>
<source>Useful if multiple people use the same PC.</source>
- <translation type="unfinished"/>
+ <translation>複数の人が同じPCを使用する場合に便利です。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="340"/>
<source>Pause when not in focus</source>
- <translation type="unfinished"/>
+ <translation>非アクティブ時に一時停止</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="341"/>
<source>Pauses emulation when focusing on other windows.</source>
- <translation type="unfinished"/>
+ <translation>他のウィンドウにフォーカスが移ると、エミュレーションを一時停止します。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="342"/>
<source>Confirm before stopping emulation</source>
- <translation>エミュレーションを停止する前に確認する</translation>
+ <translation>エミュレーション停止前の確認</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="343"/>
<source>Overrides prompts asking to confirm stopping the emulation.
Enabling it bypasses such prompts and directly exits the emulation.</source>
- <translation type="unfinished"/>
+ <translation>エミュレーション停止時の確認ダイアログの表示方法を設定します。
+「確認しない」に設定すると、確認ダイアログを表示せずに直ちにエミュレーションを終了します。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="345"/>
@@ -1116,37 +1180,37 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="346"/>
<source>Hides the mouse after 2.5s of inactivity.</source>
- <translation type="unfinished"/>
+ <translation>2.5秒間操作がないと、マウスカーソルを非表示にします。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="347"/>
<source>Check for updates</source>
- <translation type="unfinished"/>
+ <translation>アップデートを確認</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="348"/>
<source>Whether or not to check for updates upon startup.</source>
- <translation type="unfinished"/>
+ <translation>起動時にアップデートを確認するかどうかを設定します。</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="351"/>
<source>Enable Gamemode</source>
- <translation type="unfinished"/>
+ <translation>GameModeを有効化</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="353"/>
<source>Force X11 as Graphics Backend</source>
- <translation type="unfinished"/>
+ <translation>グラフィックスバックエンドにX11を強制</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="380"/>
<source>Custom frontend</source>
- <translation type="unfinished"/>
+ <translation>カスタムフロントエンド</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="381"/>
<source>Real applet</source>
- <translation type="unfinished"/>
+ <translation>システムアプレット (実機)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="382"/>
@@ -1157,17 +1221,17 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="387"/>
<source>Never</source>
- <translation type="unfinished"/>
+ <translation>しない</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="388"/>
<source>On Load</source>
- <translation type="unfinished"/>
+ <translation>読み込み時</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="389"/>
<source>Always</source>
- <translation type="unfinished"/>
+ <translation>常時</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="393"/>
@@ -1182,7 +1246,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="395"/>
<source>CPU Asynchronous</source>
- <translation>CPU 非同期</translation>
+ <translation>CPU (非同期)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="400"/>
@@ -1208,32 +1272,32 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="407"/>
<source>30 FPS</source>
- <translation type="unfinished"/>
+ <translation>30 FPS</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="408"/>
<source>60 FPS</source>
- <translation type="unfinished"/>
+ <translation>60 FPS</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="409"/>
<source>90 FPS</source>
- <translation type="unfinished"/>
+ <translation>90 FPS</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="410"/>
<source>120 FPS</source>
- <translation type="unfinished"/>
+ <translation>120 FPS</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="414"/>
<source>Conservative</source>
- <translation type="unfinished"/>
+ <translation>控えめ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="415"/>
<source>Aggressive</source>
- <translation type="unfinished"/>
+ <translation>積極的</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="419"/>
@@ -1248,29 +1312,29 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="422"/>
<source>OpenGL GLASM (Assembly Shaders, NVIDIA Only)</source>
- <translation>OpenGL GLASM (アセンブリシェーダー、NVIDIA のみ)</translation>
+ <translation>OpenGL GLASM (アセンブリシェーダー、NVIDIAのみ)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="423"/>
<source>OpenGL SPIR-V (Experimental, AMD/Mesa Only)</source>
- <translation>OpenGL SPIR-V(実験的、AMD/Mesaのみ)</translation>
+ <translation>OpenGL SPIR-V (実験的、AMD/Mesaのみ)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="425"/>
<source>Null</source>
- <translation type="unfinished"/>
+ <translation>なし</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="428"/>
<source>Fast</source>
- <translation type="unfinished"/>
+ <translation>高速</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="429"/>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="442"/>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="448"/>
<source>Accurate</source>
- <translation>正確</translation>
+ <translation>高精度</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="433"/>
@@ -1282,27 +1346,27 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="434"/>
<source>Unsafe (fast)</source>
- <translation type="unfinished"/>
+ <translation>高速 (非安全)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="435"/>
<source>Safe (stable)</source>
- <translation type="unfinished"/>
+ <translation>安全 (安定)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="440"/>
<source>Immediate</source>
- <translation type="unfinished"/>
+ <translation>即時</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="441"/>
<source>Balanced</source>
- <translation type="unfinished"/>
+ <translation>バランス</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="449"/>
<source>Unsafe</source>
- <translation>不安定</translation>
+ <translation>非安全</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="450"/>
@@ -1312,7 +1376,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="451"/>
<source>Debugging</source>
- <translation type="unfinished"/>
+ <translation>デバッグ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="455"/>
@@ -1337,22 +1401,22 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="465"/>
<source>No Video Output</source>
- <translation>ビデオ出力しない</translation>
+ <translation>映像出力なし</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="466"/>
<source>CPU Video Decoding</source>
- <translation>ビデオをCPUでデコード</translation>
+ <translation>CPUで動画をデコード</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="467"/>
<source>GPU Video Decoding (Default)</source>
- <translation>ビデオをGPUでデコード (デフォルト)</translation>
+ <translation>GPUで動画をデコード (デフォルト)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="472"/>
<source>0.25X (180p/270p) [EXPERIMENTAL]</source>
- <translation>0.25X (180p/270p) [EXPERIMENTAL]</translation>
+ <translation>0.25X (180p/270p) [実験的]</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="473"/>
@@ -1372,7 +1436,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="476"/>
<source>1.25X (900p/1350p) [EXPERIMENTAL]</source>
- <translation>1.25X (900p/1350p) [EXPERIMENTAL]</translation>
+ <translation>1.25X (900p/1350p) [実験的]</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="477"/>
@@ -1452,7 +1516,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="495"/>
<source>Area</source>
- <translation type="unfinished"/>
+ <translation>Area</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="496"/>
@@ -1462,32 +1526,32 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="497"/>
<source>Zero-Tangent</source>
- <translation type="unfinished"/>
+ <translation>Zero-Tangent</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="498"/>
<source>B-Spline</source>
- <translation type="unfinished"/>
+ <translation>B-Spline</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="499"/>
<source>Mitchell</source>
- <translation type="unfinished"/>
+ <translation>Mitchell</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="500"/>
<source>Spline-1</source>
- <translation type="unfinished"/>
+ <translation>Spline-1</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="501"/>
<source>Snapdragon Game Super Resolution</source>
- <translation type="unfinished"/>
+ <translation>Snapdragon Game Super Resolution</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="502"/>
<source>Snapdragon Game Super Resolution EdgeDir</source>
- <translation type="unfinished"/>
+ <translation>Snapdragon Game Super Resolution EdgeDir</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="506"/>
@@ -1512,22 +1576,22 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="513"/>
<source>Force 4:3</source>
- <translation>強制 4:3</translation>
+ <translation>4:3に固定</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="514"/>
<source>Force 21:9</source>
- <translation>強制 21:9</translation>
+ <translation>21:9に固定</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="515"/>
<source>Force 16:10</source>
- <translation>強制 16:10</translation>
+ <translation>16:10に固定</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="516"/>
<source>Stretch to Window</source>
- <translation>ウィンドウに合わせる</translation>
+ <translation>ウィンドウに合わせて拡大</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="520"/>
@@ -1557,7 +1621,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="530"/>
<source>Japanese (日本語)</source>
- <translation>日本語</translation>
+ <translation>日本語 (Japanese)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="531"/>
@@ -1612,7 +1676,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="541"/>
<source>Taiwanese</source>
- <translation>台湾語</translation>
+ <translation>繁体字中国語 (台湾)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="542"/>
@@ -1647,12 +1711,12 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="548"/>
<source>Polish (polska)</source>
- <translation type="unfinished"/>
+ <translation>ポーランド語 (polska)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="549"/>
<source>Thai (แบบไทย)</source>
- <translation type="unfinished"/>
+ <translation>タイ語 (แบบไทย)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="553"/>
@@ -1700,12 +1764,12 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="569"/>
<source>Default (%1)</source>
<comment>Default time zone</comment>
- <translation>既定 (%1)</translation>
+ <translation>デフォルト (%1)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="571"/>
<source>CET</source>
- <translation>中央ヨーロッパ時間</translation>
+ <translation>中央ヨーロッパ時間 (CET)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="572"/>
@@ -1720,7 +1784,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="574"/>
<source>EET</source>
- <translation>東ヨーロッパ標準時</translation>
+ <translation>東ヨーロッパ時間 (EET)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="575"/>
@@ -1735,7 +1799,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="577"/>
<source>EST</source>
- <translation>アメリカ東部標準時</translation>
+ <translation>東部標準時 (EST)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="578"/>
@@ -1750,7 +1814,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="580"/>
<source>GB-Eire</source>
- <translation>イギリス-アイルランド</translation>
+ <translation>イギリス・アイルランド (GB-Eire)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="581"/>
@@ -1785,7 +1849,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="587"/>
<source>HST</source>
- <translation>ハワイ標準時</translation>
+ <translation>ハワイ標準時 (HST)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="588"/>
@@ -1820,12 +1884,12 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="595"/>
<source>MET</source>
- <translation>中東時間</translation>
+ <translation>中央ヨーロッパ時間 (MET)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="596"/>
<source>MST</source>
- <translation>MST</translation>
+ <translation>山岳部標準時 (MST)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="597"/>
@@ -1910,7 +1974,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="613"/>
<source>WET</source>
- <translation>WET</translation>
+ <translation>西ヨーロッパ時間 (WET)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="614"/>
@@ -1940,7 +2004,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="625"/>
<source>6GB DRAM (Unsafe)</source>
- <translation>6GB DRAM (不安定)</translation>
+ <translation>6GB DRAM (非推奨)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="626"/>
@@ -1950,17 +2014,17 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="627"/>
<source>10GB DRAM (Unsafe)</source>
- <translation type="unfinished"/>
+ <translation>10GB DRAM (非推奨)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="628"/>
<source>12GB DRAM (Unsafe)</source>
- <translation type="unfinished"/>
+ <translation>12GB DRAM (非推奨)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="632"/>
<source>Docked</source>
- <translation>Docked</translation>
+ <translation>ドックモード</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="633"/>
@@ -1971,19 +2035,19 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="637"/>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="650"/>
<source>Normal</source>
- <translation type="unfinished"/>
+ <translation>標準</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="638"/>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="651"/>
<source>Boost</source>
- <translation type="unfinished"/>
+ <translation>ブースト</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="639"/>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="652"/>
<source>Overclock</source>
- <translation type="unfinished"/>
+ <translation>オーバークロック</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="644"/>
@@ -1993,7 +2057,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="645"/>
<source>Only if game specifies not to stop</source>
- <translation>ゲームが停止しないように指定しているときのみ</translation>
+ <translation>ゲームが停止しないよう指定している場合のみ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="646"/>
@@ -2003,102 +2067,102 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="656"/>
<source>Very Small (16 MB)</source>
- <translation type="unfinished"/>
+ <translation>最小 (16 MB)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="657"/>
<source>Small (32 MB)</source>
- <translation type="unfinished"/>
+ <translation>小 (32 MB)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="658"/>
<source>Normal (128 MB)</source>
- <translation type="unfinished"/>
+ <translation>標準 (128 MB)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="659"/>
<source>Large (256 MB)</source>
- <translation type="unfinished"/>
+ <translation>大 (256 MB)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="660"/>
<source>Very Large (512 MB)</source>
- <translation type="unfinished"/>
+ <translation>最大 (512 MB)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="664"/>
<source>Very Low (4 MB)</source>
- <translation type="unfinished"/>
+ <translation>最低 (4 MB)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="665"/>
<source>Low (8 MB)</source>
- <translation type="unfinished"/>
+ <translation>低 (8 MB)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="666"/>
<source>Normal (16 MB)</source>
- <translation type="unfinished"/>
+ <translation>標準 (16 MB)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="667"/>
<source>Medium (32 MB)</source>
- <translation type="unfinished"/>
+ <translation>中 (32 MB)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="668"/>
<source>High (64 MB)</source>
- <translation type="unfinished"/>
+ <translation>高 (64 MB)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="672"/>
<source>Very Low (32)</source>
- <translation type="unfinished"/>
+ <translation>最低 (32)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="673"/>
<source>Low (64)</source>
- <translation type="unfinished"/>
+ <translation>低 (64)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="674"/>
<source>Normal (128)</source>
- <translation type="unfinished"/>
+ <translation>標準 (128)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="675"/>
<source>Medium (256)</source>
- <translation type="unfinished"/>
+ <translation>中 (256)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="676"/>
<source>High (512)</source>
- <translation type="unfinished"/>
+ <translation>高 (512)</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="682"/>
<source>ExtendedDynamicState 1</source>
- <translation type="unfinished"/>
+ <translation>ExtendedDynamicState 1</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="683"/>
<source>ExtendedDynamicState 2</source>
- <translation type="unfinished"/>
+ <translation>ExtendedDynamicState 2</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="684"/>
<source>ExtendedDynamicState 3</source>
- <translation type="unfinished"/>
+ <translation>ExtendedDynamicState 3</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="689"/>
<source>Tree View</source>
- <translation type="unfinished"/>
+ <translation>ツリー表示</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.cpp" line="690"/>
<source>Grid View</source>
- <translation type="unfinished"/>
+ <translation>グリッド表示</translation>
</message>
</context>
<context>
@@ -2111,12 +2175,12 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_applets.ui" line="17"/>
<source>Applets</source>
- <translation type="unfinished"/>
+ <translation>アプレット</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_applets.ui" line="25"/>
<source>Applet mode preference</source>
- <translation type="unfinished"/>
+ <translation>アプレットモードの優先設定</translation>
</message>
</context>
<context>
@@ -2133,22 +2197,22 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_camera.ui" line="14"/>
<source>Configure Infrared Camera</source>
- <translation>赤外線カメラを設定</translation>
+ <translation>赤外線カメラの設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_camera.ui" line="26"/>
<source>Select where the image of the emulated camera comes from. It may be a virtual camera or a real camera.</source>
- <translation>エミュレートされたカメラの画像のソースを選択します。仮想カメラでも実際のカメラでもかまいません。</translation>
+ <translation>エミュレートするカメラ映像の入力元を選択します。仮想カメラまたは実際のカメラを使用できます。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_camera.ui" line="52"/>
<source>Camera Image Source:</source>
- <translation>カメラ映像入力元:</translation>
+ <translation>カメラ映像の入力元:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_camera.ui" line="71"/>
<source>Input device:</source>
- <translation>入力デバイス</translation>
+ <translation>入力デバイス:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_camera.ui" line="96"/>
@@ -2158,7 +2222,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_camera.ui" line="108"/>
<source>Resolution: 320*240</source>
- <translation>解像度: 320*240</translation>
+ <translation>解像度: 320×240</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_camera.ui" line="115"/>
@@ -2196,7 +2260,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu.ui" line="52"/>
<source>We recommend setting accuracy to &quot;Auto&quot;.</source>
- <translation>エミュレーション精度の設定は「自動」推奨です.</translation>
+ <translation>精度は「自動」に設定することを推奨します。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu.ui" line="68"/>
@@ -2206,12 +2270,12 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu.ui" line="95"/>
<source>Unsafe CPU Optimization Settings</source>
- <translation>不安定なCPU最適化設定</translation>
+ <translation>非安全なCPU最適化設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu.ui" line="101"/>
<source>These settings reduce accuracy for speed.</source>
- <translation>これらの設定は高速化のために精度を犠牲にします.</translation>
+ <translation>これらの設定は、エミュレーション精度を犠牲にして処理速度を向上させます。</translation>
</message>
</context>
<context>
@@ -2229,12 +2293,12 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="25"/>
<source>Toggle CPU Optimizations</source>
- <translation>CPU最適化設定の切替</translation>
+ <translation>CPU最適化の切り替え</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="31"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;For debugging only.&lt;/span&gt;&lt;br/&gt;If you&apos;re not sure what these do, keep all of these enabled. &lt;br/&gt;These settings, when disabled, only take effect when CPU Debugging is enabled. &lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
- <translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;デバッグ用の設定です。&lt;/span&gt;設定の意味が理解できない場合は、全て有効のままにしてください。 &lt;br/&gt;これらの設定を無効にしても、設定内容が反映されるのはCPUデバッグが有効時のみです。&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
+ <translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;デバッグ専用の設定です。&lt;/span&gt;&lt;br/&gt;各設定の役割がわからない場合は、すべて有効のままにしてください。&lt;br/&gt;これらの設定を無効にした場合、CPUの精度が「デバッグ」のときのみ反映されます。&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="41"/>
@@ -2245,14 +2309,14 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
</source>
<translation>
&lt;div style=&quot;white-space: nowrap&quot;&gt;この最適化により、ゲストプログラムによるメモリアクセスが高速化されます。&lt;/div&gt;
- &lt;div style=&quot;white-space: nowrap&quot;&gt;これを有効にすると、PageTable :: pointersへのアクセスが発行されたコードにインライン化されます。&lt;/div&gt;
- &lt;div style=&quot;white-space: nowrap&quot;&gt;これを無効にすると、すべてのメモリアクセスが強制的にMemory :: Read / Memory :: Write関数を経由します。&lt;/div&gt;
+ &lt;div style=&quot;white-space: nowrap&quot;&gt;有効にすると、PageTable::pointers へのアクセスが生成コードにインライン展開されます。&lt;/div&gt;
+ &lt;div style=&quot;white-space: nowrap&quot;&gt;無効にすると、すべてのメモリアクセスが強制的に Memory::Read / Memory::Write 関数を経由します。&lt;/div&gt;
</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="48"/>
<source>Enable inline page tables</source>
- <translation>Enable inline page tables</translation>
+ <translation>インラインページテーブルを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="55"/>
@@ -2260,13 +2324,13 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
&lt;div&gt;This optimization avoids dispatcher lookups by allowing emitted basic blocks to jump directly to other basic blocks if the destination PC is static.&lt;/div&gt;
</source>
<translation>
- &lt;div&gt;この最適化では、宛先PCが静的な場合、発行された基本ブロックが他の基本ブロックに直接ジャンプできるようにすることで、ディスパッチャーのルックアップを回避します。&lt;/div&gt;
+ &lt;div&gt;この最適化は、ジャンプ先のPCが静的な場合に、生成された基本ブロックから他の基本ブロックへ直接ジャンプできるようにして、ディスパッチャーの検索を回避します。&lt;/div&gt;
</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="60"/>
<source>Enable block linking</source>
- <translation>Enable block linking</translation>
+ <translation>ブロックリンクを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="67"/>
@@ -2274,13 +2338,13 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
&lt;div&gt;This optimization avoids dispatcher lookups by keeping track potential return addresses of BL instructions. This approximates what happens with a return stack buffer on a real CPU.&lt;/div&gt;
</source>
<translation>
- &lt;div&gt;この最適化は、BL命令の潜在的な戻りアドレスを追跡することにより、ディスパッチャーの検索を回避します。これは、実際のCPUのリターンスタックバッファで何が起こるかを概算します。&lt;/div&gt;
+ &lt;div&gt;この最適化は、BL命令の戻りアドレスの候補を追跡することで、ディスパッチャーの検索を回避します。これは、実際のCPUのリターンスタックバッファの動作を近似したものです。&lt;/div&gt;
</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="72"/>
<source>Enable return stack buffer</source>
- <translation>Enable return stack buffer</translation>
+ <translation>リターンスタックバッファを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="79"/>
@@ -2288,13 +2352,13 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
&lt;div&gt;Enable a two-tiered dispatch system. A faster dispatcher written in assembly has a small MRU cache of jump destinations is used first. If that fails, dispatch falls back to the slower C++ dispatcher.&lt;/div&gt;
</source>
<translation>
- &lt;div&gt;2層のディスパッチシステムを有効にします。アセンブリで記述されたより高速なディスパッチャには、ジャンプ先の小さなMRUキャッシュが最初に使用されます。それが失敗した場合、ディスパッチはより遅いC ++ディスパッチャーにフォールバックします。&lt;/div&gt;
+ &lt;div&gt;2段階のディスパッチシステムを有効にします。まず、ジャンプ先の小規模なMRUキャッシュを持つ高速なアセンブリ製ディスパッチャーが使用され、ミスした場合は低速なC++製ディスパッチャーにフォールバックします。&lt;/div&gt;
</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="84"/>
<source>Enable fast dispatcher</source>
- <translation>Enable fast dispatcher</translation>
+ <translation>高速ディスパッチャーを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="91"/>
@@ -2308,7 +2372,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="96"/>
<source>Enable context elimination</source>
- <translation>Enable context elimination</translation>
+ <translation>コンテキスト削減を有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="103"/>
@@ -2316,13 +2380,13 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
&lt;div&gt;Enables IR optimizations that involve constant propagation.&lt;/div&gt;
</source>
<translation>
- &lt;div&gt;一定の伝播を伴うIR最適化を有効にします。&lt;/div&gt;
+ &lt;div&gt;定数伝播を伴うIR最適化を有効にします。&lt;/div&gt;
</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="108"/>
<source>Enable constant propagation</source>
- <translation>定伝播を有効にする</translation>
+ <translation>定数伝播を有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="115"/>
@@ -2345,14 +2409,14 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
&lt;div style=&quot;white-space: nowrap&quot;&gt;When disabled, a misalignment is triggered on all misaligned accesses.&lt;/div&gt;
</source>
<translation>
- &lt;div style=&quot;white-space: nowrap&quot;&gt;有効にすると、アクセスがページの境界を越えたときにのみ、ミスアライメントがトリガーされます。&lt;/div&gt;
- &lt;div style=&quot;white-space: nowrap&quot;&gt;無効にすると、ミスアライメントされたすべてのアクセスでミスアライメントがトリガーされます。&lt;/div&gt;
+ &lt;div style=&quot;white-space: nowrap&quot;&gt;有効にすると、アクセスがページ境界をまたぐ場合にのみ、アライメント違反が発生します。&lt;/div&gt;
+ &lt;div style=&quot;white-space: nowrap&quot;&gt;無効にすると、アライメントされていないすべてのアクセスでアライメント違反が発生します。&lt;/div&gt;
</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="133"/>
<source>Enable misalignment check reduction</source>
- <translation>ミスアライメントチェックの低減を有効にする</translation>
+ <translation>アライメントチェックの削減を有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="140"/>
@@ -2370,7 +2434,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="147"/>
<source>Enable Host MMU Emulation (general memory instructions)</source>
- <translation>ホストMMUエミュレーションの有効化(汎用メモリ命令)</translation>
+ <translation>ホストMMUエミュレーションを有効にする (汎用メモリ命令)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="154"/>
@@ -2388,7 +2452,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="161"/>
<source>Enable Host MMU Emulation (exclusive memory instructions)</source>
- <translation>ホストMMUエミュレーションの有効化(排他的メモリ命令)</translation>
+ <translation>ホストMMUエミュレーションを有効にする (排他的メモリ命令)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="168"/>
@@ -2398,13 +2462,13 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
</source>
<translation>
&lt;div style=&quot;white-space: nowrap&quot;&gt;この最適化により、ゲストプログラムによる排他的メモリアクセスが高速化されます。&lt;/div&gt;
-&lt;div style=&quot;white-space: nowrap&quot;&gt;有効にすると、排他的メモリアクセスのfastmemの失敗によるオーバーヘッドが軽減されます。&lt;/div&gt;
+&lt;div style=&quot;white-space: nowrap&quot;&gt;有効にすると、排他的メモリアクセスでfastmemが失敗した際のオーバーヘッドが軽減されます。&lt;/div&gt;
</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="174"/>
<source>Enable recompilation of exclusive memory instructions</source>
- <translation>排他的メモリ命令のリコンパイルを有効にする</translation>
+ <translation>排他的メモリ命令の再コンパイルを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="181"/>
@@ -2413,19 +2477,19 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
&lt;div style=&quot;white-space: nowrap&quot;&gt;Enabling it reduces the overhead of all memory accesses and has no impact on programs that don't access invalid memory.&lt;/div&gt;
</source>
<translation>
- &lt;div style=&quot;white-space: nowrap&quot;&gt;この最適化により、無効なメモリアクセスを成功させることで、メモリアクセスを高速化することができます。&lt;/div&gt;
- &lt;div style=&quot;white-space: nowrap&quot;&gt;有効にすると、すべてのメモリアクセスのオーバーヘッドが減少するうえ、無効なメモリにアクセスしないプログラムには影響はありません。&lt;/div&gt;
+ &lt;div style=&quot;white-space: nowrap&quot;&gt;無効なメモリアクセスをエラーにせず通過させることで、メモリアクセスを高速化します。&lt;/div&gt;
+ &lt;div style=&quot;white-space: nowrap&quot;&gt;有効にするとすべてのメモリアクセスのオーバーヘッドが軽減され、無効なメモリにアクセスしないプログラムには悪影響を与えません。&lt;/div&gt;
</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="187"/>
<source>Enable fallbacks for invalid memory accesses</source>
- <translation>無効なメモリアクセスに対するフォールバックを有効にする</translation>
+ <translation>無効なメモリアクセスのフォールバックを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_cpu_debug.ui" line="212"/>
<source>CPU settings are available only when game is not running.</source>
- <translation>CPUの設定はゲームを実行していない時にのみ変更できます。</translation>
+ <translation>CPUの設定は、ゲームを実行していないときのみ変更できます。</translation>
</message>
</context>
<context>
@@ -2433,17 +2497,17 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="40"/>
<source>Debugger</source>
- <translation>デバッガ―</translation>
+ <translation>デバッガー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="85"/>
<source>Enable GDB Stub</source>
- <translation>GDBスタブの有効化</translation>
+ <translation>GDBスタブを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="119"/>
<source>Port:</source>
- <translation>ポート:</translation>
+ <translation>ポート:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="157"/>
@@ -2458,37 +2522,37 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="224"/>
<source>GPU Logging/Level</source>
- <translation type="unfinished"/>
+ <translation>GPUログ/レベル</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="231"/>
<source>Detail level for GPU logs. Off disables logging entirely.</source>
- <translation type="unfinished"/>
+ <translation>GPUログの詳細レベルです。「オフ」にするとログを完全に無効にします。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="235"/>
<source>Off</source>
- <translation type="unfinished"/>
+ <translation>オフ</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="240"/>
<source>Errors</source>
- <translation type="unfinished"/>
+ <translation>エラー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="245"/>
<source>Standard</source>
- <translation type="unfinished"/>
+ <translation>標準</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="250"/>
<source>Verbose</source>
- <translation type="unfinished"/>
+ <translation>詳細</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="255"/>
<source>All</source>
- <translation type="unfinished"/>
+ <translation>すべて</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="266"/>
@@ -2498,17 +2562,17 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="276"/>
<source>When checked, the max size of the log increases from 100 MB to 1 GB</source>
- <translation>チェックすると、ログの最大サイズが100MBから1GBに増加します。</translation>
+ <translation>チェックすると、ログの最大サイズが100 MBから1 GBに増加します。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="279"/>
<source>Enable Extended Logging**</source>
- <translation>拡張ログの有効化**</translation>
+ <translation>拡張ログを有効にする**</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="298"/>
<source>Open Log Location</source>
- <translation>ログ出力フォルダを開く</translation>
+ <translation>ログの保存場所を開く</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="310"/>
@@ -2518,27 +2582,27 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="318"/>
<source>Arguments String</source>
- <translation>引数</translation>
+ <translation>引数文字列</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="335"/>
<source>Graphics</source>
- <translation>グラフィック</translation>
+ <translation>グラフィックス</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="341"/>
<source>When checked, it executes shaders without loop logic changes</source>
- <translation>チェックすると、ループロジックを変更せずにシェーダーを実行します。</translation>
+ <translation>チェックすると、ループ処理を変更せずにシェーダーを実行します。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="344"/>
<source>Disable Loop safety checks</source>
- <translation>ループ安全性チェックの無効化</translation>
+ <translation>ループの安全性チェックを無効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="354"/>
<source>When checked, it will dump all the macro programs of the GPU</source>
- <translation>チェックすると、GPUのすべてのマクロプログラムをダンプします</translation>
+ <translation>チェックすると、GPUのすべてのマクロプログラムをダンプします。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="357"/>
@@ -2548,107 +2612,107 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="364"/>
<source>When checked, it enables Nsight Aftermath crash dumps</source>
- <translation>チェックすると、Nsight Aftermathのクラッシュダンプが有効になります</translation>
+ <translation>チェックすると、Nsight Aftermathによるクラッシュダンプを有効にします。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="367"/>
<source>Enable Nsight Aftermath</source>
- <translation>Nsight Aftermathの有効化</translation>
+ <translation>Nsight Aftermathを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="377"/>
<source>When checked, it will dump original Maxwell guest shader bytecode (the input to the recompiler) as .ash files under DumpDir/shaders/. Useful for inspection with nvdisasm.</source>
- <translation type="unfinished"/>
+ <translation>チェックすると、元のMaxwellゲストシェーダーのバイトコード (リコンパイラへの入力) を、DumpDir/shaders/ 以下に .ash ファイルとしてダンプします。nvdisasmでの解析に便利です。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="380"/>
<source>Dump Guest (Maxwell) Shaders</source>
- <translation type="unfinished"/>
+ <translation>ゲスト (Maxwell) シェーダーをダンプ</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="387"/>
<source>Enable Renderdoc Hotkey</source>
- <translation>Renderdocのホットキーを有効化</translation>
+ <translation>Renderdocのホットキーを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="397"/>
<source>When checked, it disables the macro Just In Time compiler. Enabling this makes games run slower</source>
- <translation>チェックすると、マクロのJust In Timeコンパイラが無効になります。有効にすると、ゲームの動作が遅くなります。</translation>
+ <translation>チェックすると、マクロのJITコンパイラを無効にします。有効にすると、ゲームの動作が遅くなります。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="400"/>
<source>Disable Macro JIT</source>
- <translation>Macro JITを無効化</translation>
+ <translation>マクロJITを無効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="410"/>
<source>When checked, it disables the macro HLE functions. Enabling this makes games run slower</source>
- <translation>有効化すると、マクロHLE機能を無効にします。これを有効にすると、ゲームの動作が遅くなります</translation>
+ <translation>チェックすると、マクロのHLE機能を無効にします。有効にすると、ゲームの動作が遅くなります。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="413"/>
<source>Disable Macro HLE</source>
- <translation>Macro HLE を無効化</translation>
+ <translation>マクロHLEを無効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="423"/>
<source>When checked, the graphics API enters a slower debugging mode</source>
- <translation>チェックすると、 グラフィックAPIはより遅いデバッグモードになります。</translation>
+ <translation>チェックすると、グラフィックスAPIが低速なデバッグモードで動作します。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="426"/>
<source>Enable Graphics Debugging</source>
- <translation>グラフィックデバッグの有効化</translation>
+ <translation>グラフィックスのデバッグを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="449"/>
<source>When checked, yuzu will log statistics about the compiled pipeline cache</source>
- <translation>チェックすると、コンパイルしたパイプラインキャッシュの統計情報をロギングします</translation>
+ <translation>チェックすると、コンパイル済みパイプラインキャッシュの統計情報をログに記録します。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="452"/>
<source>Enable Shader Feedback</source>
- <translation>シェーダーフィードバックの有効j化</translation>
+ <translation>シェーダーフィードバックを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="459"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;When checked, disables reordering of mapped memory uploads which allows to associate uploads with specific draws. May reduce performance in some cases.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
- <translation type="unfinished"/>
+ <translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;チェックすると、マップされたメモリのアップロードの並べ替えを無効にします。これにより、アップロードを特定の描画に関連付けられるようになります。場合によってはパフォーマンスが低下することがあります。&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="462"/>
<source>Disable Buffer Reorder</source>
- <translation type="unfinished"/>
+ <translation>バッファの並べ替えを無効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="469"/>
<source>When checked, it will dump the recompiler&apos;s output SPIR-V binaries (.spv) under LogDir/shaders/. Inspect via SPIRV-Tools (spirv-dis / spirv-cross / spirv-val) or RenderDoc.</source>
- <translation type="unfinished"/>
+ <translation>チェックすると、リコンパイラが出力したSPIR-Vバイナリ (.spv) を LogDir/shaders/ 以下にダンプします。SPIRV-Tools (spirv-dis / spirv-cross / spirv-val) やRenderDocで確認できます。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="472"/>
<source>Dump SPIR-V Shaders</source>
- <translation type="unfinished"/>
+ <translation>SPIR-Vシェーダーをダンプ</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="482"/>
<source>Advanced</source>
- <translation>高度</translation>
+ <translation>詳細設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="488"/>
<source>Enables yuzu to check for a working Vulkan environment when the program starts up. Disable this if this is causing issues with external programs seeing yuzu.</source>
- <translation>プログラム起動時にyuzuがVulkan環境が動作しているかどうかを確認するようにします。外部プログラムがyuzuを参照する際に問題がある場合は、これを無効にしてください。</translation>
+ <translation>起動時に動作可能なVulkan環境があるか確認します。外部プログラムからEdenを認識する際に問題が発生する場合は無効にしてください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="491"/>
<source>Perform Startup Vulkan Check</source>
- <translation>スタートアップ時にVulkanのチェックを実行する</translation>
+ <translation>起動時にVulkanのチェックを実行する</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="498"/>
<source>Disable Web Applet</source>
- <translation>Webアプレットの無効化</translation>
+ <translation>Webアプレットを無効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="505"/>
@@ -2658,22 +2722,22 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="512"/>
<source>Enable Auto-Stub</source>
- <translation type="unfinished"/>
+ <translation>オートスタブを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="519"/>
<source>Kiosk (Quest) Mode</source>
- <translation>Kiosk (Quest) Mode</translation>
+ <translation>キオスク (Quest) モード</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="526"/>
<source>Use dev.keys</source>
- <translation type="unfinished"/>
+ <translation>dev.keysを使用する</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="533"/>
<source>Enable Debug Asserts</source>
- <translation>デバッグアサートの有効化</translation>
+ <translation>デバッグアサートを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="559"/>
@@ -2683,42 +2747,42 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="592"/>
<source>Battery Serial:</source>
- <translation type="unfinished"/>
+ <translation>バッテリーのシリアル番号:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="632"/>
<source>Debug Knobs:</source>
- <translation type="unfinished"/>
+ <translation>デバッグノブ:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="645"/>
<source>Bitmask for quick development toggles</source>
- <translation type="unfinished"/>
+ <translation>開発用トグルを素早く切り替えるためのビットマスク</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="648"/>
<source>Set debug knobs (bitmask)</source>
- <translation type="unfinished"/>
+ <translation>デバッグノブを設定 (ビットマスク)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="651"/>
<source>16-bit debug knob set for quick development toggles</source>
- <translation type="unfinished"/>
+ <translation>開発用トグルを素早く切り替えるための16ビットデバッグノブ設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="654"/>
<source> (bitmask)</source>
- <translation type="unfinished"/>
+ <translation> (ビットマスク)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="691"/>
<source>Unit Serial:</source>
- <translation type="unfinished"/>
+ <translation>本体のシリアル番号:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="720"/>
<source>Enable this to output the latest generated audio command list to the console. Only affects games using the audio renderer.</source>
- <translation>これを有効にすると、最新のオーディオコマンドリストがコンソールに出力されます。オーディオレンダラーを使用するゲームにのみ影響します。</translation>
+ <translation>有効にすると、最後に生成されたオーディオコマンドリストをコンソールに出力します。オーディオレンダラーを使用するゲームにのみ影響します。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="723"/>
@@ -2728,27 +2792,27 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="730"/>
<source>Flush log output on each line</source>
- <translation type="unfinished"/>
+ <translation>1行ごとにログ出力をフラッシュする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="737"/>
<source>Enable FS Access Log</source>
- <translation>FSアクセスログの有効化</translation>
+ <translation>FSアクセスログを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="744"/>
<source>Enable Verbose Reporting Services**</source>
- <translation>詳細なレポートサービスの有効化**</translation>
+ <translation>詳細なレポートサービスログを有効にする**</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="751"/>
<source>Censor username in logs</source>
- <translation type="unfinished"/>
+ <translation>ログ内のユーザー名を伏せる</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug.ui" line="787"/>
<source>**This will be reset automatically when Eden closes.</source>
- <translation type="unfinished"/>
+ <translation>**この設定は、Edenの終了時に自動的にリセットされます。</translation>
</message>
</context>
<context>
@@ -2756,12 +2820,12 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_debug_controller.ui" line="14"/>
<source>Configure Debug Controller</source>
- <translation>デバッグコントローラー設定</translation>
+ <translation>デバッグコントローラーを設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug_controller.ui" line="40"/>
<source>Clear</source>
- <translation>全て消去</translation>
+ <translation>クリア</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_debug_controller.ui" line="47"/>
@@ -2793,17 +2857,17 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure.ui" line="20"/>
<source>Eden Configuration</source>
- <translation type="unfinished"/>
+ <translation>Edenの設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure.ui" line="67"/>
<source>Some settings are only available when a game is not running.</source>
- <translation>いくつかの設定はゲームが実行中でないときのみ設定できます</translation>
+ <translation>一部の設定は、ゲームを実行していないときのみ変更できます。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_dialog.cpp" line="69"/>
<source>Applets</source>
- <translation type="unfinished"/>
+ <translation>アプレット</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_dialog.cpp" line="70"/>
@@ -2837,17 +2901,17 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<location filename="../../src/yuzu/configuration/configure_dialog.cpp" line="75"/>
<location filename="../../src/yuzu/configuration/configure_dialog.cpp" line="181"/>
<source>Graphics</source>
- <translation>グラフィック</translation>
+ <translation>グラフィックス</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_dialog.cpp" line="76"/>
<source>GraphicsAdvanced</source>
- <translation>拡張グラフィック</translation>
+ <translation>グラフィックス (詳細)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_dialog.cpp" line="77"/>
<source>GraphicsExtra</source>
- <translation type="unfinished"/>
+ <translation>グラフィックス (追加)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_dialog.cpp" line="78"/>
@@ -2907,7 +2971,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="31"/>
<source>NAND</source>
- <translation>NANDの場所</translation>
+ <translation>NAND</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="38"/>
@@ -2922,12 +2986,12 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="51"/>
<source>SD Card</source>
- <translation>SDカードの場所</translation>
+ <translation>SDカード</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="65"/>
<source>Save Data</source>
- <translation type="unfinished"/>
+ <translation>セーブデータ</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="101"/>
@@ -2937,12 +3001,12 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="107"/>
<source>Path</source>
- <translation>ゲームカードのパス</translation>
+ <translation>パス</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="117"/>
<source>Inserted</source>
- <translation>挿入をエミュレート</translation>
+ <translation>挿入済み</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="124"/>
@@ -2957,7 +3021,7 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="169"/>
<source>Dump Decompressed NSOs</source>
- <translation>解凍されたNSOをダンプ</translation>
+ <translation>展開済みNSOをダンプ</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="176"/>
@@ -2967,12 +3031,12 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="185"/>
<source>Mod Load Root</source>
- <translation>Mod読込元ディレクトリのルート</translation>
+ <translation>MOD読み込み元のルート</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="192"/>
<source>Dump Root</source>
- <translation>ダンプディレクトリのルート</translation>
+ <translation>ダンプ先のルート</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="218"/>
@@ -2987,23 +3051,23 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.ui" line="233"/>
<source>Reset Metadata Cache</source>
- <translation>メタデータのキャッシュをクリア</translation>
+ <translation>メタデータキャッシュをリセット</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="105"/>
<source>Select Emulated NAND Directory...</source>
- <translation>NANDディレクトリを選択...</translation>
+ <translation>エミュレートするNANDのディレクトリを選択...</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="108"/>
<source>Select Emulated SD Directory...</source>
- <translation>SDカードディレクトリを選択...</translation>
+ <translation>エミュレートするSDカードのディレクトリを選択...</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="111"/>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="159"/>
<source>Select Save Data Directory...</source>
- <translation type="unfinished"/>
+ <translation>セーブデータのディレクトリを選択...</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="114"/>
@@ -3013,32 +3077,32 @@ Enabling it bypasses such prompts and directly exits the emulation.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="117"/>
<source>Select Dump Directory...</source>
- <translation>ダンプディレクトリを選択...</translation>
+ <translation>ダンプ先ディレクトリを選択...</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="120"/>
<source>Select Mod Load Directory...</source>
- <translation>Mod読込元ディレクトリを選択...</translation>
+ <translation>MOD読み込み元のディレクトリを選択...</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="148"/>
<source>Save Data Directory</source>
- <translation type="unfinished"/>
+ <translation>セーブデータのディレクトリ</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="149"/>
<source>Choose an action for the save data directory:</source>
- <translation type="unfinished"/>
+ <translation>セーブデータのディレクトリに対する操作を選択してください:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="151"/>
<source>Set Custom Path</source>
- <translation type="unfinished"/>
+ <translation>カスタムパスを設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="152"/>
<source>Reset to NAND</source>
- <translation type="unfinished"/>
+ <translation>NANDに戻す</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="208"/>
@@ -3049,7 +3113,13 @@ New: %2
Would you like to migrate saves from the old location?
WARNING: This will overwrite any conflicting saves in the new location!</source>
- <translation type="unfinished"/>
+ <translation>変更前と変更後の両方の保存場所にセーブデータが存在します。
+
+変更前: %1
+変更後: %2
+
+変更前の保存場所からセーブデータを移行しますか?
+警告: 変更後の保存場所にある競合するセーブデータはすべて上書きされます!</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="216"/>
@@ -3057,17 +3127,20 @@ WARNING: This will overwrite any conflicting saves in the new location!</source>
From: %1
To: %2</source>
- <translation type="unfinished"/>
+ <translation>セーブデータを新しい保存場所に移行しますか?
+
+移行元: %1
+移行先: %2</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="224"/>
<source>Migrate Save Data</source>
- <translation type="unfinished"/>
+ <translation>セーブデータを移行</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="231"/>
<source>Migrating save data...</source>
- <translation type="unfinished"/>
+ <translation>セーブデータを移行しています...</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="231"/>
@@ -3078,30 +3151,33 @@ To: %2</source>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="239"/>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="252"/>
<source>Migration Failed</source>
- <translation type="unfinished"/>
+ <translation>移行に失敗しました</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="240"/>
<source>Failed to create destination directory.</source>
- <translation type="unfinished"/>
+ <translation>保存先ディレクトリを作成できませんでした。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="253"/>
<source>Failed to migrate save data:
%1</source>
- <translation type="unfinished"/>
+ <translation>セーブデータの移行に失敗しました:
+%1</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="258"/>
<source>Migration Complete</source>
- <translation type="unfinished"/>
+ <translation>移行完了</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_filesystem.cpp" line="259"/>
<source>Save data has been migrated successfully.
Would you like to delete the old save data?</source>
- <translation type="unfinished"/>
+ <translation>セーブデータの移行が完了しました。
+
+古いセーブデータを削除しますか?</translation>
</message>
</context>
<context>
@@ -3120,22 +3196,22 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_general.ui" line="52"/>
<source>External Content</source>
- <translation type="unfinished"/>
+ <translation>外部コンテンツ</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_general.ui" line="58"/>
<source>Add directories to scan for DLCs and Updates without installing to NAND</source>
- <translation type="unfinished"/>
+ <translation>NANDにインストールせずにDLCやアップデートを読み込むディレクトリを追加します</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_general.ui" line="77"/>
<source>Add Directory</source>
- <translation type="unfinished"/>
+ <translation>ディレクトリを追加</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_general.ui" line="84"/>
<source>Remove Selected</source>
- <translation type="unfinished"/>
+ <translation>選択項目を削除</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_general.ui" line="142"/>
@@ -3150,22 +3226,22 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_general.cpp" line="102"/>
<source>This reset all settings and remove all per-game configurations. This will not delete game directories, profiles, or input profiles. Proceed?</source>
- <translation>すべての設定がリセットされ、ゲームごとの設定もすべて削除されます。ゲームディレクトリ、プロファイル、入力プロファイルは削除されません。続行しますか?</translation>
+ <translation>すべての設定をリセットし、ゲームごとの個別設定もすべて削除します。ゲームディレクトリ、ユーザープロファイル、入力プロファイルは削除されません。続行しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_general.cpp" line="140"/>
<source>Select External Content Directory...</source>
- <translation type="unfinished"/>
+ <translation>外部コンテンツのディレクトリを選択...</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_general.cpp" line="153"/>
<source>Directory Already Added</source>
- <translation type="unfinished"/>
+ <translation>ディレクトリは追加済みです</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_general.cpp" line="154"/>
<source>This directory is already in the list.</source>
- <translation type="unfinished"/>
+ <translation>このディレクトリはすでにリストに登録されています。</translation>
</message>
</context>
<context>
@@ -3178,7 +3254,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics.ui" line="17"/>
<source>Graphics</source>
- <translation>グラフィック</translation>
+ <translation>グラフィックス</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics.ui" line="25"/>
@@ -3188,23 +3264,23 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics.ui" line="58"/>
<source>Graphics Settings</source>
- <translation>グラフィック設定</translation>
+ <translation>グラフィックス設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics.ui" line="103"/>
<source>Background Color:</source>
- <translation>背景色:</translation>
+ <translation>背景色:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics.cpp" line="234"/>
<source>%</source>
<comment>FSR/SGSR sharpening percentage (e.g. 50%)</comment>
- <translation type="unfinished"/>
+ <translation>%</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics.cpp" line="310"/>
<source>Configure Effects...</source>
- <translation type="unfinished"/>
+ <translation>エフェクトを設定...</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics.cpp" line="388"/>
@@ -3214,7 +3290,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics.cpp" line="389"/>
<source>VSync Off</source>
- <translation>VSync オフ</translation>
+ <translation>VSyncオフ</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics.cpp" line="391"/>
@@ -3229,7 +3305,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics.cpp" line="397"/>
<source>VSync On</source>
- <translation>VSync オン</translation>
+ <translation>VSyncオン</translation>
</message>
</context>
<context>
@@ -3242,12 +3318,12 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_advanced.ui" line="17"/>
<source>Advanced</source>
- <translation>高度な設定</translation>
+ <translation>詳細設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_advanced.ui" line="23"/>
<source>Advanced Graphics Settings</source>
- <translation>グラフィックの高度な設定</translation>
+ <translation>グラフィックスの詳細設定</translation>
</message>
</context>
<context>
@@ -3255,27 +3331,27 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.ui" line="14"/>
<source>Form</source>
- <translation type="unfinished"/>
+ <translation>フォーム</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.ui" line="17"/>
<source>Extras</source>
- <translation type="unfinished"/>
+ <translation>追加機能</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.ui" line="23"/>
<source>Hacks</source>
- <translation type="unfinished"/>
+ <translation>ハック</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.ui" line="29"/>
<source>Changing these options from their default may cause issues. Novitii cavete!</source>
- <translation type="unfinished"/>
+ <translation>これらの設定をデフォルトから変更すると、不具合が発生する場合があります。変更には十分ご注意ください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.ui" line="62"/>
<source>Vulkan Extensions</source>
- <translation>GPU拡張機能</translation>
+ <translation>Vulkan拡張機能</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.cpp" line="49"/>
@@ -3286,7 +3362,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_graphics_extensions.cpp" line="68"/>
<source>Extended Dynamic State is disabled on macOS due to MoltenVK compatibility issues that cause black screens.</source>
- <translation type="unfinished"/>
+ <translation>macOSでは、MoltenVKとの互換性問題により黒画面になるため、拡張ダイナミックステートは無効化されています。</translation>
</message>
</context>
<context>
@@ -3304,12 +3380,12 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_hotkeys.ui" line="25"/>
<source>Double-click on a binding to change it.</source>
- <translation>項目をダブルクリックで変更します。</translation>
+ <translation>項目をダブルクリックすると割り当てを変更できます。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_hotkeys.ui" line="45"/>
<source>Clear All</source>
- <translation>全て消去</translation>
+ <translation>すべてクリア</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_hotkeys.ui" line="52"/>
@@ -3329,20 +3405,20 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_hotkeys.cpp" line="115"/>
<source>Controller Hotkey</source>
- <translation>コントローラー ホットキー</translation>
+ <translation>コントローラーのホットキー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_hotkeys.cpp" line="155"/>
<location filename="../../src/yuzu/configuration/configure_hotkeys.cpp" line="181"/>
<location filename="../../src/yuzu/configuration/configure_hotkeys.cpp" line="421"/>
<source>Conflicting Key Sequence</source>
- <translation>入力された組合せの衝突</translation>
+ <translation>キーの組み合わせが競合しています</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_hotkeys.cpp" line="156"/>
<location filename="../../src/yuzu/configuration/configure_hotkeys.cpp" line="182"/>
<source>The entered key sequence is already assigned to: %1</source>
- <translation>入力された組合せは既に次の操作に割り当てられています:%1</translation>
+ <translation>入力されたキーの組み合わせは、すでに次の操作に割り当てられています: %1</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_hotkeys.cpp" line="194"/>
@@ -3362,7 +3438,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_hotkeys.cpp" line="344"/>
<source>An error occurred. Please report this issue on github.</source>
- <translation>エラーが発生しました。この問題をgithubで報告してください。</translation>
+ <translation>エラーが発生しました。この問題をGitHub等で報告してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_hotkeys.cpp" line="384"/>
@@ -3372,22 +3448,22 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_hotkeys.cpp" line="385"/>
<source>Clear</source>
- <translation>消去</translation>
+ <translation>クリア</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_hotkeys.cpp" line="406"/>
<source>Conflicting Button Sequence</source>
- <translation>ボタンが競合しています</translation>
+ <translation>ボタンの組み合わせが競合しています</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_hotkeys.cpp" line="407"/>
<source>The default button sequence is already assigned to: %1</source>
- <translation>デフォルトのボタン配列はすでに %1 に割り当てられています。</translation>
+ <translation>デフォルトのボタンの組み合わせは、すでに次の操作に割り当てられています: %1</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_hotkeys.cpp" line="422"/>
<source>The default key sequence is already assigned to: %1</source>
- <translation>デフォルトの組合せはすでに %1 に割り当てられています。</translation>
+ <translation>デフォルトのキーの組み合わせは、すでに次の操作に割り当てられています: %1</translation>
</message>
</context>
<context>
@@ -3449,7 +3525,7 @@ Would you like to delete the old save data?</source>
<location filename="../../src/yuzu/configuration/configure_input.ui" line="103"/>
<location filename="../../src/yuzu/configuration/configure_input.ui" line="106"/>
<source>Advanced</source>
- <translation>高度な設定</translation>
+ <translation>詳細設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input.ui" line="138"/>
@@ -3459,7 +3535,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_input.ui" line="159"/>
<source>Docked</source>
- <translation>接続</translation>
+ <translation>ドックモード</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input.ui" line="169"/>
@@ -3530,7 +3606,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_input.ui" line="435"/>
<source>Connected</source>
- <translation>接続の状態</translation>
+ <translation>接続済み</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input.ui" line="494"/>
@@ -3540,7 +3616,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_input.ui" line="537"/>
<source>Clear</source>
- <translation>消去</translation>
+ <translation>クリア</translation>
</message>
</context>
<context>
@@ -3570,7 +3646,7 @@ Would you like to delete the old save data?</source>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="1955"/>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2241"/>
<source>L Body</source>
- <translation>L本体</translation>
+ <translation>L 本体</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="219"/>
@@ -3582,7 +3658,7 @@ Would you like to delete the old save data?</source>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2010"/>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2296"/>
<source>L Button</source>
- <translation>Lボタン</translation>
+ <translation>L ボタン</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="295"/>
@@ -3594,7 +3670,7 @@ Would you like to delete the old save data?</source>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2086"/>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2372"/>
<source>R Body</source>
- <translation>R本体</translation>
+ <translation>R 本体</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="350"/>
@@ -3606,7 +3682,7 @@ Would you like to delete the old save data?</source>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2141"/>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2427"/>
<source>R Button</source>
- <translation>Rボタン</translation>
+ <translation>R ボタン</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="411"/>
@@ -3646,7 +3722,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2533"/>
<source>Emulated Devices</source>
- <translation>エミュレートされたデバイス</translation>
+ <translation>エミュレートするデバイス</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2545"/>
@@ -3666,7 +3742,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2588"/>
<source>Advanced</source>
- <translation>高度な設定</translation>
+ <translation>詳細設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2595"/>
@@ -3706,42 +3782,42 @@ Would you like to delete the old save data?</source>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2701"/>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2717"/>
<source>Requires restarting Eden</source>
- <translation type="unfinished"/>
+ <translation>Edenの再起動が必要です</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2668"/>
<source>Enable XInput 8 player support (disables web applet)</source>
- <translation>XInput 8プレイヤーサポートの有効化 (webアプレットの無効化)</translation>
+ <translation>XInputの8人プレイ対応を有効にする (Webアプレットは無効になります)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2681"/>
<source>Enable UDP controllers (not needed for motion)</source>
- <translation type="unfinished"/>
+ <translation>UDPコントローラーを有効にする (モーション操作のみの場合は不要)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2694"/>
<source>Controller navigation</source>
- <translation type="unfinished"/>
+ <translation>コントローラーでの操作</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2710"/>
<source>Enable direct JoyCon driver</source>
- <translation type="unfinished"/>
+ <translation>Joy-Conダイレクトドライバーを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2726"/>
<source>Enable direct Pro Controller driver [EXPERIMENTAL]</source>
- <translation type="unfinished"/>
+ <translation>Proコントローラーのダイレクトドライバーを有効にする [実験的]</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2733"/>
<source>Allows unlimited uses of the same Amiibo in games that would otherwise limit you to one use.</source>
- <translation>同じAmiiboをゲーム内で無制限に使用できるようにします。</translation>
+ <translation>通常は1日1回などに制限されているゲームでも、同じamiiboを無制限に使用できるようにします。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2742"/>
<source>Use random Amiibo ID</source>
- <translation>ランダムな Amiibo ID を使用</translation>
+ <translation>ランダムなamiibo IDを使用</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2749"/>
@@ -3751,12 +3827,12 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2763"/>
<source>Aimed to disable SDL GUIDE button hack: synthetic GUIDE(HOME) event when SELECT(MINUS) + START(PLUS) pressed. May impact Win related trigger/rumble/etc stuff</source>
- <translation type="unfinished"/>
+ <translation>SELECT (−) とSTART (+) の同時押しで擬似的にGUIDE (HOME) イベントを発生させるSDLのハックを無効にします。Windows環境でのトリガーや振動機能などに影響する場合があります。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_advanced.ui" line="2772"/>
<source>Disable SDL WGI/XInput (Requires restart)</source>
- <translation type="unfinished"/>
+ <translation>SDLのWGI/XInputを無効にする (再起動が必要)</translation>
</message>
</context>
<context>
@@ -3769,7 +3845,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_input_per_game.ui" line="17"/>
<source>Graphics</source>
- <translation>グラフィック</translation>
+ <translation>グラフィックス</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_per_game.ui" line="28"/>
@@ -3824,7 +3900,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_input_per_game.cpp" line="47"/>
<source>Player %1 profile</source>
- <translation>プレイヤー%1 プロファイル</translation>
+ <translation>プレイヤー%1のプロファイル</translation>
</message>
</context>
<context>
@@ -3837,7 +3913,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="63"/>
<source>Connect Controller</source>
- <translation>有効</translation>
+ <translation>コントローラーを接続</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="100"/>
@@ -3918,7 +3994,7 @@ Would you like to delete the old save data?</source>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="2601"/>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="2640"/>
<source>Pressed</source>
- <translation>押下</translation>
+ <translation>押し込み</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="573"/>
@@ -3926,7 +4002,7 @@ Would you like to delete the old save data?</source>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="2650"/>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="2689"/>
<source>Modifier</source>
- <translation>変更</translation>
+ <translation>修飾</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="622"/>
@@ -3944,13 +4020,13 @@ Would you like to delete the old save data?</source>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="698"/>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="2772"/>
<source>Deadzone: 0%</source>
- <translation>遊び:0%</translation>
+ <translation>デッドゾーン: 0%</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="722"/>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="2796"/>
<source>Modifier Range: 0%</source>
- <translation>変更範囲:0%</translation>
+ <translation>修飾範囲: 0%</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="755"/>
@@ -3991,7 +4067,7 @@ Would you like to delete the old save data?</source>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="1369"/>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="1408"/>
<source>Minus</source>
- <translation>-</translation>
+ <translation>−</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="1418"/>
@@ -4075,7 +4151,7 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="2851"/>
<source>Mouse panning</source>
- <translation>マウス パンニング</translation>
+ <translation>マウスパンニング</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.ui" line="2875"/>
@@ -4110,12 +4186,12 @@ Would you like to delete the old save data?</source>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="398"/>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="670"/>
<source>Toggle button</source>
- <translation type="unfinished"/>
+ <translation>ボタンをトグル</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="404"/>
<source>Turbo button</source>
- <translation>ターボボタン</translation>
+ <translation>連射ボタン</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="412"/>
@@ -4134,12 +4210,12 @@ Would you like to delete the old save data?</source>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="428"/>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="481"/>
<source>Choose a value between 0% and 100%</source>
- <translation>0%から100%の間の値を選択してください</translation>
+ <translation>0%〜100%の間の値を選択してください</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="440"/>
<source>Toggle axis</source>
- <translation type="unfinished"/>
+ <translation>軸をトグル</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="477"/>
@@ -4154,31 +4230,31 @@ Would you like to delete the old save data?</source>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="526"/>
<source>Map Analog Stick</source>
- <translation>アナログスティックをマップ</translation>
+ <translation>アナログスティックを割り当て</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="527"/>
<source>After pressing OK, first move your joystick horizontally, and then vertically.
To invert the axes, first move your joystick vertically, and then horizontally.</source>
- <translation>OKを押した後、スティックを水平方向に動かし、次に垂直方向に動かしてください。
-軸を反転させる場合、 最初に垂直方向に動かし、次に水平方向に動かしてください。</translation>
+ <translation>「OK」を押した後、最初にスティックを左右 (水平方向) に動かし、次に上下 (垂直方向) に動かしてください。
+軸を反転させる場合は、先に上下、次に左右の順で動かしてください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="595"/>
<source>Center axis</source>
- <translation type="unfinished"/>
+ <translation>軸を中央に設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="703"/>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1068"/>
<source>Deadzone: %1%</source>
- <translation>遊び:%1%</translation>
+ <translation>デッドゾーン: %1%</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="712"/>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1073"/>
<source>Modifier Range: %1%</source>
- <translation>変更範囲:%1%</translation>
+ <translation>修飾範囲: %1%</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="753"/>
@@ -4189,47 +4265,47 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1102"/>
<source>Dual Joycons</source>
- <translation>Joy-Con(L/R)</translation>
+ <translation>Joy-Con (L/R)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1106"/>
<source>Left Joycon</source>
- <translation>Joy-Con(L)</translation>
+ <translation>Joy-Con (L)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1110"/>
<source>Right Joycon</source>
- <translation>Joy-Con(R)</translation>
+ <translation>Joy-Con (R)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1114"/>
<source>Handheld</source>
- <translation>携帯コントローラー</translation>
+ <translation>携帯モード</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1118"/>
<source>GameCube Controller</source>
- <translation>ゲームキューブコントローラー</translation>
+ <translation>ゲームキューブ コントローラー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1127"/>
<source>Poke Ball Plus</source>
- <translation>モンスターボールプラス</translation>
+ <translation>モンスターボール Plus</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1131"/>
<source>NES Controller</source>
- <translation>ファミコン・コントローラー</translation>
+ <translation>ファミコン コントローラー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1135"/>
<source>SNES Controller</source>
- <translation>スーパーファミコン・コントローラー</translation>
+ <translation>スーパーファミコン コントローラー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1139"/>
<source>N64 Controller</source>
- <translation>ニンテンドウ64・コントローラー</translation>
+ <translation>NINTENDO 64 コントローラー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1143"/>
@@ -4239,7 +4315,7 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1373"/>
<source>Start / Pause</source>
- <translation>スタート/ ポーズ</translation>
+ <translation>スタート / ポーズ</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1376"/>
@@ -4249,7 +4325,7 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1377"/>
<source>Control Stick</source>
- <translation type="unfinished"/>
+ <translation>コントロールスティック</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1378"/>
@@ -4259,12 +4335,12 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1479"/>
<source>Shake!</source>
- <translation>振ってください</translation>
+ <translation>振る</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1481"/>
<source>[waiting]</source>
- <translation>[待機中]</translation>
+ <translation>[入力待ち]</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1578"/>
@@ -4285,12 +4361,12 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1587"/>
<source>The given profile name is not valid!</source>
- <translation>プロファイル名が無効です!</translation>
+ <translation>指定されたプロファイル名は無効です。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1595"/>
<source>Failed to create the input profile &quot;%1&quot;</source>
- <translation>入力プロファイル &quot;%1&quot; の作成に失敗しました</translation>
+ <translation>入力プロファイル「%1」の作成に失敗しました</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1615"/>
@@ -4300,27 +4376,27 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1616"/>
<source>Failed to delete the input profile &quot;%1&quot;</source>
- <translation>入力プロファイル &quot;%1&quot; の削除に失敗しました</translation>
+ <translation>入力プロファイル「%1」の削除に失敗しました</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1638"/>
<source>Load Input Profile</source>
- <translation>入力プロファイルをロード</translation>
+ <translation>入力プロファイルを読み込む</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1639"/>
<source>Failed to load the input profile &quot;%1&quot;</source>
- <translation>入力プロファイル &quot;%1&quot; のロードに失敗しました</translation>
+ <translation>入力プロファイル「%1」の読み込みに失敗しました</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1664"/>
<source>Save Input Profile</source>
- <translation>入力プロファイルをセーブ</translation>
+ <translation>入力プロファイルを保存</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="1665"/>
<source>Failed to save the input profile &quot;%1&quot;</source>
- <translation>入力プロファイル &quot;%1&quot; のセーブに失敗しました</translation>
+ <translation>入力プロファイル「%1」の保存に失敗しました</translation>
</message>
</context>
<context>
@@ -4346,12 +4422,12 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.ui" line="6"/>
<source>Configure Motion / Touch</source>
- <translation>モーション / タッチ設定</translation>
+ <translation>モーション / タッチの設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.ui" line="15"/>
<source>Touch</source>
- <translation>タッチの設定</translation>
+ <translation>タッチ</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.ui" line="23"/>
@@ -4373,7 +4449,7 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.ui" line="57"/>
<source>Touch from button profile:</source>
- <translation type="unfinished"/>
+ <translation>ボタンプロファイルからのタッチ:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.ui" line="85"/>
@@ -4383,7 +4459,7 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.ui" line="91"/>
<source>You may use any Cemuhook compatible UDP input source to provide motion and touch input.</source>
- <translation>モーションとタッチの入力元として、Cemuhook互換のUDP入力ソースを使用します。</translation>
+ <translation>モーションやタッチの入力に、Cemuhook互換のUDP入力ソースを使用できます。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.ui" line="134"/>
@@ -4434,7 +4510,7 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.cpp" line="170"/>
<source>Port has to be in range 0 and 65353</source>
- <translation>ポート番号は0から65353の間で設定してください</translation>
+ <translation>ポート番号は0〜65353の範囲で指定してください</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.cpp" line="174"/>
@@ -4444,12 +4520,12 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.cpp" line="180"/>
<source>This UDP server already exists</source>
- <translation>このUDPサーバーはすでに存在してます</translation>
+ <translation>このUDPサーバーはすでに存在します</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.cpp" line="186"/>
<source>Unable to add more than 8 servers</source>
- <translation>8個以上のサーバーを追加することはできません</translation>
+ <translation>サーバーは8個までしか追加できません</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.cpp" line="202"/>
@@ -4484,7 +4560,7 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_motion_touch.cpp" line="281"/>
<source>UDP Test or calibration configuration is in progress.&lt;br&gt;Please wait for them to finish.</source>
- <translation>UDPテストまたはキャリブレーション実行中です。&lt;br&gt;完了までお待ちください。</translation>
+ <translation>UDPのテストまたはキャリブレーションを実行中です。&lt;br&gt;完了するまでお待ちください。</translation>
</message>
</context>
<context>
@@ -4492,17 +4568,17 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_mouse_panning.ui" line="6"/>
<source>Configure mouse panning</source>
- <translation>マウス パンニングを設定</translation>
+ <translation>マウスパンニングの設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_mouse_panning.ui" line="12"/>
<source>Enable mouse panning</source>
- <translation>マウス パンニングを有効化</translation>
+ <translation>マウスパンニングを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_mouse_panning.ui" line="15"/>
<source>Can be toggled via a hotkey. Default hotkey is Ctrl + F9</source>
- <translation>ホットキーで切り替えできます。 デフォルトのホットキーは Ctrl + F9 です</translation>
+ <translation>ホットキーで切り替えられます。デフォルトのホットキーは Ctrl + F9 です。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_mouse_panning.ui" line="24"/>
@@ -4531,17 +4607,17 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<message>
<location filename="../../src/yuzu/configuration/configure_mouse_panning.ui" line="85"/>
<source>Deadzone counterweight</source>
- <translation type="unfinished"/>
+ <translation>デッドゾーン補正</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_mouse_panning.ui" line="88"/>
<source>Counteracts a game&apos;s built-in deadzone</source>
- <translation type="unfinished"/>
+ <translation>ゲーム側のデッドゾーンを打ち消します</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_mouse_panning.ui" line="94"/>
<source>Deadzone</source>
- <translation>遊び</translation>
+ <translation>デッドゾーン</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_mouse_panning.ui" line="123"/>
@@ -4567,22 +4643,23 @@ To invert the axes, first move your joystick vertically, and then horizontally.<
<location filename="../../src/yuzu/configuration/configure_mouse_panning.cpp" line="43"/>
<source>Mouse panning works better with a deadzone of 0% and a range of 100%.
Current values are %1% and %2% respectively.</source>
- <translation>マウス パンニングは遊びが 0% から 100% の設定で良好に動作します. 現在の設定値は %1% から %2% です.</translation>
+ <translation>マウスパンニングは、デッドゾーン 0%、範囲 100% の設定で最適に動作します。
+現在の設定値は、デッドゾーン %1%、範囲 %2% です。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_mouse_panning.cpp" line="50"/>
<source>Emulated mouse is enabled. This is incompatible with mouse panning.</source>
- <translation type="unfinished"/>
+ <translation>エミュレートされたマウスが有効です。マウスパンニングとは併用できません。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_mouse_panning.cpp" line="83"/>
<source>Emulated mouse is enabled</source>
- <translation type="unfinished"/>
+ <translation>エミュレートされたマウスが有効です</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_mouse_panning.cpp" line="84"/>
<source>Real mouse input and mouse panning are incompatible. Please disable the emulated mouse in input advanced settings to allow mouse panning.</source>
- <translation type="unfinished"/>
+ <translation>実マウス入力とマウスパンニングは併用できません。マウスパンニングを使用するには、入力の詳細設定でエミュレートされたマウスを無効にしてください。</translation>
</message>
</context>
<context>
@@ -4605,12 +4682,12 @@ Current values are %1% and %2% respectively.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_network.ui" line="34"/>
<source>Network Interface</source>
- <translation>ネットワークインタフェース</translation>
+ <translation>ネットワークインターフェース</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_network.ui" line="41"/>
<source>Enable Airplane Mode</source>
- <translation type="unfinished"/>
+ <translation>機内モードを有効にする</translation>
</message>
</context>
<context>
@@ -4628,7 +4705,7 @@ Current values are %1% and %2% respectively.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.ui" line="93"/>
<source>Name</source>
- <translation>タイトル</translation>
+ <translation>名前</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.ui" line="100"/>
@@ -4663,7 +4740,7 @@ Current values are %1% and %2% respectively.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.ui" line="240"/>
<source>Some settings are only available when a game is not running.</source>
- <translation>いくつかの設定はゲームが実行中でないときのみ設定できます</translation>
+ <translation>一部の設定は、ゲームを実行していないときのみ変更できます。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.cpp" line="78"/>
@@ -4683,17 +4760,17 @@ Current values are %1% and %2% respectively.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.cpp" line="81"/>
<source>Graphics</source>
- <translation>グラフィック</translation>
+ <translation>グラフィックス</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.cpp" line="82"/>
<source>Adv. Graphics</source>
- <translation>高度なグラフィック</translation>
+ <translation>グラフィックス (詳細)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.cpp" line="83"/>
<source>Ext. Graphics</source>
- <translation type="unfinished"/>
+ <translation>グラフィックス (追加)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.cpp" line="84"/>
@@ -4713,7 +4790,7 @@ Current values are %1% and %2% respectively.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.cpp" line="87"/>
<source>Applets</source>
- <translation type="unfinished"/>
+ <translation>アプレット</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game.cpp" line="90"/>
@@ -4736,17 +4813,17 @@ Current values are %1% and %2% respectively.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.ui" line="23"/>
<source>Import Mod from ZIP</source>
- <translation type="unfinished"/>
+ <translation>ZIPからMODをインポート</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.ui" line="30"/>
<source>Import Mod from Folder</source>
- <translation type="unfinished"/>
+ <translation>フォルダーからMODをインポート</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="56"/>
<source>Patch Name</source>
- <translation>名称</translation>
+ <translation>パッチ名</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="57"/>
@@ -4756,87 +4833,92 @@ Current values are %1% and %2% respectively.</source>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="153"/>
<source>Mod Install Succeeded</source>
- <translation type="unfinished"/>
+ <translation>MODのインストールに成功しました</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="154"/>
<source>Successfully installed all mods.</source>
- <translation type="unfinished"/>
+ <translation>すべてのMODをインストールしました。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="163"/>
<source>Mod Install Failed</source>
- <translation type="unfinished"/>
+ <translation>MODのインストールに失敗しました</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="164"/>
<source>Failed to install the following mods:
%1
Check the log for details.</source>
- <translation type="unfinished"/>
+ <translation>次のMODのインストールに失敗しました:
+ %1
+詳細はログを確認してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="183"/>
<source>Mod Folder</source>
- <translation type="unfinished"/>
+ <translation>MODフォルダー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="194"/>
<source>Zipped Mod Location</source>
- <translation type="unfinished"/>
+ <translation>ZIP形式のMODの場所</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="196"/>
<source>Zipped Archives (*.zip)</source>
- <translation type="unfinished"/>
+ <translation>ZIPアーカイブ (*.zip)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="214"/>
<source>Invalid Selection</source>
- <translation type="unfinished"/>
+ <translation>無効な選択</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="215"/>
<source>Only mods, cheats, and patches can be deleted.
To delete NAND-installed updates, right-click the game in the game list and click Remove -&gt; Remove Installed Update.</source>
- <translation type="unfinished"/>
+ <translation>削除できるのはMOD、チート、パッチのみです。
+NANDにインストールされたアップデートを削除するには、ゲームリストでゲームを右クリックし、[削除] -&gt; [インストール済みアップデートを削除] を選択してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="221"/>
<source>You are about to delete the following installed mods:
</source>
- <translation type="unfinished"/>
+ <translation>次のインストール済みMODを削除しようとしています:
+</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="227"/>
<source>
Once deleted, these can NOT be recovered. Are you 100% sure you want to delete them?</source>
- <translation type="unfinished"/>
+ <translation>
+削除すると元に戻せません。本当に削除しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="232"/>
<source>Delete add-on(s)?</source>
- <translation type="unfinished"/>
+ <translation>アドオンを削除しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="243"/>
<source>Successfully deleted</source>
- <translation type="unfinished"/>
+ <translation>削除しました</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="244"/>
<source>Successfully deleted all selected mods.</source>
- <translation type="unfinished"/>
+ <translation>選択したMODをすべて削除しました。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="267"/>
<source>&amp;Delete</source>
- <translation type="unfinished"/>
+ <translation>削除(&amp;D)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_per_game_addons.cpp" line="274"/>
<source>&amp;Open in File Manager</source>
- <translation type="unfinished"/>
+ <translation>ファイルマネージャーで開く(&amp;O)</translation>
</message>
</context>
<context>
@@ -4844,123 +4926,124 @@ Once deleted, these can NOT be recovered. Are you 100% sure you want to delete t
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="67"/>
<source>Post-Processing Effects</source>
- <translation type="unfinished"/>
+ <translation>ポストプロセスエフェクト</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="74"/>
<source>Changes apply immediately while a game is running.</source>
- <translation type="unfinished"/>
+ <translation>ゲーム実行中の変更は即座に反映されます。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="78"/>
<source>Enable post-processing</source>
- <translation type="unfinished"/>
+ <translation>ポストプロセスを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="86"/>
<source>Preset:</source>
- <translation type="unfinished"/>
+ <translation>プリセット:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="91"/>
<source>Apply</source>
- <translation type="unfinished"/>
+ <translation>適用</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="102"/>
<source>Reset Values</source>
- <translation type="unfinished"/>
+ <translation>値をリセット</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="103"/>
<source>Return every effect to the values the preset ships with.</source>
- <translation type="unfinished"/>
+ <translation>すべてのエフェクトをプリセットの初期値に戻します。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="114"/>
<source>Save As...</source>
- <translation type="unfinished"/>
+ <translation>名前を付けて保存...</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="118"/>
<source>Save Preset</source>
- <translation type="unfinished"/>
+ <translation>プリセットを保存</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="118"/>
<source>Preset name:</source>
- <translation type="unfinished"/>
+ <translation>プリセット名:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="129"/>
<source>Clear</source>
- <translation type="unfinished"/>
+ <translation>クリア</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="130"/>
<source>Remove every effect in use and deselect the preset.</source>
- <translation type="unfinished"/>
+ <translation>使用中のエフェクトをすべて削除し、プリセットの選択を解除します。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="155"/>
<source>Add Effect</source>
- <translation type="unfinished"/>
+ <translation>エフェクトを追加</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="202"/>
<source>None</source>
- <translation type="unfinished"/>
+ <translation>なし</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="231"/>
<source>No effects in use. Pick a preset and press Apply, or add effects one by one.</source>
- <translation type="unfinished"/>
+ <translation>使用中のエフェクトはありません。プリセットを選択して [適用] を押すか、エフェクトを個別にプロファイルへ追加してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="235"/>
<source>Custom chain. Pick a preset above and press Apply to replace it.</source>
- <translation type="unfinished"/>
+ <translation>カスタムチェーンです。置き換えるには、上でプリセットを選んで [適用] を押してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="241"/>
<source>Active preset: %1 (modified). Press Reset Values to go back to how it ships.</source>
- <translation type="unfinished"/>
+ <translation>使用中のプリセット: %1 (変更あり)。[値をリセット] を押すと初期状態に戻ります。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="246"/>
<source>Active preset: %1</source>
- <translation type="unfinished"/>
+ <translation>使用中のプリセット: %1</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="307"/>
<source>Enabled</source>
- <translation type="unfinished"/>
+ <translation>有効</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="308"/>
<source>Disabled</source>
- <translation type="unfinished"/>
+ <translation>無効</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="420"/>
<source>Reset to defaults</source>
- <translation type="unfinished"/>
+ <translation>デフォルトに戻す</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="455"/>
<source>Effect &apos;%1&apos; was not found.</source>
- <translation type="unfinished"/>
+ <translation>エフェクト「%1」が見つかりませんでした。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="464"/>
<source>Effect failed to compile:
%1</source>
- <translation type="unfinished"/>
+ <translation>エフェクトのコンパイルに失敗しました:
+%1</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_post_processing.cpp" line="505"/>
<source>No usable ReShade FX effects were found. Place .fx files in the post_shaders folder.</source>
- <translation type="unfinished"/>
+ <translation>使用可能なReShade FXエフェクトが見つかりませんでした。.fxファイルを post_shaders フォルダーに配置してください。</translation>
</message>
</context>
<context>
@@ -4978,12 +5061,12 @@ Once deleted, these can NOT be recovered. Are you 100% sure you want to delete t
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.ui" line="25"/>
<source>Profile Manager</source>
- <translation>プロファイルマネージャ</translation>
+ <translation>プロファイルマネージャー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.ui" line="42"/>
<source>Current User</source>
- <translation>アクティブなユーザー</translation>
+ <translation>現在のユーザー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.ui" line="86"/>
@@ -4998,7 +5081,7 @@ Once deleted, these can NOT be recovered. Are you 100% sure you want to delete t
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.ui" line="138"/>
<source>Profile management is available only when game is not running.</source>
- <translation>プロファイル管理はゲーム未実行時にのみ行えます。</translation>
+ <translation>プロファイル管理は、ゲームを実行していないときのみ利用できます。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="58"/>
@@ -5011,7 +5094,7 @@ Once deleted, these can NOT be recovered. Are you 100% sure you want to delete t
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="139"/>
<source>Users</source>
- <translation>ユーザ</translation>
+ <translation>ユーザー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="194"/>
@@ -5021,7 +5104,7 @@ Once deleted, these can NOT be recovered. Are you 100% sure you want to delete t
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="195"/>
<source>Error occurred attempting to overwrite previous image at: %1.</source>
- <translation>既存画像の上書き時にエラーが発生しました: %1 </translation>
+ <translation>既存の画像を上書き中にエラーが発生しました: %1</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="203"/>
@@ -5036,37 +5119,37 @@ Once deleted, these can NOT be recovered. Are you 100% sure you want to delete t
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="211"/>
<source>Error creating user image directory</source>
- <translation>ユーザー画像ディレクトリ作成失敗</translation>
+ <translation>ユーザー画像ディレクトリの作成エラー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="212"/>
<source>Unable to create directory %1 for storing user images.</source>
- <translation>ユーザー画像保存ディレクトリ”%1”を作成できませんでした。</translation>
+ <translation>ユーザー画像を保存するディレクトリ「%1」を作成できませんでした。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="217"/>
<source>Error saving user image</source>
- <translation type="unfinished"/>
+ <translation>ユーザー画像の保存エラー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="218"/>
<source>Unable to save image to file</source>
- <translation type="unfinished"/>
+ <translation>画像をファイルに保存できませんでした</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="230"/>
<source>&amp;Edit</source>
- <translation type="unfinished"/>
+ <translation>編集(&amp;E)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="231"/>
<source>&amp;Delete</source>
- <translation type="unfinished"/>
+ <translation>削除(&amp;D)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="291"/>
<source>Edit User</source>
- <translation type="unfinished"/>
+ <translation>ユーザーを編集</translation>
</message>
</context>
<context>
@@ -5074,18 +5157,18 @@ Once deleted, these can NOT be recovered. Are you 100% sure you want to delete t
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="359"/>
<source>Delete this user? All of the user&apos;s save data will be deleted.</source>
- <translation>このユーザを削除しますか? このユーザのすべてのセーブデータが削除されます.</translation>
+ <translation>このユーザーを削除しますか?このユーザーのセーブデータはすべて削除されます。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="374"/>
<source>Confirm Delete</source>
- <translation>ユーザの削除</translation>
+ <translation>削除の確認</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_profile_manager.cpp" line="397"/>
<source>Name: %1
UUID: %2</source>
- <translation>名称: %1
+ <translation>名前: %1
UUID: %2</translation>
</message>
</context>
@@ -5099,12 +5182,12 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.ui" line="26"/>
<source>To use Ring-Con, configure player 1 as right Joy-Con (both physical and emulated), and player 2 as left Joy-Con (left physical and dual emulated) before starting the game.</source>
- <translation type="unfinished"/>
+ <translation>リングコンを使用するには、ゲームを起動する前に、プレイヤー1を「Joy-Con (R)」(実機・エミュレートの両方)、プレイヤー2を「Joy-Con (L)」(実機は左、エミュレートはL/R) に設定してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.ui" line="52"/>
<source>Virtual Ring Sensor Parameters</source>
- <translation>仮想リングセンサー パラメータ</translation>
+ <translation>仮想リングセンサーのパラメーター</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.ui" line="84"/>
@@ -5121,17 +5204,17 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.ui" line="206"/>
<source>Deadzone: 0%</source>
- <translation>遊び:0%</translation>
+ <translation>デッドゾーン: 0%</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.ui" line="233"/>
<source>Direct Joycon Driver</source>
- <translation type="unfinished"/>
+ <translation>Joy-Conダイレクトドライバー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.ui" line="293"/>
<source>Enable Ring Input</source>
- <translation>リングコン入力を有効化</translation>
+ <translation>リングコン入力を有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.ui" line="300"/>
@@ -5142,13 +5225,13 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.ui" line="307"/>
<source>Ring Sensor Value</source>
- <translation>リングコン センサー値</translation>
+ <translation>リングコンのセンサー値</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.ui" line="314"/>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="285"/>
<source>Not connected</source>
- <translation>接続なし</translation>
+ <translation>未接続</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.ui" line="344"/>
@@ -5174,7 +5257,7 @@ UUID: %2</translation>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="194"/>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="260"/>
<source>Deadzone: %1%</source>
- <translation>遊び:%1%</translation>
+ <translation>デッドゾーン: %1%</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="282"/>
@@ -5184,7 +5267,7 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="288"/>
<source>Direct Joycon driver is not enabled</source>
- <translation type="unfinished"/>
+ <translation>Joy-Conダイレクトドライバーが有効になっていません</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="293"/>
@@ -5194,22 +5277,22 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="305"/>
<source>The current mapped device doesn&apos;t support the ring controller</source>
- <translation type="unfinished"/>
+ <translation>現在マッピングされているデバイスはリングコンに対応していません</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="309"/>
<source>The current mapped device doesn&apos;t have a ring attached</source>
- <translation type="unfinished"/>
+ <translation>現在マッピングされているデバイスにリングコンが接続されていません</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="312"/>
<source>The current mapped device is not connected</source>
- <translation type="unfinished"/>
+ <translation>現在マッピングされているデバイスは接続されていません</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="316"/>
<source>Unexpected driver result %1</source>
- <translation type="unfinished"/>
+ <translation>予期しないドライバーの結果: %1</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="339"/>
@@ -5238,7 +5321,7 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_system.cpp" line="71"/>
<source>Warning: &quot;%1&quot; is not a valid language for region &quot;%2&quot;</source>
- <translation type="unfinished"/>
+ <translation>警告:「%1」はリージョン「%2」で有効な言語ではありません</translation>
</message>
</context>
<context>
@@ -5251,17 +5334,17 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.ui" line="17"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Reads controller input from scripts in the same format as TAS-nx scripts.&lt;br/&gt;For a more detailed explanation, please consult the user handbook.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
- <translation type="unfinished"/>
+ <translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;TAS-nx形式のスクリプトからコントローラー入力を読み込みます。&lt;br/&gt;詳細については、ユーザーハンドブックをご覧ください。&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.ui" line="27"/>
<source>To check which hotkeys control the playback/recording, please refer to the Hotkey settings (Configure -&gt; General -&gt; Hotkeys).</source>
- <translation>どのホットキーで再生/録音を制御するかは、ホットキーの設定(設定>一般>ホットキー)を参照してください。</translation>
+ <translation>再生や記録を操作するホットキーについては、ホットキー設定 (設定 &gt; 全般 &gt; ホットキー) を参照してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.ui" line="37"/>
<source>WARNING: This is an experimental feature.&lt;br/&gt;It will not play back scripts frame perfectly with the current, imperfect syncing method.</source>
- <translation>警告:実験的な機能です。&lt;br/&gt;現状では完全な再生や同期はできません。</translation>
+ <translation>警告: これは実験的な機能です。&lt;br/&gt;現在の不完全な同期方式では、スクリプトをフレーム単位で正確に再生することはできません。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.ui" line="54"/>
@@ -5271,7 +5354,7 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.ui" line="60"/>
<source>Enable TAS features</source>
- <translation>TAS機能の有効化</translation>
+ <translation>TAS機能を有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.ui" line="67"/>
@@ -5286,7 +5369,7 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.ui" line="84"/>
<source>Show recording dialog</source>
- <translation type="unfinished"/>
+ <translation>記録ダイアログを表示</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.ui" line="98"/>
@@ -5309,12 +5392,12 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.cpp" line="22"/>
<source>TAS Configuration</source>
- <translation>TAS 設定</translation>
+ <translation>TAS設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_tas.cpp" line="55"/>
<source>Select TAS Load Directory...</source>
- <translation>TAS ロードディレクトリを選択...</translation>
+ <translation>TAS読み込みディレクトリを選択...</translation>
</message>
</context>
<context>
@@ -5327,7 +5410,7 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_touch_from_button.ui" line="22"/>
<source>Mapping:</source>
- <translation>マッピング:</translation>
+ <translation>マッピング:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touch_from_button.ui" line="48"/>
@@ -5342,19 +5425,19 @@ UUID: %2</translation>
<message>
<location filename="../../src/yuzu/configuration/configure_touch_from_button.ui" line="74"/>
<source>Rename</source>
- <translation>リネーム</translation>
+ <translation>名前を変更</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touch_from_button.ui" line="92"/>
<source>Click the bottom area to add a point, then press a button to bind.
Drag points to change position, or double-click table cells to edit values.</source>
- <translation>下部エリア内をクリックしてタッチポイントを追加し、対象デバイスのボタンを押すことで割り当てができます。
-ポイントの位置変更はポイントをドラッグするか、表から選択して値を直接編集で可能です。</translation>
+ <translation>下部のエリアをクリックしてポイントを追加し、ボタンを押して割り当てます。
+ポイントをドラッグして位置を変更するか、表のセルをダブルクリックして値を編集できます。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touch_from_button.ui" line="116"/>
<source>Delete Point</source>
- <translation>削除</translation>
+ <translation>ポイントを削除</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touch_from_button.cpp" line="78"/>
@@ -5381,32 +5464,32 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/configuration/configure_touch_from_button.cpp" line="195"/>
<source>Enter the name for the new profile.</source>
- <translation>プロファイル名を入力</translation>
+ <translation>新しいプロファイルの名前を入力してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touch_from_button.cpp" line="206"/>
<source>Delete Profile</source>
- <translation>プロファイルの削除</translation>
+ <translation>プロファイルを削除</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touch_from_button.cpp" line="206"/>
<source>Delete profile %1?</source>
- <translation>プロファイル%1を削除しますか?</translation>
+ <translation>プロファイル「%1」を削除しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touch_from_button.cpp" line="219"/>
<source>Rename Profile</source>
- <translation>プロファイルのリネーム</translation>
+ <translation>プロファイル名を変更</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touch_from_button.cpp" line="219"/>
<source>New name:</source>
- <translation>新しいプロファイル名:</translation>
+ <translation>新しい名前:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touch_from_button.cpp" line="231"/>
<source>[press key]</source>
- <translation>[キーを押す]</translation>
+ <translation>[キーを入力]</translation>
</message>
</context>
<context>
@@ -5419,22 +5502,22 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/configuration/configure_touchscreen_advanced.ui" line="26"/>
<source>Warning: The settings in this page affect the inner workings of Eden&apos;s emulated touchscreen. Changing them may result in undesirable behavior, such as the touchscreen partially or not working. You should only use this page if you know what you are doing.</source>
- <translation type="unfinished"/>
+ <translation>警告: このページの設定は、Edenがエミュレートするタッチスクリーンの内部動作に影響します。変更すると、タッチスクリーンが一部または完全に反応しなくなるなど、予期しない動作を引き起こす可能性があります。内容を十分に理解している場合のみ変更してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touchscreen_advanced.ui" line="52"/>
<source>Touch Parameters</source>
- <translation>タッチパラメータ</translation>
+ <translation>タッチパラメーター</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touchscreen_advanced.ui" line="71"/>
<source>Touch Diameter Y</source>
- <translation>タッチ直径Y</translation>
+ <translation>タッチ直径 Y</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touchscreen_advanced.ui" line="91"/>
<source>Touch Diameter X</source>
- <translation>タッチ直径X</translation>
+ <translation>タッチ直径 X</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touchscreen_advanced.ui" line="98"/>
@@ -5511,17 +5594,17 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="31"/>
<source>Note: Changing language will apply your configuration.</source>
- <translation>注意:UIの言語設定は変更後すぐに適用されます。</translation>
+ <translation>注意: 言語を変更すると、現在の設定が適用されます。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="43"/>
<source>Interface language:</source>
- <translation>UIの言語:</translation>
+ <translation>インターフェース言語:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="57"/>
<source>Theme:</source>
- <translation>テーマ:</translation>
+ <translation>テーマ:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="74"/>
@@ -5531,37 +5614,37 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="82"/>
<source>Show Add-Ons Column</source>
- <translation>アドオンを表示</translation>
+ <translation>アドオン列を表示</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="89"/>
<source>Show Size Column</source>
- <translation>サイズを表示</translation>
+ <translation>サイズ列を表示</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="96"/>
<source>Show File Types Column</source>
- <translation>ファイルタイプを表示</translation>
+ <translation>ファイル種別列を表示</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="103"/>
<source>Show Play Time Column</source>
- <translation>プレイ時間を表示</translation>
+ <translation>プレイ時間列を表示</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="112"/>
<source>Folder Icon Size:</source>
- <translation>フォルダアイコンサイズ:</translation>
+ <translation>フォルダーアイコンのサイズ:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="126"/>
<source>Row 1 Text:</source>
- <translation>1行目の表示内容:</translation>
+ <translation>1行目のテキスト:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="140"/>
<source>Row 2 Text:</source>
- <translation>2行目の表示内容:</translation>
+ <translation>2行目のテキスト:</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="157"/>
@@ -5571,12 +5654,12 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="165"/>
<source>Ask Where To Save Screenshots (Windows Only)</source>
- <translation>スクリーンショット時に保存先を確認する(Windowsのみ)</translation>
+ <translation>スクリーンショットの保存先を毎回確認する (Windowsのみ)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="174"/>
<source>Screenshots Path: </source>
- <translation>スクリーンショットの保存先:</translation>
+ <translation>スクリーンショットの保存先: </translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="184"/>
@@ -5586,7 +5669,7 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="200"/>
<source>TextLabel</source>
- <translation type="unfinished"/>
+ <translation>テキストラベル</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.ui" line="219"/>
@@ -5601,7 +5684,7 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/configuration/configure_ui.cpp" line="227"/>
<source>&lt;System&gt;</source>
- <translation>&lt;System&gt;</translation>
+ <translation>&lt;システム&gt;</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ui.cpp" line="228"/>
@@ -5612,7 +5695,7 @@ Drag points to change position, or double-click table cells to edit values.</sou
<location filename="../../src/yuzu/configuration/configure_ui.cpp" line="311"/>
<source>Auto (%1 x %2, %3 x %4)</source>
<comment>Screenshot width value</comment>
- <translation type="unfinished"/>
+ <translation>自動 (%1 x %2, %3 x %4)</translation>
</message>
</context>
<context>
@@ -5620,7 +5703,7 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/configuration/configure_vibration.ui" line="14"/>
<source>Configure Vibration</source>
- <translation>振動設定</translation>
+ <translation>振動の設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_vibration.ui" line="23"/>
@@ -5692,7 +5775,7 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/configuration/configure_vibration.ui" line="509"/>
<source>Enable Accurate Vibration</source>
- <translation>正確な振動の有効化</translation>
+ <translation>高精度な振動を有効にする</translation>
</message>
</context>
<context>
@@ -5710,32 +5793,32 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/configuration/configure_web.ui" line="25"/>
<source>Eden Web Service</source>
- <translation type="unfinished"/>
+ <translation>Eden Webサービス</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_web.ui" line="33"/>
<source>Token: </source>
- <translation>トークン:</translation>
+ <translation>トークン: </translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_web.ui" line="46"/>
<source>Username: </source>
- <translation>ユーザー名:</translation>
+ <translation>ユーザー名: </translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_web.ui" line="105"/>
<source>Generate</source>
- <translation type="unfinished"/>
+ <translation>生成</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_web.ui" line="130"/>
<source>Web Service configuration can only be changed when a public room isn&apos;t being hosted.</source>
- <translation>Webサービスの設定は、公開ルームがホストされていない時にのみ変更することができます。</translation>
+ <translation>Webサービスの設定は、公開ルームをホストしていないときのみ変更できます。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_web.ui" line="142"/>
<source>Discord Presence</source>
- <translation>Discord 連携</translation>
+ <translation>Discord連携</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_web.ui" line="148"/>
@@ -5747,19 +5830,19 @@ Drag points to change position, or double-click table cells to edit values.</sou
<location filename="../../src/yuzu/configuration/configure_web.cpp" line="118"/>
<source>All Good</source>
<comment>Tooltip</comment>
- <translation type="unfinished"/>
+ <translation>問題ありません</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_web.cpp" line="113"/>
<source>Must be between 4-20 characters</source>
<comment>Tooltip</comment>
- <translation type="unfinished"/>
+ <translation>4〜20文字で入力してください</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_web.cpp" line="122"/>
<source>Must be 48 characters, and lowercase a-z</source>
<comment>Tooltip</comment>
- <translation type="unfinished"/>
+ <translation>半角英小文字 (a〜z) の48文字で入力してください</translation>
</message>
</context>
<context>
@@ -5767,12 +5850,12 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/debugger/controller.cpp" line="23"/>
<source>Controller P1</source>
- <translation>Controller P1</translation>
+ <translation>コントローラー P1</translation>
</message>
<message>
<location filename="../../src/yuzu/debugger/controller.cpp" line="60"/>
<source>&amp;Controller P1</source>
- <translation>&amp;Controller P1</translation>
+ <translation>コントローラー P1(&amp;C)</translation>
</message>
</context>
<context>
@@ -5780,37 +5863,37 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/data_dialog.ui" line="26"/>
<source>Data Manager</source>
- <translation type="unfinished"/>
+ <translation>データマネージャー</translation>
</message>
<message>
<location filename="../../src/yuzu/data_dialog.ui" line="48"/>
<source>Deleting ANY data is IRREVERSABLE!</source>
- <translation type="unfinished"/>
+ <translation>データの削除は、いかなる場合も元に戻せません!</translation>
</message>
<message>
<location filename="../../src/yuzu/data_dialog.cpp" line="31"/>
<source>Shaders</source>
- <translation type="unfinished"/>
+ <translation>シェーダー</translation>
</message>
<message>
<location filename="../../src/yuzu/data_dialog.cpp" line="32"/>
<source>User NAND</source>
- <translation type="unfinished"/>
+ <translation>ユーザーNAND</translation>
</message>
<message>
<location filename="../../src/yuzu/data_dialog.cpp" line="33"/>
<source>System NAND</source>
- <translation type="unfinished"/>
+ <translation>システムNAND</translation>
</message>
<message>
<location filename="../../src/yuzu/data_dialog.cpp" line="34"/>
<source>Mods</source>
- <translation type="unfinished"/>
+ <translation>MOD</translation>
</message>
<message>
<location filename="../../src/yuzu/data_dialog.cpp" line="35"/>
<source>Saves</source>
- <translation type="unfinished"/>
+ <translation>セーブデータ</translation>
</message>
</context>
<context>
@@ -5818,37 +5901,37 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/data_widget.ui" line="14"/>
<source>Form</source>
- <translation type="unfinished"/>
+ <translation>フォーム</translation>
</message>
<message>
<location filename="../../src/yuzu/data_widget.ui" line="22"/>
<source>Tooltip</source>
- <translation type="unfinished"/>
+ <translation>ツールチップ</translation>
</message>
<message>
<location filename="../../src/yuzu/data_widget.ui" line="70"/>
<source>Open with your system file manager</source>
- <translation type="unfinished"/>
+ <translation>システムのファイルマネージャーで開く</translation>
</message>
<message>
<location filename="../../src/yuzu/data_widget.ui" line="106"/>
<source>Delete all data in this directory. THIS IS 100% IRREVERSABLE!</source>
- <translation type="unfinished"/>
+ <translation>このディレクトリ内のすべてのデータを削除します。この操作は完全に元に戻せません!</translation>
</message>
<message>
<location filename="../../src/yuzu/data_widget.ui" line="142"/>
<source>Export all data in this directory. This may take a while!</source>
- <translation type="unfinished"/>
+ <translation>このディレクトリ内のすべてのデータをエクスポートします。時間がかかる場合があります。</translation>
</message>
<message>
<location filename="../../src/yuzu/data_widget.ui" line="178"/>
<source>Import data for this directory. This may take a while, and will delete ALL EXISTING DATA!</source>
- <translation type="unfinished"/>
+ <translation>このディレクトリのデータをインポートします。時間がかかる場合があり、既存のデータはすべて削除されます!</translation>
</message>
<message>
<location filename="../../src/yuzu/data_dialog.cpp" line="95"/>
<source>Calculating...</source>
- <translation type="unfinished"/>
+ <translation>計算中...</translation>
</message>
</context>
<context>
@@ -5856,22 +5939,22 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/deps_dialog.ui" line="14"/>
<source>Eden Dependencies</source>
- <translation type="unfinished"/>
+ <translation>Edenの依存関係</translation>
</message>
<message>
<location filename="../../src/yuzu/deps_dialog.ui" line="72"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:28pt;&quot;&gt;Eden Dependencies&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
- <translation type="unfinished"/>
+ <translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-size:28pt;&quot;&gt;Edenの依存関係&lt;/span&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../../src/yuzu/deps_dialog.ui" line="85"/>
<source>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;The projects that make Eden possible&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</source>
- <translation type="unfinished"/>
+ <translation>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Edenを支えるプロジェクト&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</translation>
</message>
<message>
<location filename="../../src/yuzu/deps_dialog.cpp" line="22"/>
<source>Dependency</source>
- <translation type="unfinished"/>
+ <translation>依存関係</translation>
</message>
<message>
<location filename="../../src/yuzu/deps_dialog.cpp" line="22"/>
@@ -5940,12 +6023,12 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="11"/>
<source>Username is not valid. Must be 4 to 20 alphanumeric characters.</source>
- <translation>ユーザー名が無効です。4~20文字の英数字で入力してください。</translation>
+ <translation>ユーザー名が無効です。4〜20文字の半角英数字で入力してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="13"/>
<source>Room name is not valid. Must be 4 to 20 alphanumeric characters.</source>
- <translation>ルーム名が無効です。4~20文字の英数字で入力してください。</translation>
+ <translation>ルーム名が無効です。4〜20文字の半角英数字で入力してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="15"/>
@@ -5960,62 +6043,62 @@ Drag points to change position, or double-click table cells to edit values.</sou
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="19"/>
<source>Port must be a number between 0 to 65535.</source>
- <translation>0〜65535の間を入力してください</translation>
+ <translation>ポート番号は0〜65535の数値で入力してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="20"/>
<source>You must choose a Preferred Game to host a room. If you do not have any games in your game list yet, add a game folder by clicking on the plus icon in the game list.</source>
- <translation type="unfinished"/>
+ <translation>ルームをホストするには「優先ゲーム」を選択する必要があります。ゲームリストにゲームがない場合は、プラスアイコンをクリックしてゲームフォルダーを追加してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="24"/>
<source>Unable to find an internet connection. Check your internet settings.</source>
- <translation type="unfinished"/>
+ <translation>インターネット接続が見つかりません。インターネット設定を確認してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="26"/>
<source>Unable to connect to the host. Verify that the connection settings are correct. If you still cannot connect, contact the room host and verify that the host is properly configured with the external port forwarded.</source>
- <translation type="unfinished"/>
+ <translation>ホストに接続できません。接続設定が正しいか確認してください。解決しない場合は、ルームのホストに連絡し、外部ポート開放 (ポートフォワーディング) が正しく設定されているか確認してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="30"/>
<source>Unable to connect to the room because it is already full.</source>
- <translation type="unfinished"/>
+ <translation>ルームが満室のため接続できません。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="32"/>
<source>Creating a room failed. Please retry. Restarting Eden might be necessary.</source>
- <translation type="unfinished"/>
+ <translation>ルームの作成に失敗しました。もう一度お試しください。Edenの再起動が必要な場合があります。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="34"/>
<source>The host of the room has banned you. Speak with the host to unban you or try a different room.</source>
- <translation type="unfinished"/>
+ <translation>ルームのホストによってBANされています。BANの解除についてはホストに相談するか、別のルームをお試しください。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="37"/>
<source>Version mismatch! Please update to the latest version of Eden. If the problem persists, contact the room host and ask them to update the server.</source>
- <translation type="unfinished"/>
+ <translation>バージョンが一致しません!Edenを最新バージョンに更新してください。問題が解決しない場合は、ルームのホストに連絡し、サーバーを更新するよう依頼してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="39"/>
<source>Incorrect password.</source>
- <translation type="unfinished"/>
+ <translation>パスワードが正しくありません。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="40"/>
<source>An unknown error occurred. If this error continues to occur, please open an issue</source>
- <translation type="unfinished"/>
+ <translation>不明なエラーが発生しました。このエラーが繰り返し発生する場合は、Issueを作成してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="43"/>
<source>Connection to room lost. Try to reconnect.</source>
- <translation type="unfinished"/>
+ <translation>ルームとの接続が切断されました。再接続をお試しください。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="45"/>
<source>You have been kicked by the room host.</source>
- <translation type="unfinished"/>
+ <translation>ルームのホストによりキックされました。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="47"/>
@@ -6031,13 +6114,15 @@ Drag points to change position, or double-click table cells to edit values.</sou
<location filename="../../src/yuzu/multiplayer/message.cpp" line="50"/>
<source>The user you are trying to kick/ban could not be found.
They may have left the room.</source>
- <translation type="unfinished"/>
+ <translation>キック/BANしようとしたユーザーが見つかりませんでした。
+すでにルームから退出した可能性があります。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="52"/>
<source>No valid network interface is selected.
Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
- <translation type="unfinished"/>
+ <translation>有効なネットワークインターフェースが選択されていません。
+[設定] &gt; [システム] &gt; [ネットワーク] から選択してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="63"/>
@@ -6051,24 +6136,24 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<location filename="../../src/yuzu/bootmanager.cpp" line="845"/>
<location filename="../../src/yuzu/bootmanager.cpp" line="862"/>
<source>OpenGL not available!</source>
- <translation>OpenGLは使用できません!</translation>
+ <translation>OpenGLを利用できません</translation>
</message>
<message>
<location filename="../../src/yuzu/bootmanager.cpp" line="846"/>
<source>OpenGL shared contexts are not supported.</source>
- <translation type="unfinished"/>
+ <translation>OpenGLの共有コンテキストはサポートされていません。</translation>
</message>
<message>
<location filename="../../src/yuzu/bootmanager.cpp" line="863"/>
<source>Eden has not been compiled with OpenGL support.</source>
- <translation type="unfinished"/>
+ <translation>EdenはOpenGLサポートを有効にしてコンパイルされていません。</translation>
</message>
<message>
<location filename="../../src/yuzu/bootmanager.cpp" line="887"/>
<location filename="../../src/yuzu/bootmanager.cpp" line="905"/>
<location filename="../../src/yuzu/bootmanager.cpp" line="915"/>
<source>Error while initializing OpenGL!</source>
- <translation>OpenGL初期化エラー</translation>
+ <translation>OpenGLの初期化エラー</translation>
</message>
<message>
<location filename="../../src/yuzu/bootmanager.cpp" line="888"/>
@@ -6078,22 +6163,22 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/yuzu/bootmanager.cpp" line="897"/>
<source>Error while initializing OpenGL 4.6!</source>
- <translation>OpenGL4.6初期化エラー!</translation>
+ <translation>OpenGL 4.6の初期化エラー</translation>
</message>
<message>
<location filename="../../src/yuzu/bootmanager.cpp" line="898"/>
<source>Your GPU may not support OpenGL 4.6, or you do not have the latest graphics driver.&lt;br&gt;&lt;br&gt;GL Renderer:&lt;br&gt;%1</source>
- <translation>GPUがOpenGL4.6をサポートしていないか、グラフィックスドライバーが最新ではありません。&lt;br&gt;&lt;br&gt;GL レンダラ:&lt;br&gt;%1</translation>
+ <translation>GPUがOpenGL 4.6をサポートしていないか、グラフィックスドライバーが最新ではありません。&lt;br&gt;&lt;br&gt;GLレンダラー:&lt;br&gt;%1</translation>
</message>
<message>
<location filename="../../src/yuzu/bootmanager.cpp" line="906"/>
<source>Your GPU may not support one or more required OpenGL extensions. Please ensure you have the latest graphics driver.&lt;br&gt;&lt;br&gt;GL Renderer:&lt;br&gt;%1&lt;br&gt;&lt;br&gt;Unsupported extensions:&lt;br&gt;%2</source>
- <translation>GPUが1つ以上の必要なOpenGL拡張機能をサポートしていない可能性があります。最新のグラフィックドライバを使用していることを確認してください。&lt;br&gt;&lt;br&gt;GL レンダラ:&lt;br&gt;%1&lt;br&gt;&lt;br&gt;サポートされていない拡張機能:&lt;br&gt;%2</translation>
+ <translation>GPUが必要なOpenGL拡張機能を1つ以上サポートしていない可能性があります。最新のグラフィックスドライバーがインストールされていることを確認してください。&lt;br&gt;&lt;br&gt;GLレンダラー:&lt;br&gt;%1&lt;br&gt;&lt;br&gt;サポートされていない拡張機能:&lt;br&gt;%2</translation>
</message>
<message>
<location filename="../../src/yuzu/bootmanager.cpp" line="916"/>
<source>This build doesn&apos;t have OpenGL support.</source>
- <translation type="unfinished"/>
+ <translation>このビルドはOpenGLをサポートしていません。</translation>
</message>
</context>
<context>
@@ -6101,7 +6186,7 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="417"/>
<source>&amp;Add New Game Directory</source>
- <translation type="unfinished"/>
+ <translation>新しいゲームディレクトリを追加(&amp;A)</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="451"/>
@@ -6121,22 +6206,22 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="457"/>
<source>Open Save Data Location</source>
- <translation>セーブデータディレクトリを開く</translation>
+ <translation>セーブデータの場所を開く</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="458"/>
<source>Open Mod Data Location</source>
- <translation>Modデータディレクトリを開く</translation>
+ <translation>MODデータの場所を開く</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="460"/>
<source>Open Transferable Pipeline Cache</source>
- <translation>パイプラインキャッシュを開く</translation>
+ <translation>転送可能なパイプラインキャッシュを開く</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="461"/>
<source>Link to Ryujinx</source>
- <translation type="unfinished"/>
+ <translation>Ryujinxにリンク</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="463"/>
@@ -6146,12 +6231,12 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="464"/>
<source>Remove Installed Update</source>
- <translation>インストールされているアップデートを削除</translation>
+ <translation>インストール済みのアップデートを削除</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="465"/>
<source>Remove All Installed DLC</source>
- <translation>全てのインストールされているDLCを削除</translation>
+ <translation>インストール済みのDLCをすべて削除</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="466"/>
@@ -6181,22 +6266,22 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="472"/>
<source>Remove All Installed Contents</source>
- <translation>全てのインストールされているコンテンツを削除</translation>
+ <translation>インストール済みのコンテンツをすべて削除</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="473"/>
<source>Manage Play Time</source>
- <translation type="unfinished"/>
+ <translation>プレイ時間を管理</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="474"/>
<source>Edit Play Time Data</source>
- <translation type="unfinished"/>
+ <translation>プレイ時間データを編集</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="475"/>
<source>Remove Play Time Data</source>
- <translation>プレイ時間情報を削除</translation>
+ <translation>プレイ時間データを削除</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="476"/>
@@ -6237,17 +6322,17 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="488"/>
<source>Configure Game</source>
- <translation type="unfinished"/>
+ <translation>ゲームを設定</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="581"/>
<source>Scan Subfolders</source>
- <translation>サブフォルダをスキャンする</translation>
+ <translation>サブフォルダーをスキャン</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="582"/>
<source>Remove Game Directory</source>
- <translation>ゲームディレクトリを削除する</translation>
+ <translation>ゲームディレクトリを削除</translation>
</message>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="601"/>
@@ -6275,42 +6360,42 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="194"/>
<source>Ingame</source>
- <translation type="unfinished"/>
+ <translation>ゲーム内</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="194"/>
<source>Game starts, but crashes or major glitches prevent it from being completed.</source>
- <translation>ゲームは始まるが、クラッシュや大きな不具合でクリアできない。</translation>
+ <translation>ゲームは起動しますが、クラッシュや重大な不具合により最後までプレイできません。</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="196"/>
<source>Perfect</source>
- <translation>カンペキ</translation>
+ <translation>完璧</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="196"/>
<source>Game can be played without issues.</source>
- <translation>ゲームは問題なくプレイできる。</translation>
+ <translation>問題なくプレイできます。</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="197"/>
<source>Playable</source>
- <translation>プレイ可</translation>
+ <translation>プレイ可能</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="197"/>
<source>Game functions with minor graphical or audio glitches and is playable from start to finish.</source>
- <translation>ゲームは、グラフィックやオーディオに小さな不具合はあるが、最初から最後までプレイできる。</translation>
+ <translation>グラフィックスやオーディオに軽微な不具合はありますが、最初から最後までプレイできます。</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="200"/>
<source>Intro/Menu</source>
- <translation>イントロ</translation>
+ <translation>イントロ/メニュー</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="200"/>
<source>Game loads, but is unable to progress past the Start Screen.</source>
- <translation>ゲームはロードされるが、スタート画面から先に進めない。</translation>
+ <translation>ゲームは読み込まれますが、スタート画面から先に進めません。</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="201"/>
@@ -6320,7 +6405,7 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="201"/>
<source>The game crashes when attempting to startup.</source>
- <translation>ゲームは起動時にクラッシュしました。</translation>
+ <translation>ゲームの起動時にクラッシュします。</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="202"/>
@@ -6338,27 +6423,27 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/qt_common/game_list/model.cpp" line="194"/>
<source>Name</source>
- <translation type="unfinished"/>
+ <translation>名前</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/model.cpp" line="195"/>
<source>Add-ons</source>
- <translation type="unfinished"/>
+ <translation>アドオン</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/model.cpp" line="196"/>
<source>File type</source>
- <translation type="unfinished"/>
+ <translation>ファイル形式</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/model.cpp" line="197"/>
<source>Size</source>
- <translation type="unfinished"/>
+ <translation>サイズ</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/model.cpp" line="198"/>
<source>Play time</source>
- <translation type="unfinished"/>
+ <translation>プレイ時間</translation>
</message>
</context>
<context>
@@ -6366,7 +6451,7 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/yuzu/game/game_list.cpp" line="785"/>
<source>Double-click to add a new folder to the game list</source>
- <translation>新しいゲームリストフォルダを追加するにはダブルクリックしてください。</translation>
+ <translation>ダブルクリックしてゲームリストに新しいフォルダーを追加します</translation>
</message>
</context>
<context>
@@ -6374,17 +6459,17 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message numerus="yes">
<location filename="../../src/yuzu/game/search_field.cpp" line="67"/>
<source>%1 of %n result(s)</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>%n 件中 %1 件</numerusform></translation>
</message>
<message>
<location filename="../../src/yuzu/game/search_field.cpp" line="122"/>
<source>Filter:</source>
- <translation>フィルター:</translation>
+ <translation>フィルター:</translation>
</message>
<message>
<location filename="../../src/yuzu/game/search_field.cpp" line="123"/>
<source>Enter pattern to filter</source>
- <translation>フィルターパターンを入力</translation>
+ <translation>検索ワードを入力して絞り込み</translation>
</message>
</context>
<context>
@@ -6392,7 +6477,7 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/yuzu/multiplayer/host_room.ui" line="14"/>
<source>Create Room</source>
- <translation>ルーム作成</translation>
+ <translation>ルームを作成</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/host_room.ui" line="37"/>
@@ -6417,7 +6502,7 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/yuzu/multiplayer/host_room.ui" line="101"/>
<source>(Leave blank for open game)</source>
- <translation type="unfinished"/>
+ <translation>(空欄にすると誰でも参加できます)</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/host_room.ui" line="118"/>
@@ -6432,12 +6517,12 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/yuzu/multiplayer/host_room.ui" line="139"/>
<source>Room Description</source>
- <translation>ルーム説明</translation>
+ <translation>ルームの説明</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/host_room.ui" line="153"/>
<source>Load Previous Ban List</source>
- <translation>以前の BAN リストをロード</translation>
+ <translation>以前のBANリストを読み込む</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/host_room.ui" line="184"/>
@@ -6447,12 +6532,12 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<message>
<location filename="../../src/yuzu/multiplayer/host_room.ui" line="189"/>
<source>Unlisted</source>
- <translation>非公開</translation>
+ <translation>非掲載</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/host_room.ui" line="197"/>
<source>Host Room</source>
- <translation>ホストルーム</translation>
+ <translation>ルームをホスト</translation>
</message>
</context>
<context>
@@ -6466,7 +6551,8 @@ Please go to Configure -&gt; System -&gt; Network and make a selection.</source>
<location filename="../../src/yuzu/multiplayer/host_room.cpp" line="187"/>
<source>Failed to announce the room to the public lobby. In order to host a room publicly, you must have a valid Eden account configured in Emulation -&gt; Configure -&gt; Web. If you do not want to publish a room in the public lobby, then select Unlisted instead.
Debug Message: </source>
- <translation type="unfinished"/>
+ <translation>公開ロビーにルームを掲載できませんでした。公開ルームをホストするには、[エミュレーション] &gt; [設定] &gt; [Web] で有効なEdenアカウントを設定する必要があります。公開ロビーに掲載しない場合は「非掲載」を選択してください。
+デバッグメッセージ: </translation>
</message>
</context>
<context>
@@ -6474,7 +6560,7 @@ Debug Message: </source>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="34"/>
<source>Audio Mute/Unmute</source>
- <translation>音声ミュート/解除</translation>
+ <translation>音声のミュート/解除</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="34"/>
@@ -6511,7 +6597,7 @@ Debug Message: </source>
<location filename="../../src/qt_common/config/uisettings.cpp" line="65"/>
<location filename="../../src/qt_common/config/uisettings.cpp" line="66"/>
<source>Main Window</source>
- <translation>メイン画面</translation>
+ <translation>メインウィンドウ</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="35"/>
@@ -6531,27 +6617,27 @@ Debug Message: </source>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="38"/>
<source>Change Adapting Filter</source>
- <translation>適応フィルターの変更</translation>
+ <translation>適応フィルターを変更</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="39"/>
<source>Change Docked Mode</source>
- <translation>ドックモードを変更</translation>
+ <translation>ドックモードを切り替え</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="40"/>
<source>Change GPU Mode</source>
- <translation type="unfinished"/>
+ <translation>GPUモードを切り替え</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="41"/>
<source>Configure</source>
- <translation type="unfinished"/>
+ <translation>設定</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="42"/>
<source>Configure Current Game</source>
- <translation type="unfinished"/>
+ <translation>現在のゲームを設定</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="43"/>
@@ -6561,117 +6647,117 @@ Debug Message: </source>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="44"/>
<source>Exit Fullscreen</source>
- <translation>フルスクリーンをやめる</translation>
+ <translation>全画面表示を終了</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="45"/>
<source>Exit Eden</source>
- <translation type="unfinished"/>
+ <translation>Edenを終了</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="46"/>
<source>Fullscreen</source>
- <translation>フルスクリーン</translation>
+ <translation>全画面表示</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="47"/>
<source>Load File</source>
- <translation>ファイルのロード</translation>
+ <translation>ファイルを読み込む</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="48"/>
<source>Load/Remove Amiibo</source>
- <translation>読み込み/解除 Amiibo</translation>
+ <translation>amiiboの読み込み/解除</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="49"/>
<source>Browse Public Game Lobby</source>
- <translation type="unfinished"/>
+ <translation>公開ゲームロビーを参照</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="50"/>
<source>Create Room</source>
- <translation type="unfinished"/>
+ <translation>ルームを作成</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="51"/>
<source>Direct Connect to Room</source>
- <translation type="unfinished"/>
+ <translation>ルームに直接接続</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="52"/>
<source>Leave Room</source>
- <translation type="unfinished"/>
+ <translation>ルームから退出</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="53"/>
<source>Show Current Room</source>
- <translation type="unfinished"/>
+ <translation>現在のルームを表示</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="54"/>
<source>Restart Emulation</source>
- <translation>エミュレーションをリスタート</translation>
+ <translation>エミュレーションを再起動</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="55"/>
<source>Stop Emulation</source>
- <translation>エミュレーションをやめる</translation>
+ <translation>エミュレーションを停止</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="56"/>
<source>TAS Record</source>
- <translation>TAS 記録</translation>
+ <translation>TASの記録</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="57"/>
<source>TAS Reset</source>
- <translation>TAS リセット</translation>
+ <translation>TASのリセット</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="58"/>
<source>TAS Start/Stop</source>
- <translation>TAS 開始/停止</translation>
+ <translation>TASの開始/停止</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="59"/>
<source>Toggle Filter Bar</source>
- <translation>フィルターバー切り替え</translation>
+ <translation>フィルターバーの表示切り替え</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="60"/>
<source>Toggle Framerate Limit</source>
- <translation>フレームレート制限切り替え</translation>
+ <translation>フレームレート制限の切り替え</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="61"/>
<source>Toggle Turbo Speed</source>
- <translation type="unfinished"/>
+ <translation>ターボ速度の切り替え</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="62"/>
<source>Toggle Slow Speed</source>
- <translation type="unfinished"/>
+ <translation>スロー速度の切り替え</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="63"/>
<source>Toggle Mouse Panning</source>
- <translation type="unfinished"/>
+ <translation>マウスパンニングの切り替え</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="64"/>
<source>Toggle Renderdoc Capture</source>
- <translation type="unfinished"/>
+ <translation>RenderDocキャプチャの切り替え</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="65"/>
<source>Toggle Status Bar</source>
- <translation>ステータスバー切り替え</translation>
+ <translation>ステータスバーの表示切り替え</translation>
</message>
<message>
<location filename="../../src/qt_common/config/uisettings.cpp" line="66"/>
<source>Toggle Performance Overlay</source>
- <translation type="unfinished"/>
+ <translation>パフォーマンスオーバーレイの表示切り替え</translation>
</message>
</context>
<context>
@@ -6679,12 +6765,12 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/install_dialog.cpp" line="32"/>
<source>Please confirm these are the files you wish to install.</source>
- <translation>これらがインストールするファイルであることを確認してください。</translation>
+ <translation>インストールするファイルが正しいことを確認してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/install_dialog.cpp" line="35"/>
<source>Installing an Update or DLC will overwrite the previously installed one.</source>
- <translation>アップデート、またはDLCをインストールすると、以前にインストールしたものが上書きされます。</translation>
+ <translation>アップデートまたはDLCをインストールすると、以前インストールしたものは上書きされます。</translation>
</message>
<message>
<location filename="../../src/yuzu/install_dialog.cpp" line="39"/>
@@ -6694,7 +6780,7 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/install_dialog.cpp" line="52"/>
<source>Install Files to NAND</source>
- <translation>ファイルをNANDへインストール</translation>
+ <translation>ファイルをNANDにインストール</translation>
</message>
</context>
<context>
@@ -6703,7 +6789,7 @@ Debug Message: </source>
<location filename="../../src/yuzu/util/limitable_input_dialog.cpp" line="60"/>
<source>The text can't contain any of the following characters:
%1</source>
- <translation>テキストに以下の文字を含めることはできません:
+ <translation>テキストには次の文字を含めることはできません:
%1</translation>
</message>
</context>
@@ -6712,27 +6798,27 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/loading_screen.ui" line="84"/>
<source>Loading Shaders 387 / 1628</source>
- <translation>シェーダーをロード中 387 / 1628</translation>
+ <translation>シェーダーを読み込み中 387 / 1628</translation>
</message>
<message>
<location filename="../../src/yuzu/loading_screen.ui" line="121"/>
<source>Loading Shaders %v out of %m</source>
- <translation>シェーダーをロード中 %v / %m</translation>
+ <translation>シェーダーを読み込み中 %v / %m</translation>
</message>
<message>
<location filename="../../src/yuzu/loading_screen.ui" line="135"/>
<source>Estimated Time 5m 4s</source>
- <translation>予想時間 5分4秒</translation>
+ <translation>推定残り時間 5分4秒</translation>
</message>
<message>
<location filename="../../src/yuzu/loading_screen.cpp" line="86"/>
<source>Loading...</source>
- <translation>ロード中...</translation>
+ <translation>読み込み中...</translation>
</message>
<message>
<location filename="../../src/yuzu/loading_screen.cpp" line="87"/>
<source>Loading Shaders %1 / %2</source>
- <translation>シェーダーをロード中 %1 / %2</translation>
+ <translation>シェーダーを読み込み中 %1 / %2</translation>
</message>
<message>
<location filename="../../src/yuzu/loading_screen.cpp" line="88"/>
@@ -6742,7 +6828,7 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/loading_screen.cpp" line="173"/>
<source>Estimated Time %1</source>
- <translation>予想時間 %1</translation>
+ <translation>推定残り時間 %1</translation>
</message>
</context>
<context>
@@ -6750,7 +6836,7 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/multiplayer/lobby.ui" line="14"/>
<source>Public Room Browser</source>
- <translation>公開ルームブラウザ</translation>
+ <translation>公開ルームブラウザー</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/lobby.ui" line="32"/>
@@ -6786,12 +6872,12 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/multiplayer/lobby.ui" line="110"/>
<source>Refresh Lobby</source>
- <translation>ロビー更新</translation>
+ <translation>ロビーを更新</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/lobby.cpp" line="122"/>
<source>Password Required to Join</source>
- <translation>参加にはパスワードが必要です。</translation>
+ <translation>参加にはパスワードが必要です</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/lobby.cpp" line="122"/>
@@ -6826,7 +6912,7 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/multiplayer/lobby.cpp" line="296"/>
<source>Refresh List</source>
- <translation>リスト更新</translation>
+ <translation>リストを更新</translation>
</message>
</context>
<context>
@@ -6849,7 +6935,7 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/main.ui" line="62"/>
<source>Open &amp;Eden Folders</source>
- <translation type="unfinished"/>
+ <translation>Edenフォルダーを開く(&amp;E)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="85"/>
@@ -6864,7 +6950,7 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/main.ui" line="100"/>
<source>&amp;Reset Window Size</source>
- <translation>ウィンドウサイズのリセット(&amp;R)</translation>
+ <translation>ウィンドウサイズをリセット(&amp;R)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="105"/>
@@ -6874,47 +6960,47 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/main.ui" line="110"/>
<source>&amp;Game List Mode</source>
- <translation type="unfinished"/>
+ <translation>ゲームリスト表示モード(&amp;G)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="118"/>
<source>Game &amp;Icon Size</source>
- <translation type="unfinished"/>
+ <translation>ゲームアイコンサイズ(&amp;I)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="123"/>
<source>Reset Window Size to &amp;720p</source>
- <translation>&amp;720P</translation>
+ <translation>ウィンドウサイズを720pにリセット(&amp;7)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="126"/>
<source>Reset Window Size to 720p</source>
- <translation>ウィンドウサイズを720Pにリセット</translation>
+ <translation>ウィンドウサイズを720pにリセット</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="131"/>
<source>Reset Window Size to &amp;900p</source>
- <translation>&amp;900P</translation>
+ <translation>ウィンドウサイズを900pにリセット(&amp;9)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="134"/>
<source>Reset Window Size to 900p</source>
- <translation>ウィンドウサイズを900Pにリセット</translation>
+ <translation>ウィンドウサイズを900pにリセット</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="139"/>
<source>Reset Window Size to &amp;1080p</source>
- <translation>&amp;1080P</translation>
+ <translation>ウィンドウサイズを1080pにリセット(&amp;1)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="142"/>
<source>Reset Window Size to 1080p</source>
- <translation>ウィンドウサイズを1080Pにリセット</translation>
+ <translation>ウィンドウサイズを1080pにリセット</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="164"/>
<source>&amp;Multiplayer</source>
- <translation>マルチプレイヤー (&amp;M)</translation>
+ <translation>マルチプレイ(&amp;M)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="175"/>
@@ -6924,27 +7010,27 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/main.ui" line="179"/>
<source>Am&amp;iibo</source>
- <translation type="unfinished"/>
+ <translation>amiibo(&amp;I)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="188"/>
<source>Launch &amp;Applet</source>
- <translation type="unfinished"/>
+ <translation>アプレットを起動(&amp;A)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="197"/>
<source>&amp;TAS</source>
- <translation>&amp;TAS</translation>
+ <translation>TAS(&amp;T)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="207"/>
<source>&amp;Create Home Menu Shortcut</source>
- <translation type="unfinished"/>
+ <translation>ホームメニューのショートカットを作成(&amp;C)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="214"/>
<source>Install &amp;Firmware</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアをインストール(&amp;F)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="233"/>
@@ -6959,12 +7045,12 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/main.ui" line="259"/>
<source>L&amp;oad File...</source>
- <translation>ファイルをロード...(&amp;L)</translation>
+ <translation>ファイルを読み込む...(&amp;O)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="264"/>
<source>Load &amp;Folder...</source>
- <translation>フォルダをロード...(&amp;F)</translation>
+ <translation>フォルダーを読み込む...(&amp;F)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="269"/>
@@ -6975,7 +7061,7 @@ Debug Message: </source>
<location filename="../../src/yuzu/main.ui" line="277"/>
<location filename="../../src/yuzu/main_window.cpp" line="1661"/>
<source>&amp;Pause</source>
- <translation>中断(&amp;P)</translation>
+ <translation>一時停止(&amp;P)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="285"/>
@@ -6985,12 +7071,12 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/main.ui" line="290"/>
<source>&amp;Verify Installed Contents</source>
- <translation>インストールされたコンテンツを確認(&amp;V)</translation>
+ <translation>インストール済みコンテンツを検証(&amp;V)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="295"/>
<source>&amp;About Eden</source>
- <translation type="unfinished"/>
+ <translation>Edenについて(&amp;A)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="303"/>
@@ -7005,117 +7091,117 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/main.ui" line="311"/>
<source>Ctrl+,</source>
- <translation type="unfinished"/>
+ <translation>Ctrl+,</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="322"/>
<source>Enable Overlay Display Applet</source>
- <translation type="unfinished"/>
+ <translation>オーバーレイ表示アプレットを有効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="330"/>
<source>Show &amp;Filter Bar</source>
- <translation>フィルターバーを表示 (&amp;F)</translation>
+ <translation>フィルターバーを表示(&amp;F)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="338"/>
<source>Show &amp;Status Bar</source>
- <translation>ステータスバー(&amp;S)</translation>
+ <translation>ステータスバーを表示(&amp;S)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="341"/>
<source>Show Status Bar</source>
- <translation>ステータスバーの表示</translation>
+ <translation>ステータスバーを表示</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="349"/>
<source>&amp;Browse Public Game Lobby</source>
- <translation>公開ゲームロビーを参照 (&amp;B)</translation>
+ <translation>公開ゲームロビーを参照(&amp;B)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="357"/>
<source>&amp;Create Room</source>
- <translation>ルームを作成 (&amp;C)</translation>
+ <translation>ルームを作成(&amp;C)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="365"/>
<source>&amp;Leave Room</source>
- <translation>ルームを退出 (&amp;L)</translation>
+ <translation>ルームから退出(&amp;L)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="370"/>
<source>&amp;Direct Connect to Room</source>
- <translation>ルームに直接接続 (&amp;D)</translation>
+ <translation>ルームに直接接続(&amp;D)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="378"/>
<source>&amp;Show Current Room</source>
- <translation>現在のルームを表示 (&amp;S)</translation>
+ <translation>現在のルームを表示(&amp;S)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="386"/>
<source>F&amp;ullscreen</source>
- <translation>全画面表示(&amp;F)</translation>
+ <translation>全画面表示(&amp;U)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="394"/>
<source>&amp;Restart</source>
- <translation>再実行(&amp;R)</translation>
+ <translation>再起動(&amp;R)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="402"/>
<source>Load/Remove &amp;Amiibo...</source>
- <translation>&amp;Amiibo をロード/削除...</translation>
+ <translation>amiiboの読み込み/解除...(&amp;A)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="407"/>
<source>Open &amp;Mods Page</source>
- <translation>&amp;Modページを開く</translation>
+ <translation>MODページを開く(&amp;M)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="412"/>
<source>&amp;User Handbook</source>
- <translation type="unfinished"/>
+ <translation>ユーザーハンドブック(&amp;U)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="420"/>
<source>&amp;Capture Screenshot</source>
- <translation>スクリーンショットをキャプチャ(&amp;C)</translation>
+ <translation>スクリーンショットを撮る(&amp;C)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="425"/>
<source>&amp;Album</source>
- <translation type="unfinished"/>
+ <translation>アルバム(&amp;A)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="430"/>
<source>&amp;Set Nickname and Owner</source>
- <translation>オーナーとニックネームを設定 (&amp;S)</translation>
+ <translation>ニックネームとオーナーを設定(&amp;S)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="435"/>
<source>&amp;Delete Game Data</source>
- <translation>ゲームデータの消去 (&amp;D)</translation>
+ <translation>ゲームデータを削除(&amp;D)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="440"/>
<source>&amp;Restore Amiibo</source>
- <translation>Amiibo を復旧 (&amp;R)</translation>
+ <translation>amiiboを復元(&amp;R)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="445"/>
<source>&amp;Format Amiibo</source>
- <translation>Amiibo を初期化(&amp;F)</translation>
+ <translation>amiiboを初期化(&amp;F)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="450"/>
<source>&amp;Mii Editor</source>
- <translation type="unfinished"/>
+ <translation>Miiエディター(&amp;M)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="455"/>
<source>&amp;Configure TAS...</source>
- <translation>TASを設定... (&amp;C)</translation>
+ <translation>TASを設定...(&amp;C)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="466"/>
@@ -7126,7 +7212,7 @@ Debug Message: </source>
<location filename="../../src/yuzu/main.ui" line="477"/>
<location filename="../../src/yuzu/main_window.cpp" line="4156"/>
<source>&amp;Start</source>
- <translation>実行(&amp;S)</translation>
+ <translation>開始(&amp;S)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="485"/>
@@ -7137,849 +7223,855 @@ Debug Message: </source>
<location filename="../../src/yuzu/main.ui" line="493"/>
<location filename="../../src/yuzu/main_window.cpp" line="4157"/>
<source>R&amp;ecord</source>
- <translation>記録(&amp;R)</translation>
+ <translation>記録(&amp;E)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="498"/>
<source>Open &amp;Controller Menu</source>
- <translation>コントローラーメニューを開く (&amp;C)</translation>
+ <translation>コントローラーメニューを開く(&amp;C)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="503"/>
<source>Install Decryption &amp;Keys</source>
- <translation type="unfinished"/>
+ <translation>復号キーをインストール(&amp;K)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="508"/>
<source>&amp;Home Menu</source>
- <translation type="unfinished"/>
+ <translation>ホームメニュー(&amp;H)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="513"/>
<source>&amp;Desktop</source>
- <translation type="unfinished"/>
+ <translation>デスクトップ(&amp;D)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="518"/>
<source>&amp;Application Menu</source>
- <translation type="unfinished"/>
+ <translation>アプリケーションメニュー(&amp;A)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="523"/>
<source>&amp;Root Data Folder</source>
- <translation type="unfinished"/>
+ <translation>ルートデータフォルダー(&amp;R)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="528"/>
<source>&amp;NAND Folder</source>
- <translation type="unfinished"/>
+ <translation>NANDフォルダー(&amp;N)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="533"/>
<source>&amp;SDMC Folder</source>
- <translation type="unfinished"/>
+ <translation>SDMCフォルダー(&amp;S)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="538"/>
<source>&amp;Mod Folder</source>
- <translation type="unfinished"/>
+ <translation>MODフォルダー(&amp;M)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="543"/>
<source>&amp;Log Folder</source>
- <translation type="unfinished"/>
+ <translation>ログフォルダー(&amp;L)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="548"/>
<source>From Folder</source>
- <translation type="unfinished"/>
+ <translation>フォルダーから</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="553"/>
<source>From ZIP</source>
- <translation type="unfinished"/>
+ <translation>ZIPから</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="558"/>
<source>&amp;Eden Dependencies</source>
- <translation type="unfinished"/>
+ <translation>Edenの依存関係(&amp;E)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="563"/>
<source>&amp;Data Manager</source>
- <translation type="unfinished"/>
+ <translation>データマネージャー(&amp;D)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="571"/>
<source>&amp;Tree View</source>
- <translation type="unfinished"/>
+ <translation>ツリー表示(&amp;T)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="579"/>
<source>&amp;Grid View</source>
- <translation type="unfinished"/>
+ <translation>グリッド表示(&amp;G)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="584"/>
<source>Game Icon Size</source>
- <translation type="unfinished"/>
+ <translation>ゲームアイコンサイズ</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="589"/>
<location filename="../../src/yuzu/main_window.cpp" line="331"/>
<location filename="../../src/qt_common/config/shared_translation.h" line="31"/>
<source>None</source>
- <translation type="unfinished"/>
+ <translation>なし</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="597"/>
<source>Show Game &amp;Name</source>
- <translation type="unfinished"/>
+ <translation>ゲーム名を表示(&amp;N)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="605"/>
<source>Show &amp;Performance Overlay</source>
- <translation type="unfinished"/>
+ <translation>パフォーマンスオーバーレイを表示(&amp;P)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="610"/>
<source>Post-Processing &amp;Shaders...</source>
- <translation type="unfinished"/>
+ <translation>ポストプロセスシェーダー...(&amp;S)</translation>
</message>
<message>
<location filename="../../src/yuzu/main.ui" line="618"/>
<source>&amp;Carousel View</source>
- <translation type="unfinished"/>
+ <translation>カルーセル表示(&amp;C)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="332"/>
<source>Small (32x32)</source>
- <translation type="unfinished"/>
+ <translation>小 (32x32)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="333"/>
<source>Standard (64x64)</source>
- <translation type="unfinished"/>
+ <translation>標準 (64x64)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="334"/>
<source>Large (128x128)</source>
- <translation type="unfinished"/>
+ <translation>大 (128x128)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="335"/>
<source>Full Size (256x256)</source>
- <translation type="unfinished"/>
+ <translation>フルサイズ (256x256)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="481"/>
<source>Broken Vulkan Installation Detected</source>
- <translation type="unfinished"/>
+ <translation>破損したVulkan環境が検出されました</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="482"/>
<source>Vulkan initialization failed during boot.</source>
- <translation type="unfinished"/>
+ <translation>起動時にVulkanの初期化に失敗しました。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="503"/>
<source>Running a game</source>
<extracomment>TRANSLATORS: This string is shown to the user to explain why yuzu needs to prevent the computer from sleeping</extracomment>
- <translation type="unfinished"/>
+ <translation>ゲームを実行中</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="786"/>
<source>Loading Web Applet...</source>
- <translation type="unfinished"/>
+ <translation>Webアプレットを読み込み中...</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="836"/>
<location filename="../../src/yuzu/main_window.cpp" line="839"/>
<source>Disable Web Applet</source>
- <translation type="unfinished"/>
+ <translation>Webアプレットを無効にする</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="840"/>
<source>Disabling the web applet can lead to undefined behavior and should only be used with Super Mario 3D All-Stars. Are you sure you want to disable the web applet?
(This can be re-enabled in the Debug settings.)</source>
- <translation type="unfinished"/>
+ <translation>Webアプレットを無効にすると予期しない動作を引き起こす可能性があるため、『スーパーマリオ 3Dコレクション』でのみ使用してください。Webアプレットを無効にしますか?
+(デバッグ設定から再度有効にできます。)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="954"/>
<source>The amount of shaders currently being built</source>
- <translation type="unfinished"/>
+ <translation>現在ビルド中のシェーダーの数</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="956"/>
<source>The current selected resolution scaling multiplier.</source>
- <translation type="unfinished"/>
+ <translation>現在選択されている解像度のスケール倍率です。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="959"/>
<source>Current emulation speed. Values higher or lower than 100% indicate emulation is running faster or slower than a Switch.</source>
- <translation type="unfinished"/>
+ <translation>現在のエミュレーション速度です。100%より高い場合は実機のSwitchより速く、低い場合は遅く動作していることを示します。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="962"/>
<source>How many frames per second the game is currently displaying. This will vary from game to game and scene to scene.</source>
- <translation type="unfinished"/>
+ <translation>ゲームが現在表示している1秒あたりのフレーム数です。ゲームやシーンによって変動します。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="966"/>
<source>Time taken to emulate a Switch frame, not counting framelimiting or v-sync. For full-speed emulation this should be at most 16.67 ms.</source>
- <translation type="unfinished"/>
+ <translation>Switchの1フレームをエミュレートするのにかかった時間です (フレーム制限や垂直同期は含みません)。フルスピードで動作させるには16.67 ms以下である必要があります。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1027"/>
<source>Unmute</source>
- <translation type="unfinished"/>
+ <translation>ミュート解除</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1027"/>
<source>Mute</source>
- <translation type="unfinished"/>
+ <translation>ミュート</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1032"/>
<source>Reset Volume</source>
- <translation type="unfinished"/>
+ <translation>音量をリセット</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1070"/>
<source>Configure Effects...</source>
- <translation type="unfinished"/>
+ <translation>エフェクトを設定...</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1242"/>
<source>&amp;Clear Recent Files</source>
- <translation type="unfinished"/>
+ <translation>最近使ったファイルをクリア(&amp;C)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1659"/>
<source>&amp;Continue</source>
- <translation type="unfinished"/>
+ <translation>続行(&amp;C)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1821"/>
<source>Warning: Outdated Game Format</source>
- <translation type="unfinished"/>
+ <translation>警告: 古いゲーム形式</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1822"/>
<source>You are using the deconstructed ROM directory format for this game, which is an outdated format that has been superseded by others such as NCA, NAX, XCI, or NSP. Deconstructed ROM directories lack icons, metadata, and update support.&lt;br&gt;For an explanation of the various Switch formats Eden supports, out our user handbook. This message will not be shown again.</source>
- <translation type="unfinished"/>
+ <translation>このゲームには展開済みROMディレクトリ形式が使用されています。これはNCA、NAX、XCI、NSPなどに置き換えられた古い形式であり、アイコン、メタデータ、アップデートに対応していません。&lt;br&gt;Edenが対応しているSwitchの各種ファイル形式については、ユーザーハンドブックをご覧ください。このメッセージは今後表示されません。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1833"/>
<location filename="../../src/yuzu/main_window.cpp" line="1863"/>
<source>Error while loading ROM!</source>
- <translation type="unfinished"/>
+ <translation>ROMの読み込み中にエラーが発生しました。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1834"/>
<source>The ROM format is not supported.</source>
- <translation type="unfinished"/>
+ <translation>このROM形式はサポートされていません。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1838"/>
<source>An error occurred initializing the video core.</source>
- <translation type="unfinished"/>
+ <translation>ビデオコアの初期化中にエラーが発生しました。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1839"/>
<source>This is usually caused by outdated GPU drivers. Please see the log for more details. See: &lt;a href=&apos;https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/user/HowToAccessLogs.md&apos;&gt;How to access log files&lt;/a&gt;.</source>
- <translation type="unfinished"/>
+ <translation>通常、GPUドライバーが古いことが原因です。詳細はログをご確認ください。参照: &lt;a href='https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/user/HowToAccessLogs.md'&gt;ログファイルの確認方法&lt;/a&gt;</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1852"/>
<source>Error while loading ROM! %1</source>
<comment>%1 signifies a numeric error code.</comment>
- <translation type="unfinished"/>
+ <translation>ROMの読み込み中にエラーが発生しました: %1</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1855"/>
<source>%1&lt;br&gt;Please redump your files or ask on Discord/Stoat for help.</source>
<comment>%1 signifies an error string.</comment>
- <translation type="unfinished"/>
+ <translation>%1&lt;br&gt;ファイルをダンプし直すか、Discord/Stoatで質問してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="1864"/>
<source>An unknown error occurred. Please see the log for more details.</source>
- <translation type="unfinished"/>
+ <translation>不明なエラーが発生しました。詳細はログをご確認ください。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2038"/>
<location filename="../../src/yuzu/main_window.cpp" line="4069"/>
<source>(64-bit)</source>
- <translation type="unfinished"/>
+ <translation>(64ビット)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2038"/>
<location filename="../../src/yuzu/main_window.cpp" line="4069"/>
<source>(32-bit)</source>
- <translation type="unfinished"/>
+ <translation>(32ビット)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2039"/>
<location filename="../../src/yuzu/main_window.cpp" line="4071"/>
<source>%1 %2</source>
<comment>%1 is the title name. %2 indicates if the title is 64-bit or 32-bit</comment>
- <translation type="unfinished"/>
+ <translation>%1 %2</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2117"/>
<source>Closing software...</source>
- <translation type="unfinished"/>
+ <translation>ソフトウェアを終了しています...</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2283"/>
<source>Save Data</source>
- <translation type="unfinished"/>
+ <translation>セーブデータ</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2314"/>
<source>Mod Data</source>
- <translation type="unfinished"/>
+ <translation>MODデータ</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2327"/>
<source>Error Opening %1 Folder</source>
- <translation type="unfinished"/>
+ <translation>%1フォルダーを開く際にエラーが発生しました</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2328"/>
<location filename="../../src/yuzu/main_window.cpp" line="2661"/>
<source>Folder does not exist!</source>
- <translation type="unfinished"/>
+ <translation>フォルダーが存在しません。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2406"/>
<source>Remove Installed Game Contents?</source>
- <translation type="unfinished"/>
+ <translation>インストール済みのゲームコンテンツを削除しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2408"/>
<source>Remove Installed Game Update?</source>
- <translation type="unfinished"/>
+ <translation>インストール済みのゲームアップデートを削除しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2410"/>
<source>Remove Installed Game DLC?</source>
- <translation type="unfinished"/>
+ <translation>インストール済みのゲームDLCを削除しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2416"/>
<source>Remove Entry</source>
- <translation type="unfinished"/>
+ <translation>エントリーを削除</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2446"/>
<source>Delete OpenGL Transferable Shader Cache?</source>
- <translation type="unfinished"/>
+ <translation>OpenGLの転送可能なシェーダーキャッシュを削除しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2448"/>
<source>Delete Vulkan Transferable Shader Cache?</source>
- <translation type="unfinished"/>
+ <translation>Vulkanの転送可能なシェーダーキャッシュを削除しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2450"/>
<source>Delete All Transferable Shader Caches?</source>
- <translation type="unfinished"/>
+ <translation>すべての転送可能なシェーダーキャッシュを削除しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2452"/>
<source>Remove Custom Game Configuration?</source>
- <translation type="unfinished"/>
+ <translation>ゲームの個別設定を削除しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2454"/>
<source>Remove Cache Storage?</source>
- <translation type="unfinished"/>
+ <translation>キャッシュストレージを削除しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2460"/>
<source>Remove File</source>
- <translation type="unfinished"/>
+ <translation>ファイルを削除</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2497"/>
<source>Remove Play Time Data</source>
- <translation type="unfinished"/>
+ <translation>プレイ時間データを削除</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2497"/>
<source>Reset play time?</source>
- <translation type="unfinished"/>
+ <translation>プレイ時間をリセットしますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2510"/>
<location filename="../../src/yuzu/main_window.cpp" line="2595"/>
<source>RomFS Extraction Failed!</source>
- <translation type="unfinished"/>
+ <translation>RomFSの抽出に失敗しました</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2511"/>
<source>There was an error copying the RomFS files or the user cancelled the operation.</source>
- <translation type="unfinished"/>
+ <translation>RomFSファイルのコピー中にエラーが発生したか、ユーザーが操作をキャンセルしました。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2570"/>
<source>Full</source>
- <translation type="unfinished"/>
+ <translation>フル</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2570"/>
<source>Skeleton</source>
- <translation type="unfinished"/>
+ <translation>スケルトン</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2572"/>
<source>Select RomFS Dump Mode</source>
- <translation type="unfinished"/>
+ <translation>RomFSのダンプモードを選択</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2573"/>
<source>Please select the how you would like the RomFS dumped.&lt;br&gt;Full will copy all of the files into the new directory while &lt;br&gt;skeleton will only create the directory structure.</source>
- <translation type="unfinished"/>
+ <translation>RomFSのダンプ方法を選択してください。&lt;br&gt;「フル」はすべてのファイルを新しいディレクトリにコピーし、&lt;br&gt;「スケルトン」はディレクトリ構造のみを作成します。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2596"/>
<source>There is not enough free space at %1 to extract the RomFS. Please free up space or select a different dump directory at Emulation &gt; Configure &gt; System &gt; Filesystem &gt; Dump Root</source>
- <translation type="unfinished"/>
+ <translation>%1 の空き容量が不足しているため、RomFSを抽出できません。空き容量を確保するか、[エミュレーション] &gt; [設定] &gt; [システム] &gt; [ファイルシステム] &gt; [ダンプ先のルート] で別の保存先を選択してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2603"/>
<source>Extracting RomFS...</source>
- <translation type="unfinished"/>
+ <translation>RomFSを抽出しています...</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2603"/>
<location filename="../../src/yuzu/main_window.cpp" line="2845"/>
<source>Cancel</source>
- <translation type="unfinished"/>
+ <translation>キャンセル</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2613"/>
<source>RomFS Extraction Succeeded!</source>
- <translation type="unfinished"/>
+ <translation>RomFSの抽出に成功しました</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2614"/>
<source>The operation completed successfully.</source>
- <translation type="unfinished"/>
+ <translation>操作は正常に完了しました。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2660"/>
<source>Error Opening %1</source>
- <translation type="unfinished"/>
+ <translation>%1を開く際にエラーが発生しました</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2669"/>
<source>Select Directory</source>
- <translation type="unfinished"/>
+ <translation>ディレクトリを選択</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2698"/>
<source>Properties</source>
- <translation type="unfinished"/>
+ <translation>プロパティ</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2699"/>
<source>The game properties could not be loaded.</source>
- <translation type="unfinished"/>
+ <translation>ゲームのプロパティを読み込めませんでした。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2761"/>
<source>Switch Executable (%1);;All Files (*.*)</source>
<comment>%1 is an identifier for the Switch executable file extensions.</comment>
- <translation type="unfinished"/>
+ <translation>Switch実行ファイル (%1);;すべてのファイル (*.*)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2765"/>
<source>Load File</source>
- <translation type="unfinished"/>
+ <translation>ファイルを読み込む</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2778"/>
<source>Open Extracted ROM Directory</source>
- <translation type="unfinished"/>
+ <translation>展開済みROMディレクトリを開く</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2789"/>
<source>Invalid Directory Selected</source>
- <translation type="unfinished"/>
+ <translation>無効なディレクトリが選択されました</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2790"/>
<source>The directory you have selected does not contain a &apos;main&apos; file.</source>
- <translation type="unfinished"/>
+ <translation>選択したディレクトリに「main」ファイルが含まれていません。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2800"/>
<source>Installable Switch File (*.nca *.nsp *.xci);;Nintendo Content Archive (*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge Image (*.xci)</source>
- <translation type="unfinished"/>
+ <translation>インストール可能なSwitchファイル (*.nca *.nsp *.xci);;Nintendo Content Archive (*.nca);;Nintendo Submission Package (*.nsp);;NX Cartridge Image (*.xci)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2805"/>
<source>Install Files</source>
- <translation type="unfinished"/>
+ <translation>ファイルをインストール</translation>
</message>
<message numerus="yes">
<location filename="../../src/yuzu/main_window.cpp" line="2852"/>
<source>%n file(s) remaining</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>残り %n ファイル</numerusform></translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2854"/>
<source>Installing file &quot;%1&quot;...</source>
- <translation type="unfinished"/>
+ <translation>ファイル「%1」をインストールしています...</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2908"/>
<location filename="../../src/yuzu/main_window.cpp" line="2922"/>
<source>Install Results</source>
- <translation type="unfinished"/>
+ <translation>インストール結果</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2909"/>
<source>To avoid possible conflicts, we discourage users from installing base games to the NAND.
Please, only use this feature to install updates and DLC.</source>
- <translation type="unfinished"/>
+ <translation>競合を避けるため、ベースゲームをNANDにインストールすることは推奨しません。
+この機能はアップデートとDLCのインストールにのみ使用してください。</translation>
</message>
<message numerus="yes">
<location filename="../../src/yuzu/main_window.cpp" line="2915"/>
<source>%n file(s) were newly installed
</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>%n 個のファイルを新規インストールしました
+</numerusform></translation>
</message>
<message numerus="yes">
<location filename="../../src/yuzu/main_window.cpp" line="2918"/>
<source>%n file(s) were overwritten
</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>%n 個のファイルを上書きしました
+</numerusform></translation>
</message>
<message numerus="yes">
<location filename="../../src/yuzu/main_window.cpp" line="2920"/>
<source>%n file(s) failed to install
</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>%n 個のファイルのインストールに失敗しました
+</numerusform></translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2930"/>
<source>System Application</source>
- <translation type="unfinished"/>
+ <translation>システムアプリケーション</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2931"/>
<source>System Archive</source>
- <translation type="unfinished"/>
+ <translation>システムアーカイブ</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2932"/>
<source>System Application Update</source>
- <translation type="unfinished"/>
+ <translation>システムアプリケーションのアップデート</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2933"/>
<source>Firmware Package (Type A)</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアパッケージ (タイプA)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2934"/>
<source>Firmware Package (Type B)</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアパッケージ (タイプB)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2935"/>
<source>Game</source>
- <translation type="unfinished"/>
+ <translation>ゲーム</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2936"/>
<source>Game Update</source>
- <translation type="unfinished"/>
+ <translation>ゲームアップデート</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2937"/>
<source>Game DLC</source>
- <translation type="unfinished"/>
+ <translation>ゲームDLC</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2938"/>
<source>Delta Title</source>
- <translation type="unfinished"/>
+ <translation>差分タイトル</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2941"/>
<source>Select NCA Install Type...</source>
- <translation type="unfinished"/>
+ <translation>NCAのインストール種別を選択...</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2942"/>
<source>Please select the type of title you would like to install this NCA as:
(In most instances, the default &apos;Game&apos; is fine.)</source>
- <translation type="unfinished"/>
+ <translation>このNCAをインストールするタイトルの種別を選択してください:
+(ほとんどの場合、デフォルトの「ゲーム」で問題ありません。)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2948"/>
<source>Failed to Install</source>
- <translation type="unfinished"/>
+ <translation>インストールに失敗しました</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2949"/>
<source>The title type you selected for the NCA is invalid.</source>
- <translation type="unfinished"/>
+ <translation>NCAに選択されたタイトル種別が無効です。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2984"/>
<source>File not found</source>
- <translation type="unfinished"/>
+ <translation>ファイルが見つかりません</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="2985"/>
<source>File &quot;%1&quot; not found</source>
- <translation type="unfinished"/>
+ <translation>ファイル「%1」が見つかりません</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3123"/>
<source>OK</source>
- <translation type="unfinished"/>
+ <translation>OK</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3141"/>
<source>Function Disabled</source>
- <translation type="unfinished"/>
+ <translation>機能は無効です</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3142"/>
<source>Compatibility list reporting is currently disabled. Check back later!</source>
- <translation type="unfinished"/>
+ <translation>互換性リストへの報告は現在無効です。後ほどもう一度お試しください。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3184"/>
<source>Error opening URL</source>
- <translation type="unfinished"/>
+ <translation>URLを開く際にエラーが発生しました</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3185"/>
<source>Unable to open the URL &quot;%1&quot;.</source>
- <translation type="unfinished"/>
+ <translation>URL「%1」を開けませんでした。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3560"/>
<source>TAS Recording</source>
- <translation type="unfinished"/>
+ <translation>TASの記録</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3560"/>
<source>Overwrite file of player 1?</source>
- <translation type="unfinished"/>
+ <translation>プレイヤー1のファイルを上書きしますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3584"/>
<source>Invalid config detected</source>
- <translation type="unfinished"/>
+ <translation>無効な設定が検出されました</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3585"/>
<source>Handheld controller can&apos;t be used on docked mode. Pro controller will be selected.</source>
- <translation type="unfinished"/>
+ <translation>携帯モードのコントローラーはドックモードでは使用できません。Proコントローラーが選択されます。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3736"/>
<location filename="../../src/yuzu/main_window.cpp" line="3787"/>
<source>Amiibo</source>
- <translation type="unfinished"/>
+ <translation>amiibo</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3736"/>
<location filename="../../src/yuzu/main_window.cpp" line="3787"/>
<source>The current amiibo has been removed</source>
- <translation type="unfinished"/>
+ <translation>現在のamiiboが取り外されました</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3741"/>
<source>Error</source>
- <translation type="unfinished"/>
+ <translation>エラー</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3741"/>
<location filename="../../src/yuzu/main_window.cpp" line="3799"/>
<source>The current game is not looking for amiibos</source>
- <translation type="unfinished"/>
+ <translation>現在のゲームはamiiboを必要としていません</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3747"/>
<source>Amiibo File (%1);; All Files (*.*)</source>
- <translation type="unfinished"/>
+ <translation>amiiboファイル (%1);;すべてのファイル (*.*)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3748"/>
<source>Load Amiibo</source>
- <translation type="unfinished"/>
+ <translation>amiiboを読み込む</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3783"/>
<source>Error loading Amiibo data</source>
- <translation type="unfinished"/>
+ <translation>amiiboデータの読み込み中にエラーが発生しました</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3793"/>
<source>The selected file is not a valid amiibo</source>
- <translation type="unfinished"/>
+ <translation>選択したファイルは有効なamiiboではありません</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3796"/>
<source>The selected file is already on use</source>
- <translation type="unfinished"/>
+ <translation>選択したファイルはすでに使用中です</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3802"/>
<source>An unknown error occurred</source>
- <translation type="unfinished"/>
+ <translation>不明なエラーが発生しました</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3922"/>
<source>No firmware available</source>
- <translation type="unfinished"/>
+ <translation>利用可能なファームウェアがありません</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3926"/>
<source>Firmware Corrupted</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアが破損しています</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3992"/>
<source>Unknown applet</source>
- <translation type="unfinished"/>
+ <translation>不明なアプレット</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3993"/>
<source>Applet doesn&apos;t map to a known value.</source>
- <translation type="unfinished"/>
+ <translation>アプレットが既知の値に対応していません。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3996"/>
<source>Record not found</source>
- <translation type="unfinished"/>
+ <translation>レコードが見つかりません</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="3997"/>
<source>Applet not found. Please reinstall firmware.</source>
- <translation type="unfinished"/>
+ <translation>アプレットが見つかりません。ファームウェアを再インストールしてください。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4029"/>
<source>Capture Screenshot</source>
- <translation type="unfinished"/>
+ <translation>スクリーンショットを撮影</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4030"/>
<source>PNG Image (*.png)</source>
- <translation type="unfinished"/>
+ <translation>PNG画像 (*.png)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4132"/>
<source>TAS state: Running %1/%2</source>
- <translation type="unfinished"/>
+ <translation>TASの状態: 実行中 %1/%2</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4136"/>
<source>TAS state: Recording %1</source>
- <translation type="unfinished"/>
+ <translation>TASの状態: 記録中 %1</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4138"/>
<source>TAS state: Idle %1/%2</source>
- <translation type="unfinished"/>
+ <translation>TASの状態: 待機中 %1/%2</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4142"/>
<source>TAS State: Invalid</source>
- <translation type="unfinished"/>
+ <translation>TASの状態: 無効</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4156"/>
<source>&amp;Stop Running</source>
- <translation type="unfinished"/>
+ <translation>実行を停止(&amp;S)</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4157"/>
<source>Stop R&amp;ecording</source>
- <translation type="unfinished"/>
+ <translation>記録を停止(&amp;E)</translation>
</message>
<message numerus="yes">
<location filename="../../src/yuzu/main_window.cpp" line="4182"/>
<source>Building: %n shader(s)</source>
- <translation type="unfinished"><numerusform></numerusform></translation>
+ <translation><numerusform>シェーダー構築中: %n</numerusform></translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4191"/>
<source>Scale: %1x</source>
<comment>%1 is the resolution scaling factor</comment>
- <translation type="unfinished"/>
+ <translation>倍率: %1x</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4194"/>
<source>Speed: %1% / %2%</source>
- <translation type="unfinished"/>
+ <translation>速度: %1% / %2%</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4198"/>
<source>Speed: %1%</source>
- <translation type="unfinished"/>
+ <translation>速度: %1%</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4201"/>
<source>Game: %1 FPS</source>
- <translation type="unfinished"/>
+ <translation>ゲーム: %1 FPS</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4207"/>
<source>Frame: %1 ms</source>
- <translation type="unfinished"/>
+ <translation>フレーム: %1 ms</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4248"/>
<source>NO AA</source>
- <translation type="unfinished"/>
+ <translation>AAなし</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4258"/>
<source>NO FX</source>
- <translation type="unfinished"/>
+ <translation>FXなし</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4264"/>
<source>FX</source>
- <translation type="unfinished"/>
+ <translation>FX</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4277"/>
<source>VOLUME: MUTE</source>
- <translation type="unfinished"/>
+ <translation>音量: ミュート</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4280"/>
<source>VOLUME: %1%</source>
<comment>Volume percentage (e.g. 50%)</comment>
- <translation type="unfinished"/>
+ <translation>音量: %1%</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4343"/>
<source>Derivation Components Missing</source>
- <translation type="unfinished"/>
+ <translation>キー生成コンポーネントが見つかりません</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4344"/>
<source>Decryption keys are missing. Install them now?</source>
- <translation type="unfinished"/>
+ <translation>復号キーが見つかりません。今すぐインストールしますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4374"/>
<source>Wayland Detected!</source>
- <translation type="unfinished"/>
+ <translation>Waylandが検出されました</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4376"/>
@@ -7987,211 +8079,216 @@ Please, only use this feature to install updates and DLC.</source>
It's recommended to use X11 instead.
Would you like to force it for future launches?</source>
- <translation type="unfinished"/>
+ <translation>Wayland環境では、重大なパフォーマンス低下や原因不明の不具合が発生することが知られています。
+代わりにX11を使用することを推奨します。
+
+次回以降の起動でX11を強制しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4381"/>
<source>Use X11</source>
- <translation type="unfinished"/>
+ <translation>X11を使用</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4382"/>
<source>Continue with Wayland</source>
- <translation type="unfinished"/>
+ <translation>Waylandで続行</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4385"/>
<source>Don&apos;t show again</source>
- <translation type="unfinished"/>
+ <translation>今後表示しない</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4399"/>
<source>Restart Required</source>
- <translation type="unfinished"/>
+ <translation>再起動が必要です</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4400"/>
<source>Restart Eden to apply the X11 backend.</source>
- <translation type="unfinished"/>
+ <translation>X11バックエンドを適用するには、Edenを再起動してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4438"/>
<source>Slow</source>
- <translation type="unfinished"/>
+ <translation>低速</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4441"/>
<source>Turbo</source>
- <translation type="unfinished"/>
+ <translation>ターボ</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4445"/>
<source>Unlocked</source>
- <translation type="unfinished"/>
+ <translation>制限なし</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4495"/>
<source>Select RomFS Dump Target</source>
- <translation type="unfinished"/>
+ <translation>RomFSのダンプ対象を選択</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4496"/>
<source>Please select which RomFS you would like to dump.</source>
- <translation type="unfinished"/>
+ <translation>ダンプするRomFSを選択してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4519"/>
<source>Are you sure you want to close Eden?</source>
- <translation type="unfinished"/>
+ <translation>Edenを終了してもよろしいですか?</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4520"/>
<location filename="../../src/yuzu/main_window.cpp" line="4608"/>
<location filename="../../src/yuzu/main_window.cpp" line="4620"/>
<source>Eden</source>
- <translation type="unfinished"/>
+ <translation>Eden</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4609"/>
<source>Are you sure you want to stop the emulation? Any unsaved progress will be lost.</source>
- <translation type="unfinished"/>
+ <translation>エミュレーションを停止してもよろしいですか?保存されていない進行状況は失われます。</translation>
</message>
<message>
<location filename="../../src/yuzu/main_window.cpp" line="4617"/>
<source>The currently running application has requested Eden to not exit.
Would you like to bypass this and exit anyway?</source>
- <translation type="unfinished"/>
+ <translation>実行中のアプリケーションが、Edenを終了しないよう要求しています。
+
+これを無視して終了しますか?</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="32"/>
<source>FXAA</source>
- <translation type="unfinished"/>
+ <translation>FXAA</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="33"/>
<source>SMAA</source>
- <translation type="unfinished"/>
+ <translation>SMAA</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="38"/>
<source>Nearest</source>
- <translation type="unfinished"/>
+ <translation>ニアレストネイバー</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="40"/>
<source>Bilinear</source>
- <translation type="unfinished"/>
+ <translation>バイリニア</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="41"/>
<source>Bicubic</source>
- <translation type="unfinished"/>
+ <translation>バイキュービック</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="43"/>
<source>Zero-Tangent</source>
- <translation type="unfinished"/>
+ <translation>ゼロタンジェント</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="44"/>
<source>B-Spline</source>
- <translation type="unfinished"/>
+ <translation>Bスプライン</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="46"/>
<source>Mitchell</source>
- <translation type="unfinished"/>
+ <translation>ミッチェル</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="47"/>
<source>Spline-1</source>
- <translation type="unfinished"/>
+ <translation>スプライン-1</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="49"/>
<source>Gaussian</source>
- <translation type="unfinished"/>
+ <translation>ガウシアン</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="50"/>
<source>Lanczos</source>
- <translation type="unfinished"/>
+ <translation>ランチョス</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="52"/>
<source>ScaleForce</source>
- <translation type="unfinished"/>
+ <translation>ScaleForce</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="53"/>
<source>FSR</source>
- <translation type="unfinished"/>
+ <translation>FSR</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="54"/>
<source>Area</source>
- <translation type="unfinished"/>
+ <translation>エリア</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="55"/>
<source>MMPX</source>
- <translation type="unfinished"/>
+ <translation>MMPX</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="56"/>
<source>SGSR</source>
- <translation type="unfinished"/>
+ <translation>SGSR</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="57"/>
<source>SGSR EdgeDir</source>
- <translation type="unfinished"/>
+ <translation>SGSR EdgeDir</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="61"/>
<source>Docked</source>
- <translation type="unfinished"/>
+ <translation>ドックモード</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="62"/>
<source>Handheld</source>
- <translation type="unfinished"/>
+ <translation>携帯モード</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="66"/>
<source>Fast</source>
- <translation type="unfinished"/>
+ <translation>高速</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="67"/>
<source>Accurate</source>
- <translation type="unfinished"/>
+ <translation>高精度</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="71"/>
<source>Vulkan</source>
- <translation type="unfinished"/>
+ <translation>Vulkan</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="73"/>
<source>OpenGL GLSL</source>
- <translation type="unfinished"/>
+ <translation>OpenGL GLSL</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="75"/>
<source>OpenGL SPIRV</source>
- <translation type="unfinished"/>
+ <translation>OpenGL SPIRV</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="77"/>
<source>OpenGL GLASM</source>
- <translation type="unfinished"/>
+ <translation>OpenGL GLASM</translation>
</message>
<message>
<location filename="../../src/qt_common/config/shared_translation.h" line="78"/>
<source>Null</source>
- <translation type="unfinished"/>
+ <translation>なし</translation>
</message>
</context>
<context>
@@ -8200,7 +8297,8 @@ Would you like to bypass this and exit anyway?</source>
<location filename="../../src/yuzu/migration_worker.cpp" line="52"/>
<source>Linking the old directory failed. You may need to re-run with administrative privileges on Windows.
OS gave error: %1</source>
- <translation type="unfinished"/>
+ <translation>古いディレクトリのリンクに失敗しました。Windowsでは、管理者権限で再実行する必要がある場合があります。
+OSのエラー: %1</translation>
</message>
<message>
<location filename="../../src/yuzu/migration_worker.cpp" line="70"/>
@@ -8211,7 +8309,13 @@ If this is not desirable, delete the following files:
%2
%3
%4</source>
- <translation type="unfinished"/>
+ <translation>
+
+設定とデータは%1と共有されます。
+これを望まない場合は、次のファイルを削除してください:
+%2
+%3
+%4</translation>
</message>
<message>
<location filename="../../src/yuzu/migration_worker.cpp" line="110"/>
@@ -8219,12 +8323,15 @@ If this is not desirable, delete the following files:
If you wish to clean up the files which were left in the old data location, you can do so by deleting the following directory:
%1</source>
- <translation type="unfinished"/>
+ <translation>
+
+古いデータの保存場所に残っているファイルを整理するには、次のディレクトリを削除してください:
+%1</translation>
</message>
<message>
<location filename="../../src/yuzu/migration_worker.h" line="73"/>
<source>Data was migrated successfully.</source>
- <translation type="unfinished"/>
+ <translation>データの移行が完了しました。</translation>
</message>
</context>
<context>
@@ -8232,12 +8339,12 @@ If you wish to clean up the files which were left in the old data location, you
<message>
<location filename="../../src/yuzu/configuration/addon/mod_select_dialog.ui" line="14"/>
<source>Import Mods</source>
- <translation type="unfinished"/>
+ <translation>MODをインポート</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/addon/mod_select_dialog.ui" line="20"/>
<source>The specified folder or archive contains the following mods. Select which ones to install.</source>
- <translation type="unfinished"/>
+ <translation>指定されたフォルダーまたはアーカイブには以下のMODが含まれています。インストールするものを選択してください。</translation>
</message>
</context>
<context>
@@ -8245,12 +8352,12 @@ If you wish to clean up the files which were left in the old data location, you
<message>
<location filename="../../src/yuzu/multiplayer/moderation_dialog.ui" line="6"/>
<source>Moderation</source>
- <translation type="unfinished"/>
+ <translation>モデレーション</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/moderation_dialog.ui" line="20"/>
<source>Ban List</source>
- <translation>Banリスト</translation>
+ <translation>BANリスト</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/moderation_dialog.ui" line="41"/>
@@ -8261,12 +8368,12 @@ If you wish to clean up the files which were left in the old data location, you
<message>
<location filename="../../src/yuzu/multiplayer/moderation_dialog.ui" line="51"/>
<source>Unban</source>
- <translation>Ban解除</translation>
+ <translation>BANを解除</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/moderation_dialog.cpp" line="42"/>
<source>Subject</source>
- <translation type="unfinished"/>
+ <translation>対象</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/moderation_dialog.cpp" line="43"/>
@@ -8276,7 +8383,7 @@ If you wish to clean up the files which were left in the old data location, you
<message>
<location filename="../../src/yuzu/multiplayer/moderation_dialog.cpp" line="85"/>
<source>Forum Username</source>
- <translation>フォーラムのユーザ名</translation>
+ <translation>フォーラムのユーザー名</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/moderation_dialog.cpp" line="90"/>
@@ -8309,7 +8416,7 @@ If you wish to clean up the files which were left in the old data location, you
<message>
<location filename="../../src/yuzu/multiplayer/state.cpp" line="131"/>
<source>Connected</source>
- <translation>接続の状態</translation>
+ <translation>接続済み</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/state.cpp" line="138"/>
@@ -8325,8 +8432,8 @@ If you wish to clean up the files which were left in the old data location, you
<location filename="../../src/yuzu/multiplayer/state.cpp" line="210"/>
<source>Failed to update the room information. Please check your Internet connection and try hosting the room again.
Debug Message: </source>
- <translation>ルーム情報の更新に失敗しました。インターネット接続を確認し、再度ルームのホストをお試しください。
-デバッグメッセージ:</translation>
+ <translation>ルーム情報の更新に失敗しました。インターネット接続を確認して、もう一度ルームをホストしてください。
+デバッグメッセージ: </translation>
</message>
</context>
<context>
@@ -8334,24 +8441,24 @@ Debug Message: </source>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="68"/>
<source>Game already running</source>
- <translation>ゲーム進行中</translation>
+ <translation>ゲーム実行中</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="69"/>
<source>Joining a room when the game is already running is discouraged and can cause the room feature not to work correctly.
Proceed anyway?</source>
- <translation>ゲーム進行中にルームに参加することはお勧めしません. ルーム機能が正しく動作しない原因になります.
-とにかく実行しますか?</translation>
+ <translation>ゲーム実行中にルームへ参加することは推奨されません。ルーム機能が正常に動作しなくなる可能性があります。
+それでも続行しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="75"/>
<source>Leave Room</source>
- <translation>ルームを離れる</translation>
+ <translation>ルームから退出</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="76"/>
<source>You are about to close the room. Any network connections will be closed.</source>
- <translation>ルームを閉じようとしています。ネットワーク接続がすべて終了します。</translation>
+ <translation>ルームを閉じようとしています。すべてのネットワーク接続が終了します。</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="81"/>
@@ -8361,7 +8468,7 @@ Proceed anyway?</source>
<message>
<location filename="../../src/yuzu/multiplayer/message.cpp" line="82"/>
<source>You are about to leave the room. Any network connections will be closed.</source>
- <translation>ルームを退出しようとしています。ネットワーク接続はすべて終了します。</translation>
+ <translation>ルームから退出しようとしています。すべてのネットワーク接続が終了します。</translation>
</message>
</context>
<context>
@@ -8370,127 +8477,127 @@ Proceed anyway?</source>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.ui" line="14"/>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="61"/>
<source>New User</source>
- <translation type="unfinished"/>
+ <translation>新規ユーザー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.ui" line="30"/>
<source>Change Avatar</source>
- <translation type="unfinished"/>
+ <translation>アバターを変更</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.ui" line="37"/>
<source>Set Image</source>
- <translation type="unfinished"/>
+ <translation>画像を設定</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.ui" line="53"/>
<source>UUID</source>
- <translation type="unfinished"/>
+ <translation>UUID</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.ui" line="60"/>
<source>Eden</source>
- <translation type="unfinished"/>
+ <translation>Eden</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.ui" line="70"/>
<source>Username</source>
- <translation type="unfinished"/>
+ <translation>ユーザー名</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.ui" line="82"/>
<source>UUID must be 32 hex characters (0-9, A-F)</source>
- <translation type="unfinished"/>
+ <translation>UUIDは32桁の16進数 (0〜9、A〜F) で入力してください</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.ui" line="89"/>
<source>Generate</source>
- <translation type="unfinished"/>
+ <translation>生成</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="138"/>
<source>Select User Image</source>
- <translation type="unfinished"/>
+ <translation>ユーザー画像を選択</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="139"/>
<source>Image Formats (*.jpg *.jpeg *.png *.bmp)</source>
- <translation type="unfinished"/>
+ <translation>画像ファイル (*.jpg *.jpeg *.png *.bmp)</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="171"/>
<source>No firmware available</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアがありません</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="172"/>
<source>Please install the firmware to use firmware avatars.</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアのアバターを使用するには、ファームウェアをインストールしてください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="177"/>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="184"/>
<source>Error loading archive</source>
- <translation type="unfinished"/>
+ <translation>アーカイブの読み込みエラー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="178"/>
<source>Archive is not available. Please install/reinstall firmware.</source>
- <translation type="unfinished"/>
+ <translation>アーカイブを利用できません。ファームウェアをインストールまたは再インストールしてください。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="185"/>
<source>Could not locate RomFS. Your file or decryption keys may be corrupted.</source>
- <translation type="unfinished"/>
+ <translation>RomFSが見つかりませんでした。ファイルまたは復号キーが破損している可能性があります。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="191"/>
<source>Error extracting archive</source>
- <translation type="unfinished"/>
+ <translation>アーカイブの展開エラー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="192"/>
<source>Could not extract RomFS. Your file or decryption keys may be corrupted.</source>
- <translation type="unfinished"/>
+ <translation>RomFSを展開できませんでした。ファイルまたは復号キーが破損している可能性があります。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="197"/>
<source>Error finding image directory</source>
- <translation type="unfinished"/>
+ <translation>画像ディレクトリの検索エラー</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="198"/>
<source>Failed to find image directory in the archive.</source>
- <translation type="unfinished"/>
+ <translation>アーカイブ内に画像ディレクトリが見つかりませんでした。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="218"/>
<source>No images found</source>
- <translation type="unfinished"/>
+ <translation>画像が見つかりません</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="219"/>
<source>No avatar images were found in the archive.</source>
- <translation type="unfinished"/>
+ <translation>アーカイブ内にアバター画像が見つかりませんでした。</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="338"/>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="346"/>
<source>All Good</source>
<comment>Tooltip</comment>
- <translation type="unfinished"/>
+ <translation>問題ありません</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="341"/>
<source>Must be 32 hex characters (0-9, a-f)</source>
<comment>Tooltip</comment>
- <translation type="unfinished"/>
+ <translation>32桁の16進数 (0〜9、a〜f) で入力してください</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/new_user_dialog.cpp" line="349"/>
<source>Must be between 1 and 32 characters</source>
<comment>Tooltip</comment>
- <translation type="unfinished"/>
+ <translation>1〜32文字で入力してください</translation>
</message>
</context>
<context>
@@ -8531,73 +8638,73 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/yuzu/render/performance_overlay.ui" line="14"/>
<source>Form</source>
- <translation type="unfinished"/>
+ <translation>フォーム</translation>
</message>
<message>
<location filename="../../src/yuzu/render/performance_overlay.ui" line="32"/>
<source>Frametime</source>
- <translation type="unfinished"/>
+ <translation>フレームタイム</translation>
</message>
<message>
<location filename="../../src/yuzu/render/performance_overlay.ui" line="45"/>
<source>0 ms</source>
- <translation type="unfinished"/>
+ <translation>0 ms</translation>
</message>
<message>
<location filename="../../src/yuzu/render/performance_overlay.ui" line="63"/>
<location filename="../../src/yuzu/render/performance_overlay.ui" line="143"/>
<source>Min: 0</source>
- <translation type="unfinished"/>
+ <translation>最小: 0</translation>
</message>
<message>
<location filename="../../src/yuzu/render/performance_overlay.ui" line="77"/>
<location filename="../../src/yuzu/render/performance_overlay.ui" line="157"/>
<source>Max: 0</source>
- <translation type="unfinished"/>
+ <translation>最大: 0</translation>
</message>
<message>
<location filename="../../src/yuzu/render/performance_overlay.ui" line="91"/>
<location filename="../../src/yuzu/render/performance_overlay.ui" line="171"/>
<source>Avg: 0</source>
- <translation type="unfinished"/>
+ <translation>平均: 0</translation>
</message>
<message>
<location filename="../../src/yuzu/render/performance_overlay.ui" line="112"/>
<source>FPS</source>
- <translation type="unfinished"/>
+ <translation>FPS</translation>
</message>
<message>
<location filename="../../src/yuzu/render/performance_overlay.ui" line="125"/>
<source>0 fps</source>
- <translation type="unfinished"/>
+ <translation>0 fps</translation>
</message>
<message>
<location filename="../../src/yuzu/render/performance_overlay.cpp" line="103"/>
<source>%1 fps</source>
- <translation type="unfinished"/>
+ <translation>%1 fps</translation>
</message>
<message>
<location filename="../../src/yuzu/render/performance_overlay.cpp" line="124"/>
<location filename="../../src/yuzu/render/performance_overlay.cpp" line="174"/>
<source>Avg: %1</source>
- <translation type="unfinished"/>
+ <translation>平均: %1</translation>
</message>
<message>
<location filename="../../src/yuzu/render/performance_overlay.cpp" line="133"/>
<location filename="../../src/yuzu/render/performance_overlay.cpp" line="163"/>
<source>Min: %1</source>
- <translation type="unfinished"/>
+ <translation>最小: %1</translation>
</message>
<message>
<location filename="../../src/yuzu/render/performance_overlay.cpp" line="134"/>
<location filename="../../src/yuzu/render/performance_overlay.cpp" line="164"/>
<source>Max: %1</source>
- <translation type="unfinished"/>
+ <translation>最大: %1</translation>
</message>
<message>
<location filename="../../src/yuzu/render/performance_overlay.cpp" line="151"/>
<source>%1 ms</source>
- <translation type="unfinished"/>
+ <translation>%1 ms</translation>
</message>
</context>
<context>
@@ -8605,7 +8712,7 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/yuzu/configuration/configure_input_player_widget.cpp" line="1652"/>
<source>START/PAUSE</source>
- <translation>スタート/ ポーズ</translation>
+ <translation>スタート/ポーズ</translation>
</message>
</context>
<context>
@@ -8613,22 +8720,22 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/yuzu/configuration/system/profile_avatar_dialog.cpp" line="18"/>
<source>Select</source>
- <translation type="unfinished"/>
+ <translation>選択</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/profile_avatar_dialog.cpp" line="19"/>
<source>Cancel</source>
- <translation type="unfinished"/>
+ <translation>キャンセル</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/profile_avatar_dialog.cpp" line="20"/>
<source>Background Color</source>
- <translation type="unfinished"/>
+ <translation>背景色</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/system/profile_avatar_dialog.cpp" line="41"/>
<source>Select Firmware Avatar</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアのアバターを選択</translation>
</message>
</context>
<context>
@@ -8638,56 +8745,59 @@ p, li { white-space: pre-wrap; }
<location filename="../../src/yuzu/user_data_migration.cpp" line="121"/>
<location filename="../../src/yuzu/user_data_migration.cpp" line="148"/>
<source>Migration</source>
- <translation type="unfinished"/>
+ <translation>移行</translation>
</message>
<message>
<location filename="../../src/yuzu/user_data_migration.cpp" line="57"/>
<source>Clear Shader Cache</source>
- <translation type="unfinished"/>
+ <translation>シェーダーキャッシュを消去</translation>
</message>
<message>
<location filename="../../src/yuzu/user_data_migration.cpp" line="66"/>
<source>Keep Old Data</source>
- <translation type="unfinished"/>
+ <translation>古いデータを保持</translation>
</message>
<message>
<location filename="../../src/yuzu/user_data_migration.cpp" line="67"/>
<source>Clear Old Data</source>
- <translation type="unfinished"/>
+ <translation>古いデータを消去</translation>
</message>
<message>
<location filename="../../src/yuzu/user_data_migration.cpp" line="68"/>
<source>Link Old Directory</source>
- <translation type="unfinished"/>
+ <translation>古いディレクトリをリンク</translation>
</message>
<message>
<location filename="../../src/yuzu/user_data_migration.cpp" line="98"/>
<source>
</source>
- <translation type="unfinished"/>
+ <translation>
+
+</translation>
</message>
<message>
<location filename="../../src/yuzu/user_data_migration.cpp" line="102"/>
<location filename="../../src/yuzu/user_data_migration.cpp" line="108"/>
<source>No</source>
- <translation type="unfinished"/>
+ <translation>いいえ</translation>
</message>
<message>
<location filename="../../src/yuzu/user_data_migration.cpp" line="122"/>
<source>You can manually re-trigger this prompt by deleting the new config directory:
%1</source>
- <translation type="unfinished"/>
+ <translation>新しい設定ディレクトリを削除すると、このメッセージを再度表示できます:
+%1</translation>
</message>
<message>
<location filename="../../src/yuzu/user_data_migration.cpp" line="133"/>
<source>Migrating</source>
- <translation type="unfinished"/>
+ <translation>移行中</translation>
</message>
<message>
<location filename="../../src/yuzu/user_data_migration.cpp" line="134"/>
<source>Migrating, this may take a while...</source>
- <translation type="unfinished"/>
+ <translation>移行中です。しばらくお待ちください...</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_touch_from_button.cpp" line="21"/>
@@ -8827,7 +8937,7 @@ p, li { white-space: pre-wrap; }
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="72"/>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="94"/>
<source>Start</source>
- <translation>開始</translation>
+ <translation>スタート</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="74"/>
@@ -8893,13 +9003,13 @@ p, li { white-space: pre-wrap; }
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="94"/>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="124"/>
<source>Share</source>
- <translation>Share</translation>
+ <translation>シェア</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="96"/>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="126"/>
<source>Options</source>
- <translation>Options</translation>
+ <translation>オプション</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="98"/>
@@ -8922,7 +9032,7 @@ p, li { white-space: pre-wrap; }
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="420"/>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="444"/>
<source>%1%2Hat %3</source>
- <translation type="unfinished"/>
+ <translation>%1%2十字キー %3</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="424"/>
@@ -8930,13 +9040,13 @@ p, li { white-space: pre-wrap; }
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="450"/>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="239"/>
<source>%1%2Axis %3</source>
- <translation type="unfinished"/>
+ <translation>%1%2スティック %3</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="430"/>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="219"/>
<source>%1%2Axis %3,%4,%5</source>
- <translation type="unfinished"/>
+ <translation>%1%2スティック %3,%4,%5</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_ringcon.cpp" line="434"/>
@@ -8979,12 +9089,12 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="82"/>
<source>Stick L</source>
- <translation>L スティック</translation>
+ <translation>Lスティック</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="84"/>
<source>Stick R</source>
- <translation>R スティック</translation>
+ <translation>Rスティック</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="96"/>
@@ -8994,7 +9104,7 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="98"/>
<source>Minus</source>
- <translation>-</translation>
+ <translation>−</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="100"/>
@@ -9010,7 +9120,7 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="130"/>
<source>Touch</source>
- <translation>タッチの設定</translation>
+ <translation>タッチ</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="132"/>
@@ -9036,7 +9146,7 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="140"/>
<source>Extra</source>
- <translation type="unfinished"/>
+ <translation>エクストラ</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="195"/>
@@ -9047,13 +9157,13 @@ p, li { white-space: pre-wrap; }
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="209"/>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="233"/>
<source>%1%2%3Hat %4</source>
- <translation type="unfinished"/>
+ <translation>%1%2%3十字キー %4</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="213"/>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="236"/>
<source>%1%2%3Axis %4</source>
- <translation type="unfinished"/>
+ <translation>%1%2%3スティック %4</translation>
</message>
<message>
<location filename="../../src/yuzu/configuration/configure_input_player.cpp" line="227"/>
@@ -9064,37 +9174,37 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/yuzu/multiplayer/chat_room.cpp" line="143"/>
<source>Not playing a game</source>
- <translation type="unfinished"/>
+ <translation>ゲームをプレイしていません</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/lobby_p.h" line="263"/>
<source>%1 is not playing a game</source>
- <translation>%1はゲームのプレイ中ではありません</translation>
+ <translation>%1 はゲームをプレイしていません</translation>
</message>
<message>
<location filename="../../src/yuzu/multiplayer/lobby_p.h" line="265"/>
<source>%1 is playing %2</source>
- <translation>%1は%2をプレイ中です</translation>
+ <translation>%1 は %2 をプレイ中です</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="95"/>
<source>Play Time: %1</source>
- <translation type="unfinished"/>
+ <translation>プレイ時間: %1</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="98"/>
<source>Never Played</source>
- <translation type="unfinished"/>
+ <translation>未プレイ</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="106"/>
<source>Version: %1</source>
- <translation type="unfinished"/>
+ <translation>バージョン: %1</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="108"/>
<source>Version: 1.0.0</source>
- <translation type="unfinished"/>
+ <translation>バージョン: 1.0.0</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="313"/>
@@ -9114,7 +9224,7 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="358"/>
<source>Add New Game Directory</source>
- <translation>新しいゲームディレクトリを追加する</translation>
+ <translation>新しいゲームディレクトリを追加</translation>
</message>
<message>
<location filename="../../src/qt_common/game_list/game_list_p.h" line="376"/>
@@ -9127,12 +9237,12 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/yuzu/applets/qt_amiibo_settings.ui" line="14"/>
<source>Amiibo Settings</source>
- <translation>Amiibo 設定</translation>
+ <translation>amiibo設定</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_amiibo_settings.ui" line="169"/>
<source>Amiibo Info</source>
- <translation>Amiibo 情報</translation>
+ <translation>amiibo情報</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_amiibo_settings.ui" line="177"/>
@@ -9152,7 +9262,7 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/yuzu/applets/qt_amiibo_settings.ui" line="242"/>
<source>Amiibo Data</source>
- <translation>Amiibo データ</translation>
+ <translation>amiiboデータ</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_amiibo_settings.ui" line="250"/>
@@ -9197,7 +9307,7 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/yuzu/applets/qt_amiibo_settings.ui" line="384"/>
<source>Mount Amiibo</source>
- <translation type="unfinished"/>
+ <translation>amiiboをマウント</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_amiibo_settings.ui" line="390"/>
@@ -9217,22 +9327,22 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/yuzu/applets/qt_amiibo_settings.cpp" line="236"/>
<source>The following amiibo data will be formatted:</source>
- <translation type="unfinished"/>
+ <translation>以下のamiiboデータを初期化します:</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_amiibo_settings.cpp" line="239"/>
<source>The following game data will removed:</source>
- <translation>以下のゲームデータを消去します:</translation>
+ <translation>以下のゲームデータを削除します:</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_amiibo_settings.cpp" line="242"/>
<source>Set nickname and owner:</source>
- <translation>オーナーとニックネームを設定:</translation>
+ <translation>ニックネームとオーナーを設定:</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_amiibo_settings.cpp" line="245"/>
<source>Do you wish to restore this amiibo?</source>
- <translation type="unfinished"/>
+ <translation>このamiiboを復元しますか?</translation>
</message>
</context>
<context>
@@ -9240,17 +9350,17 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/qt_common/util/content.cpp" line="30"/>
<source>Game Requires Firmware</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアが必要なゲーム</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="31"/>
<source>This game may require firmware to function properly, and you don&apos;t have any installed. Please install firmware before launching, or press &quot;OK&quot; to launch anyways.</source>
- <translation type="unfinished"/>
+ <translation>このゲームを正常に動作させるにはファームウェアが必要な場合がありますが、インストールされていません。起動前にファームウェアをインストールするか、「OK」を押してそのまま起動してください。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="44"/>
<source>Installing Firmware...</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアをインストール中...</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="44"/>
@@ -9259,22 +9369,22 @@ p, li { white-space: pre-wrap; }
<location filename="../../src/qt_common/util/content.cpp" line="380"/>
<location filename="../../src/qt_common/util/content.cpp" line="443"/>
<source>Cancel</source>
- <translation type="unfinished"/>
+ <translation>キャンセル</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="56"/>
<source>Firmware Install Failed</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアのインストールに失敗しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="57"/>
<source>Firmware Install Succeeded</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアのインストールに成功しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="165"/>
<source>Firmware integrity verification failed!</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアの整合性検証に失敗しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="166"/>
@@ -9282,241 +9392,246 @@ p, li { white-space: pre-wrap; }
<source>Verification failed for the following files:
%1</source>
- <translation type="unfinished"/>
+ <translation>以下のファイルの検証に失敗しました:
+
+%1</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="207"/>
<location filename="../../src/qt_common/util/content.cpp" line="263"/>
<source>Verifying integrity...</source>
- <translation type="unfinished"/>
+ <translation>整合性を検証中...</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="222"/>
<location filename="../../src/qt_common/util/content.cpp" line="281"/>
<source>Integrity verification succeeded!</source>
- <translation type="unfinished"/>
+ <translation>整合性の検証に成功しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="223"/>
<location filename="../../src/qt_common/util/content.cpp" line="282"/>
<source>The operation completed successfully.</source>
- <translation type="unfinished"/>
+ <translation>操作は正常に完了しました。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="226"/>
<location filename="../../src/qt_common/util/content.cpp" line="287"/>
<source>Integrity verification failed!</source>
- <translation type="unfinished"/>
+ <translation>整合性の検証に失敗しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="227"/>
<source>File contents may be corrupt or missing.</source>
- <translation type="unfinished"/>
+ <translation>ファイルの内容が破損しているか、不足している可能性があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="231"/>
<source>Integrity verification couldn&apos;t be performed</source>
- <translation type="unfinished"/>
+ <translation>整合性の検証を実行できませんでした</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="232"/>
<source>Firmware installation cancelled, firmware may be in a bad state or corrupted. File contents could not be checked for validity.</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアのインストールがキャンセルされました。ファームウェアが不完全または破損している可能性があります。ファイルの内容を検証できませんでした。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="239"/>
<source>Select Dumped Keys Location</source>
- <translation type="unfinished"/>
+ <translation>ダンプしたキーの場所を選択</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="252"/>
<source>Decryption Keys install succeeded</source>
- <translation type="unfinished"/>
+ <translation>復号キーのインストールに成功しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="255"/>
<source>Decryption Keys install failed</source>
- <translation type="unfinished"/>
+ <translation>復号キーのインストールに失敗しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="327"/>
<source>Orphaned Profiles Detected!</source>
- <translation type="unfinished"/>
+ <translation>孤立したプロファイルが検出されました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="328"/>
<source>UNEXPECTED BAD THINGS MAY HAPPEN IF YOU DON&apos;T READ THIS!&lt;br&gt;Eden has detected the following save directories with no attached profile:&lt;br&gt;%1&lt;br&gt;&lt;br&gt;The following profiles are valid:&lt;br&gt;%2&lt;br&gt;&lt;br&gt;Click &quot;OK&quot; to open your save folder and fix up your profiles.&lt;br&gt;Hint: copy the contents of the largest or last-modified folder elsewhere, delete all orphaned profiles, and move your copied contents to the good profile.&lt;br&gt;&lt;br&gt;Still confused? See the &lt;a href=&apos;https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/user/Orphaned.md&apos;&gt;help page&lt;/a&gt;.&lt;br&gt;</source>
- <translation type="unfinished"/>
+ <translation>【重要】必ずお読みください。予期せぬ問題が発生する恐れがあります。&lt;br&gt;プロファイルが紐付けられていない以下のセーブデータディレクトリが検出されました:&lt;br&gt;%1&lt;br&gt;&lt;br&gt;有効なプロファイルは以下のとおりです:&lt;br&gt;%2&lt;br&gt;&lt;br&gt;「OK」をクリックしてセーブデータフォルダーを開き、プロファイルを整理してください。&lt;br&gt;ヒント: 最もサイズが大きい、または更新日時が新しいフォルダーの中身を別の場所にコピーし、孤立したプロファイルをすべて削除してから、コピーした中身を有効なプロファイルのフォルダーに移動してください。&lt;br&gt;&lt;br&gt;ご不明な場合は、&lt;a href='https://git.eden-emu.dev/eden-emu/eden/src/branch/master/docs/user/Orphaned.md'&gt;ヘルプページ&lt;/a&gt;をご覧ください。&lt;br&gt;</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="347"/>
<source>Really clear data?</source>
- <translation type="unfinished"/>
+ <translation>本当にデータを消去しますか?</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="347"/>
<source>Important data may be lost!</source>
- <translation type="unfinished"/>
+ <translation>重要なデータが失われる可能性があります!</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="352"/>
<source>Are you REALLY sure?</source>
- <translation type="unfinished"/>
+ <translation>本当によろしいですか?</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="353"/>
<source>Once deleted, your data will NOT come back!
Only do this if you&apos;re 100% sure you want to delete this data.</source>
- <translation type="unfinished"/>
+ <translation>一度削除したデータは元に戻せません!
+このデータを削除しても問題ないと確信している場合のみ実行してください。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="360"/>
<source>Clearing...</source>
- <translation type="unfinished"/>
+ <translation>消去中...</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="374"/>
<source>Select Export Location</source>
- <translation type="unfinished"/>
+ <translation>エクスポート先を選択</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="374"/>
<source>%1.zip</source>
- <translation type="unfinished"/>
+ <translation>%1.zip</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="374"/>
<location filename="../../src/qt_common/util/content.cpp" line="428"/>
<location filename="../../src/qt_common/util/content.cpp" line="524"/>
<source>Zipped Archives (*.zip)</source>
- <translation type="unfinished"/>
+ <translation>ZIPアーカイブ (*.zip)</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="380"/>
<source>Exporting data. This may take a while...</source>
- <translation type="unfinished"/>
+ <translation>データをエクスポートしています。しばらくお待ちください...</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="382"/>
<source>Exporting</source>
- <translation type="unfinished"/>
+ <translation>エクスポート中</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="403"/>
<source>Exported Successfully</source>
- <translation type="unfinished"/>
+ <translation>エクスポート完了</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="403"/>
<source>Data was exported successfully.</source>
- <translation type="unfinished"/>
+ <translation>データを正常にエクスポートしました。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="405"/>
<source>Export Cancelled</source>
- <translation type="unfinished"/>
+ <translation>エクスポートがキャンセルされました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="405"/>
<source>Export was cancelled by the user.</source>
- <translation type="unfinished"/>
+ <translation>エクスポートはユーザーによってキャンセルされました。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="408"/>
<source>Export Failed</source>
- <translation type="unfinished"/>
+ <translation>エクスポートに失敗しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="409"/>
<source>Ensure you have write permissions on the targeted directory and try again.</source>
- <translation type="unfinished"/>
+ <translation>対象ディレクトリへの書き込み権限があることを確認して、もう一度お試しください。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="428"/>
<source>Select Import Location</source>
- <translation type="unfinished"/>
+ <translation>インポート元を選択</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="434"/>
<source>Import Warning</source>
- <translation type="unfinished"/>
+ <translation>インポートの警告</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="435"/>
<source>All previous data in this directory will be deleted. Are you sure you wish to proceed?</source>
- <translation type="unfinished"/>
+ <translation>このディレクトリ内の既存のデータはすべて削除されます。続行してもよろしいですか?</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="443"/>
<source>Importing data. This may take a while...</source>
- <translation type="unfinished"/>
+ <translation>データをインポートしています。しばらくお待ちください...</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="445"/>
<source>Importing</source>
- <translation type="unfinished"/>
+ <translation>インポート中</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="478"/>
<source>Imported Successfully</source>
- <translation type="unfinished"/>
+ <translation>インポート完了</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="478"/>
<source>Data was imported successfully.</source>
- <translation type="unfinished"/>
+ <translation>データを正常にインポートしました。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="480"/>
<source>Import Cancelled</source>
- <translation type="unfinished"/>
+ <translation>インポートがキャンセルされました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="480"/>
<source>Import was cancelled by the user.</source>
- <translation type="unfinished"/>
+ <translation>インポートはユーザーによってキャンセルされました。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="482"/>
<source>Import Failed</source>
- <translation type="unfinished"/>
+ <translation>インポートに失敗しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="483"/>
<source>Ensure you have read permissions on the targeted directory and try again.</source>
- <translation type="unfinished"/>
+ <translation>対象ディレクトリの読み取り権限があることを確認して、もう一度お試しください。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="500"/>
<source>Keys not installed</source>
- <translation type="unfinished"/>
+ <translation>キーがインストールされていません</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="501"/>
<source>Install decryption keys and restart Eden before attempting to install firmware.</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアをインストールする前に、復号キーをインストールしてEdenを再起動してください。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="513"/>
<source>Select Dumped Firmware Source Location</source>
- <translation type="unfinished"/>
+ <translation>ダンプしたファームウェアの場所を選択</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="524"/>
<source>Select Dumped Firmware ZIP</source>
- <translation type="unfinished"/>
+ <translation>ダンプしたファームウェアのZIPを選択</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="541"/>
<source>Firmware cleanup failed</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアのクリーンアップに失敗しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/content.cpp" line="542"/>
<source>Failed to clean up extracted firmware cache.
Check write permissions in the system temp directory and try again.
OS reported error: %1</source>
- <translation type="unfinished"/>
+ <translation>展開したファームウェアのキャッシュを削除できませんでした。
+システムの一時ディレクトリの書き込み権限を確認して、もう一度お試しください。
+OSのエラー: %1</translation>
</message>
</context>
<context>
@@ -9524,17 +9639,17 @@ OS reported error: %1</source>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="23"/>
<source>Linked Save Data</source>
- <translation type="unfinished"/>
+ <translation>セーブデータをリンクしました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="23"/>
<source>Save data has been linked.</source>
- <translation type="unfinished"/>
+ <translation>セーブデータをリンクしました。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="26"/>
<source>Failed to link save data</source>
- <translation type="unfinished"/>
+ <translation>セーブデータのリンクに失敗しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="27"/>
@@ -9542,76 +9657,81 @@ OS reported error: %1</source>
%1
To:
%2</source>
- <translation type="unfinished"/>
+ <translation>ディレクトリをリンクできませんでした:
+ %1
+リンク先:
+ %2</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="40"/>
<source>Already Linked</source>
- <translation type="unfinished"/>
+ <translation>すでにリンクされています</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="41"/>
<source>This title is already linked to Ryujinx. Would you like to unlink it?</source>
- <translation type="unfinished"/>
+ <translation>このタイトルはすでにRyujinxとリンクされています。リンクを解除しますか?</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="68"/>
<source>Failed to unlink old directory</source>
- <translation type="unfinished"/>
+ <translation>古いディレクトリのリンク解除に失敗しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="69"/>
<location filename="../../src/qt_common/util/fs.cpp" line="79"/>
<source>OS returned error: %1</source>
- <translation type="unfinished"/>
+ <translation>OSがエラーを返しました: %1</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="78"/>
<source>Failed to copy save data</source>
- <translation type="unfinished"/>
+ <translation>セーブデータのコピーに失敗しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="83"/>
<source>Unlink Successful</source>
- <translation type="unfinished"/>
+ <translation>リンクを解除しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="84"/>
<source>Successfully unlinked Ryujinx save data. Save data has been kept intact.</source>
- <translation type="unfinished"/>
+ <translation>Ryujinxのセーブデータとのリンクを解除しました。セーブデータはそのまま保持されています。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="97"/>
<source>Could not find Ryujinx installation</source>
- <translation type="unfinished"/>
+ <translation>Ryujinxのインストールが見つかりません</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="98"/>
<source>Could not find a valid Ryujinx installation. This may typically occur if you are using Ryujinx in portable mode.
Would you like to manually select a portable folder to use?</source>
- <translation type="unfinished"/>
+ <translation>有効なRyujinxのインストール環境が見つかりませんでした。Ryujinxをポータブルモードで使用している場合に発生することがあります。
+
+ポータブルフォルダーを手動で選択しますか?</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="105"/>
<source>Ryujinx Portable Location</source>
- <translation type="unfinished"/>
+ <translation>Ryujinxポータブルフォルダーの場所</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="122"/>
<source>Not a valid Ryujinx directory</source>
- <translation type="unfinished"/>
+ <translation>有効なRyujinxディレクトリではありません</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="123"/>
<source>The specified directory does not contain valid Ryujinx data.</source>
- <translation type="unfinished"/>
+ <translation>指定されたディレクトリに有効なRyujinxのデータが含まれていません。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/fs.cpp" line="141"/>
<location filename="../../src/qt_common/util/fs.cpp" line="145"/>
<source>Could not find Ryujinx save data</source>
- <translation type="unfinished"/>
+ <translation>Ryujinxのセーブデータが見つかりません</translation>
</message>
</context>
<context>
@@ -9619,17 +9739,17 @@ Would you like to manually select a portable folder to use?</source>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="192"/>
<source>Error Removing Contents</source>
- <translation type="unfinished"/>
+ <translation>コンテンツの削除エラー</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="194"/>
<source>Error Removing Update</source>
- <translation type="unfinished"/>
+ <translation>アップデートの削除エラー</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="196"/>
<source>Error Removing DLC</source>
- <translation type="unfinished"/>
+ <translation>DLCの削除エラー</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="207"/>
@@ -9639,196 +9759,196 @@ Would you like to manually select a portable folder to use?</source>
<location filename="../../src/qt_common/util/game.cpp" line="297"/>
<location filename="../../src/qt_common/util/game.cpp" line="322"/>
<source>Successfully Removed</source>
- <translation type="unfinished"/>
+ <translation>削除しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="208"/>
<source>Successfully removed the installed base game.</source>
- <translation type="unfinished"/>
+ <translation>インストール済みの本体ソフトを削除しました。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="213"/>
<source>The base game is not installed in the NAND and cannot be removed.</source>
- <translation type="unfinished"/>
+ <translation>本体ソフトがNANDにインストールされていないため、削除できません。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="221"/>
<source>Successfully removed the installed update.</source>
- <translation type="unfinished"/>
+ <translation>インストール済みのアップデートを削除しました。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="224"/>
<source>There is no update installed for this title.</source>
- <translation type="unfinished"/>
+ <translation>このタイトルにはアップデートがインストールされていません。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="232"/>
<source>There are no DLCs installed for this title.</source>
- <translation type="unfinished"/>
+ <translation>このタイトルにはDLCがインストールされていません。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="237"/>
<source>Successfully removed %1 installed DLC.</source>
- <translation type="unfinished"/>
+ <translation>インストール済みのDLCを%1個削除しました。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="258"/>
<location filename="../../src/qt_common/util/game.cpp" line="266"/>
<source>Error Removing Transferable Shader Cache</source>
- <translation type="unfinished"/>
+ <translation>転送可能なシェーダーキャッシュの削除エラー</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="259"/>
<location filename="../../src/qt_common/util/game.cpp" line="293"/>
<source>A shader cache for this title does not exist.</source>
- <translation type="unfinished"/>
+ <translation>このタイトルのシェーダーキャッシュは存在しません。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="264"/>
<source>Successfully removed the transferable shader cache.</source>
- <translation type="unfinished"/>
+ <translation>転送可能なシェーダーキャッシュを削除しました。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="267"/>
<source>Failed to remove the transferable shader cache.</source>
- <translation type="unfinished"/>
+ <translation>転送可能なシェーダーキャッシュを削除できませんでした。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="282"/>
<source>Error Removing Vulkan Driver Pipeline Cache</source>
- <translation type="unfinished"/>
+ <translation>Vulkanドライバーのパイプラインキャッシュの削除エラー</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="283"/>
<source>Failed to remove the driver pipeline cache.</source>
- <translation type="unfinished"/>
+ <translation>ドライバーのパイプラインキャッシュを削除できませんでした。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="292"/>
<location filename="../../src/qt_common/util/game.cpp" line="302"/>
<source>Error Removing Transferable Shader Caches</source>
- <translation type="unfinished"/>
+ <translation>転送可能なシェーダーキャッシュの削除エラー</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="298"/>
<source>Successfully removed the transferable shader caches.</source>
- <translation type="unfinished"/>
+ <translation>転送可能なシェーダーキャッシュを削除しました。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="303"/>
<source>Failed to remove the transferable shader cache directory.</source>
- <translation type="unfinished"/>
+ <translation>転送可能なシェーダーキャッシュのディレクトリを削除できませんでした。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="316"/>
<location filename="../../src/qt_common/util/game.cpp" line="325"/>
<source>Error Removing Custom Configuration</source>
- <translation type="unfinished"/>
+ <translation>個別設定の削除エラー</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="317"/>
<source>A custom configuration for this title does not exist.</source>
- <translation type="unfinished"/>
+ <translation>このタイトルの個別設定は存在しません。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="323"/>
<source>Successfully removed the custom game configuration.</source>
- <translation type="unfinished"/>
+ <translation>ゲームの個別設定を削除しました。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="326"/>
<source>Failed to remove the custom game configuration.</source>
- <translation type="unfinished"/>
+ <translation>ゲームの個別設定を削除できませんでした。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="347"/>
<source>Reset Metadata Cache</source>
- <translation type="unfinished"/>
+ <translation>メタデータキャッシュをリセット</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="353"/>
<source>The metadata cache is already empty.</source>
- <translation type="unfinished"/>
+ <translation>メタデータキャッシュはすでに空です。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="358"/>
<source>The operation completed successfully.</source>
- <translation type="unfinished"/>
+ <translation>操作は正常に完了しました。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="364"/>
<source>The metadata cache couldn&apos;t be deleted. It might be in use or non-existent.</source>
- <translation type="unfinished"/>
+ <translation>メタデータキャッシュを削除できませんでした。使用中か、存在しない可能性があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="381"/>
<source>Create Shortcut</source>
- <translation type="unfinished"/>
+ <translation>ショートカットを作成</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="381"/>
<source>Do you want to launch the game in fullscreen?</source>
- <translation type="unfinished"/>
+ <translation>ゲームをフルスクリーンで起動しますか?</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="385"/>
<source>Shortcut Created</source>
- <translation type="unfinished"/>
+ <translation>ショートカットを作成しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="385"/>
<source>Successfully created a shortcut to %1</source>
- <translation type="unfinished"/>
+ <translation>%1 へのショートカットを作成しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="390"/>
<source>Shortcut may be Volatile!</source>
- <translation type="unfinished"/>
+ <translation>ショートカットが無効になる可能性があります</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="391"/>
<source>This will create a shortcut to the current AppImage. This may not work well if you update. Continue?</source>
- <translation type="unfinished"/>
+ <translation>現在のAppImageへのショートカットを作成します。アップデートすると正常に動作しなくなる可能性があります。続行しますか?</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="397"/>
<source>Failed to Create Shortcut</source>
- <translation type="unfinished"/>
+ <translation>ショートカットの作成に失敗しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="398"/>
<source>Failed to create a shortcut to %1</source>
- <translation type="unfinished"/>
+ <translation>%1 へのショートカットを作成できませんでした</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="464"/>
<source>Create Icon</source>
- <translation type="unfinished"/>
+ <translation>アイコンを作成</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="465"/>
<source>Cannot create icon file. Path &quot;%1&quot; does not exist and cannot be created.</source>
- <translation type="unfinished"/>
+ <translation>アイコンファイルを作成できません。パス「%1」が存在せず、作成もできません。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="518"/>
<source>No firmware available</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアがありません</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="519"/>
<source>Please install firmware to use the home menu.</source>
- <translation type="unfinished"/>
+ <translation>ホームメニューを使用するには、ファームウェアをインストールしてください。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="525"/>
<source>Home Menu Applet</source>
- <translation type="unfinished"/>
+ <translation>ホームメニューアプレット</translation>
</message>
<message>
<location filename="../../src/qt_common/util/game.cpp" line="526"/>
<source>Home Menu is not available. Please reinstall firmware.</source>
- <translation type="unfinished"/>
+ <translation>ホームメニューを利用できません。ファームウェアを再インストールしてください。</translation>
</message>
</context>
<context>
@@ -9836,55 +9956,57 @@ Would you like to manually select a portable folder to use?</source>
<message>
<location filename="../../src/qt_common/util/mod.cpp" line="48"/>
<source>Mod Name</source>
- <translation type="unfinished"/>
+ <translation>MOD名</translation>
</message>
<message>
<location filename="../../src/qt_common/util/mod.cpp" line="48"/>
<source>What should this mod be called?</source>
- <translation type="unfinished"/>
+ <translation>このMODの名前を入力してください。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/mod.cpp" line="58"/>
<source>RomFS</source>
- <translation type="unfinished"/>
+ <translation>RomFS</translation>
</message>
<message>
<location filename="../../src/qt_common/util/mod.cpp" line="59"/>
<source>ExeFS/Patch</source>
- <translation type="unfinished"/>
+ <translation>ExeFS/パッチ</translation>
</message>
<message>
<location filename="../../src/qt_common/util/mod.cpp" line="60"/>
<source>Cheat</source>
- <translation type="unfinished"/>
+ <translation>チート</translation>
</message>
<message>
<location filename="../../src/qt_common/util/mod.cpp" line="64"/>
<source>Mod Type</source>
- <translation type="unfinished"/>
+ <translation>MODの種類</translation>
</message>
<message>
<location filename="../../src/qt_common/util/mod.cpp" line="65"/>
<source>Could not detect mod type automatically. Please manually specify the type of mod you downloaded.
Most mods are RomFS mods, but patches (.pchtxt) are typically ExeFS mods.</source>
- <translation type="unfinished"/>
+ <translation>MODの種類を自動で判別できませんでした。ダウンロードしたMODの種類を手動で指定してください。
+
+ほとんどのMODはRomFS MODですが、パッチ (.pchtxt) は通常ExeFS MODです。</translation>
</message>
<message>
<location filename="../../src/qt_common/util/mod.cpp" line="127"/>
<location filename="../../src/qt_common/util/mod.cpp" line="140"/>
<source>Mod Extract Failed</source>
- <translation type="unfinished"/>
+ <translation>MODの展開に失敗しました</translation>
</message>
<message>
<location filename="../../src/qt_common/util/mod.cpp" line="128"/>
<source>Failed to create temporary directory %1</source>
- <translation type="unfinished"/>
+ <translation>一時ディレクトリ %1 を作成できませんでした</translation>
</message>
<message>
<location filename="../../src/qt_common/util/mod.cpp" line="141"/>
<source>Zip file %1 is empty</source>
- <translation type="unfinished"/>
+ <translation>ZIPファイル %1 が空です</translation>
</message>
</context>
<context>
@@ -9892,12 +10014,12 @@ Most mods are RomFS mods, but patches (.pchtxt) are typically ExeFS mods.</sourc
<message>
<location filename="../../src/qt_common/util/path.cpp" line="19"/>
<source>Error Opening Shader Cache</source>
- <translation type="unfinished"/>
+ <translation>シェーダーキャッシュのオープンエラー</translation>
</message>
<message>
<location filename="../../src/qt_common/util/path.cpp" line="20"/>
<source>Failed to create or open shader cache for this title, ensure your app data directory has write permissions.</source>
- <translation type="unfinished"/>
+ <translation>このタイトルのシェーダーキャッシュを作成または開けませんでした。アプリデータディレクトリの書き込み権限を確認してください。</translation>
</message>
</context>
<context>
@@ -9905,153 +10027,159 @@ Most mods are RomFS mods, but patches (.pchtxt) are typically ExeFS mods.</sourc
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="70"/>
<source>Contains game save data. DO NOT REMOVE UNLESS YOU KNOW WHAT YOU&apos;RE DOING!</source>
- <translation type="unfinished"/>
+ <translation>ゲームのセーブデータが含まれています。内容を理解している場合を除き、削除しないでください。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="72"/>
<source>Contains Vulkan and OpenGL pipeline caches. Generally safe to remove.</source>
- <translation type="unfinished"/>
+ <translation>VulkanとOpenGLのパイプラインキャッシュが含まれています。通常は削除しても問題ありません。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="73"/>
<source>Contains updates and DLC for games.</source>
- <translation type="unfinished"/>
+ <translation>ゲームのアップデートとDLCが含まれています。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="74"/>
<source>Contains firmware and applet data.</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアとアプレットのデータが含まれています。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="75"/>
<source>Contains game mods, patches, and cheats.</source>
- <translation type="unfinished"/>
+ <translation>ゲームのMOD、パッチ、チートが含まれています。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="79"/>
<source>Decryption Keys were successfully installed</source>
- <translation type="unfinished"/>
+ <translation>復号キーをインストールしました</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="80"/>
<source>Unable to read key directory, aborting</source>
- <translation type="unfinished"/>
+ <translation>キーディレクトリを読み取れないため、中止します</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="81"/>
<source>One or more keys failed to copy.</source>
- <translation type="unfinished"/>
+ <translation>1つ以上のキーをコピーできませんでした。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="83"/>
<source>Verify your keys file has a .keys extension and try again.</source>
- <translation type="unfinished"/>
+ <translation>キーファイルの拡張子が .keys であることを確認して、もう一度お試しください。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="85"/>
<source>Decryption Keys failed to initialize. Check that your dumping tools are up to date and re-dump keys.</source>
- <translation type="unfinished"/>
+ <translation>復号キーを初期化できませんでした。ダンプツールが最新であることを確認し、キーをもう一度ダンプしてください。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="91"/>
<source>Successfully installed firmware version %1</source>
- <translation type="unfinished"/>
+ <translation>ファームウェア バージョン %1 をインストールしました</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="92"/>
<source>Unable to locate potential firmware NCA files</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアのNCAファイルが見つかりません</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="93"/>
<source>Failed to delete one or more firmware files.</source>
- <translation type="unfinished"/>
+ <translation>1つ以上のファームウェアファイルを削除できませんでした。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="94"/>
<source>One or more firmware files failed to copy into NAND.</source>
- <translation type="unfinished"/>
+ <translation>1つ以上のファームウェアファイルをNANDにコピーできませんでした。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="96"/>
<source>Firmware installation cancelled, firmware may be in a bad state or corrupted. Restart Eden or re-install firmware.</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアのインストールがキャンセルされました。ファームウェアが不完全または破損している可能性があります。Edenを再起動するか、ファームウェアを再インストールしてください。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="101"/>
<source>Firmware missing. Firmware is required to run certain games and use the Home Menu.</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアがありません。一部のゲームの実行とホームメニューの使用にはファームウェアが必要です。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="104"/>
<source>Firmware reported as present, but was unable to be read. Check for decryption keys and redump firmware if necessary.</source>
- <translation type="unfinished"/>
+ <translation>ファームウェアは存在しますが、読み取れませんでした。復号キーを確認し、必要に応じてファームウェアを再ダンプしてください。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="110"/>
<source>Eden has detected user data for the following emulators:</source>
- <translation type="unfinished"/>
+ <translation>以下のエミュレーターのユーザーデータが検出されました:</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="112"/>
<source>Would you like to migrate your data for use in Eden?
Select the corresponding button to migrate data from that emulator.
This may take a while.</source>
- <translation type="unfinished"/>
+ <translation>Edenで使用するためにデータを移行しますか?
+移行元のエミュレーターに対応するボタンを選択してください。
+完了までにしばらく時間がかかる場合があります。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="115"/>
<source>Clearing shader cache is recommended for all users.
Do not uncheck unless you know what you&apos;re doing.</source>
- <translation type="unfinished"/>
+ <translation>すべてのユーザーにシェーダーキャッシュのクリアを推奨します。
+内容を理解している場合を除き、チェックを外さないでください。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="119"/>
<source>Keeps the old data directory. This is recommended if you aren't
space-constrained and want to keep separate data for the old emulator.</source>
- <translation type="unfinished"/>
+ <translation>古いデータディレクトリを保持します。空き容量に余裕があり、
+旧エミュレーターのデータを別に残したい場合におすすめです。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="121"/>
<source>Deletes the old data directory.
This is recommended on devices with space constraints.</source>
- <translation type="unfinished"/>
+ <translation>古いデータディレクトリを削除します。
+空き容量が限られているデバイスにおすすめです。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="124"/>
<source>Creates a filesystem link between the old directory and Eden directory.
This is recommended if you want to share data between emulators.</source>
- <translation type="unfinished"/>
+ <translation>古いディレクトリとEdenのディレクトリの間にファイルシステムリンクを作成します。
+エミュレーター間でデータを共有したい場合におすすめです。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="129"/>
<source>Ryujinx title database does not exist.</source>
- <translation type="unfinished"/>
+ <translation>Ryujinxのタイトルデータベースが存在しません。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="130"/>
<source>Invalid header on Ryujinx title database.</source>
- <translation type="unfinished"/>
+ <translation>Ryujinxのタイトルデータベースのヘッダーが無効です。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="131"/>
<source>Invalid magic header on Ryujinx title database.</source>
- <translation type="unfinished"/>
+ <translation>Ryujinxのタイトルデータベースのマジックヘッダーが無効です。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="132"/>
<source>Invalid byte alignment on Ryujinx title database.</source>
- <translation type="unfinished"/>
+ <translation>Ryujinxのタイトルデータベースのバイトアライメントが無効です。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="133"/>
<source>No items found in Ryujinx title database.</source>
- <translation type="unfinished"/>
+ <translation>Ryujinxのタイトルデータベースに項目がありません。</translation>
</message>
<message>
<location filename="../../src/qt_common/qt_string_lookup.h" line="134"/>
<source>Title %1 not found in Ryujinx title database.</source>
- <translation type="unfinished"/>
+ <translation>タイトル %1 がRyujinxのタイトルデータベースに見つかりません。</translation>
</message>
</context>
<context>
@@ -10064,7 +10192,7 @@ This is recommended if you want to share data between emulators.</source>
<message>
<location filename="../../src/yuzu/applets/qt_controller.ui" line="129"/>
<source>Supported Controller Types:</source>
- <translation>サポートされているコントローラーのタイプ:</translation>
+ <translation>対応コントローラーの種類:</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_controller.ui" line="282"/>
@@ -10105,7 +10233,7 @@ This is recommended if you want to share data between emulators.</source>
<location filename="../../src/yuzu/applets/qt_controller.ui" line="2059"/>
<location filename="../../src/yuzu/applets/qt_controller.cpp" line="474"/>
<source>Dual Joycons</source>
- <translation>Joy-Con(L/R)</translation>
+ <translation>Joy-Con (L/R)</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_controller.ui" line="524"/>
@@ -10118,7 +10246,7 @@ This is recommended if you want to share data between emulators.</source>
<location filename="../../src/yuzu/applets/qt_controller.ui" line="2064"/>
<location filename="../../src/yuzu/applets/qt_controller.cpp" line="478"/>
<source>Left Joycon</source>
- <translation>Joy-Con(L)</translation>
+ <translation>Joy-Con (L)</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_controller.ui" line="529"/>
@@ -10131,7 +10259,7 @@ This is recommended if you want to share data between emulators.</source>
<location filename="../../src/yuzu/applets/qt_controller.ui" line="2069"/>
<location filename="../../src/yuzu/applets/qt_controller.cpp" line="482"/>
<source>Right Joycon</source>
- <translation>Joy-Con(R)</translation>
+ <translation>Joy-Con (R)</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_controller.ui" line="538"/>
@@ -10195,7 +10323,7 @@ This is recommended if you want to share data between emulators.</source>
<message>
<location filename="../../src/yuzu/applets/qt_controller.ui" line="2293"/>
<source>Docked</source>
- <translation>Docked</translation>
+ <translation>ドックモード</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_controller.ui" line="2313"/>
@@ -10251,7 +10379,7 @@ This is recommended if you want to share data between emulators.</source>
<message>
<location filename="../../src/yuzu/applets/qt_controller.ui" line="2538"/>
<source>Connected</source>
- <translation>接続の状態</translation>
+ <translation>接続済み</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_controller.ui" line="2552"/>
@@ -10276,32 +10404,32 @@ This is recommended if you want to share data between emulators.</source>
<message>
<location filename="../../src/yuzu/applets/qt_controller.ui" line="2653"/>
<source>Not enough controllers</source>
- <translation type="unfinished"/>
+ <translation>コントローラーが足りません</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_controller.cpp" line="490"/>
<source>GameCube Controller</source>
- <translation>ゲームキューブコントローラー</translation>
+ <translation>ゲームキューブ コントローラー</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_controller.cpp" line="499"/>
<source>Poke Ball Plus</source>
- <translation>モンスターボールプラス</translation>
+ <translation>モンスターボール Plus</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_controller.cpp" line="503"/>
<source>NES Controller</source>
- <translation>ファミコン・コントローラー</translation>
+ <translation>ファミコン コントローラー</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_controller.cpp" line="507"/>
<source>SNES Controller</source>
- <translation>スーパーファミコン・コントローラー</translation>
+ <translation>スーパーファミコン コントローラー</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_controller.cpp" line="511"/>
<source>N64 Controller</source>
- <translation>ニンテンドウ64・コントローラー</translation>
+ <translation>NINTENDO 64 コントローラー</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_controller.cpp" line="515"/>
@@ -10323,14 +10451,14 @@ This is recommended if you want to share data between emulators.</source>
<source>An error has occurred.
Please try again or contact the developer of the software.</source>
<translation>エラーが発生しました。
-もう一度試すか、開発者に報告してください。</translation>
+もう一度お試しいただくか、ソフトウェアの開発者にお問い合わせください。</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_error.cpp" line="47"/>
<source>An error occurred on %1 at %2.
Please try again or contact the developer of the software.</source>
- <translation>%1の%2でエラーが発生しました。
-再試行するか、ソフトウェアの開発者に連絡してください。</translation>
+ <translation>%1 %2 にエラーが発生しました。
+もう一度お試しいただくか、ソフトウェアの開発者にお問い合わせください。</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_error.cpp" line="62"/>
@@ -10364,73 +10492,73 @@ Please try again or contact the developer of the software.</source>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="169"/>
<source>Profile Creator</source>
- <translation type="unfinished"/>
+ <translation>プロファイルの作成</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="172"/>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="190"/>
<source>Profile Selector</source>
- <translation>プロファイル選択</translation>
+ <translation>プロファイルの選択</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="175"/>
<source>Profile Icon Editor</source>
- <translation>プロファイル アイコンエディタ</translation>
+ <translation>プロファイルアイコンエディター</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="178"/>
<source>Profile Nickname Editor</source>
- <translation>プロファイル ニックネームエディタ</translation>
+ <translation>プロファイルニックネームエディター</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="200"/>
<source>Who will receive the points?</source>
- <translation type="unfinished"/>
+ <translation>誰がポイントを受け取りますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="203"/>
<source>Who is using Nintendo eShop?</source>
- <translation>誰が Nintendo eShop を使用しますか?</translation>
+ <translation>誰がニンテンドーeショップを利用しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="206"/>
<source>Who is making this purchase?</source>
- <translation>誰が支払いを行いますか?</translation>
+ <translation>誰が購入しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="209"/>
<source>Who is posting?</source>
- <translation type="unfinished"/>
+ <translation>誰が投稿しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="212"/>
<source>Select a user to link to a Nintendo Account.</source>
- <translation>Nintendo アカウントと紐づけるユーザを選択してください.</translation>
+ <translation>ニンテンドーアカウントと連携するユーザーを選択してください。</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="215"/>
<source>Change settings for which user?</source>
- <translation>どのユーザの設定を変更しますか?</translation>
+ <translation>どのユーザーの設定を変更しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="218"/>
<source>Format data for which user?</source>
- <translation>どのユーザのデータをフォーマットしますか?</translation>
+ <translation>どのユーザーのデータを初期化しますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="221"/>
<source>Which user will be transferred to another console?</source>
- <translation>どのユーザを別のコンソールに転送しますか?</translation>
+ <translation>どのユーザーを別の本体に引っ越ししますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="224"/>
<source>Send save data for which user?</source>
- <translation>どのユーザにセーブデータを送信しますか?</translation>
+ <translation>どのユーザーのセーブデータを送りますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/applets/qt_profile_select.cpp" line="228"/>
<source>Select a user:</source>
- <translation>ユーザー選択:</translation>
+ <translation>ユーザーを選択:</translation>
</message>
</context>
<context>
@@ -10475,24 +10603,26 @@ p, li { white-space: pre-wrap; }
<message>
<location filename="../../src/yuzu/ryujinx_dialog.ui" line="14"/>
<source>Ryujinx Link</source>
- <translation type="unfinished"/>
+ <translation>Ryujinxリンク</translation>
</message>
<message>
<location filename="../../src/yuzu/ryujinx_dialog.ui" line="26"/>
<source>Linking save data to Ryujinx lets both Ryujinx and Eden reference the same save files for your games.
By selecting &quot;From Eden&quot;, previous save data stored in Ryujinx will be deleted, and vice versa for &quot;From Ryujinx&quot;.</source>
- <translation type="unfinished"/>
+ <translation>セーブデータをRyujinxとリンクすると、RyujinxとEdenの両方で同じセーブデータを共有できます。
+
+「Edenから」を選択するとRyujinx側の既存のセーブデータが上書き削除され、「Ryujinxから」を選択するとEden側の既存のセーブデータが上書き削除されます。</translation>
</message>
<message>
<location filename="../../src/yuzu/ryujinx_dialog.ui" line="40"/>
<source>From Eden</source>
- <translation type="unfinished"/>
+ <translation>Edenから</translation>
</message>
<message>
<location filename="../../src/yuzu/ryujinx_dialog.ui" line="47"/>
<source>From Ryujinx</source>
- <translation type="unfinished"/>
+ <translation>Ryujinxから</translation>
</message>
<message>
<location filename="../../src/yuzu/ryujinx_dialog.ui" line="54"/>
@@ -10502,12 +10632,12 @@ By selecting &quot;From Eden&quot;, previous save data stored in Ryujinx will be
<message>
<location filename="../../src/yuzu/ryujinx_dialog.cpp" line="37"/>
<source>Failed to link save data</source>
- <translation type="unfinished"/>
+ <translation>セーブデータのリンクに失敗しました</translation>
</message>
<message>
<location filename="../../src/yuzu/ryujinx_dialog.cpp" line="38"/>
<source>OS returned error: %1</source>
- <translation type="unfinished"/>
+ <translation>OSがエラーを返しました: %1</translation>
</message>
</context>
<context>
@@ -10523,27 +10653,27 @@ By selecting &quot;From Eden&quot;, previous save data stored in Ryujinx will be
<message>
<location filename="../../src/yuzu/set_play_time_dialog.ui" line="14"/>
<source>Set Play Time Data</source>
- <translation type="unfinished"/>
+ <translation>プレイ時間を設定</translation>
</message>
<message>
<location filename="../../src/yuzu/set_play_time_dialog.ui" line="25"/>
<source>Hours:</source>
- <translation type="unfinished"/>
+ <translation>時間:</translation>
</message>
<message>
<location filename="../../src/yuzu/set_play_time_dialog.ui" line="39"/>
<source>Minutes:</source>
- <translation type="unfinished"/>
+ <translation>分:</translation>
</message>
<message>
<location filename="../../src/yuzu/set_play_time_dialog.ui" line="53"/>
<source>Seconds:</source>
- <translation type="unfinished"/>
+ <translation>秒:</translation>
</message>
<message>
<location filename="../../src/yuzu/set_play_time_dialog.cpp" line="49"/>
<source>Total play time reached maximum.</source>
- <translation type="unfinished"/>
+ <translation>合計プレイ時間が上限に達しました。</translation>
</message>
</context>
<context>
@@ -10551,92 +10681,93 @@ By selecting &quot;From Eden&quot;, previous save data stored in Ryujinx will be
<message>
<location filename="../../src/yuzu/updater/update_dialog.ui" line="14"/>
<source>Update Available</source>
- <translation type="unfinished"/>
+ <translation>アップデートがあります</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.ui" line="20"/>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="31"/>
<source>&lt;a href=&quot;%1&quot;&gt;View on Forgejo&lt;/a&gt;</source>
- <translation type="unfinished"/>
+ <translation>&lt;a href=&quot;%1&quot;&gt;Forgejoで表示&lt;/a&gt;</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.ui" line="33"/>
<source>Would you like to install this update?</source>
- <translation type="unfinished"/>
+ <translation>このアップデートをインストールしますか?</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.ui" line="50"/>
<source>Available Versions</source>
- <translation type="unfinished"/>
+ <translation>利用可能なバージョン</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.ui" line="58"/>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="29"/>
<source>%1 is available for download.</source>
- <translation type="unfinished"/>
+ <translation>%1 をダウンロードできます。</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="80"/>
<source>New Version Location</source>
- <translation type="unfinished"/>
+ <translation>新バージョンの保存先</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="82"/>
<source>All Files (*.*)</source>
- <translation type="unfinished"/>
+ <translation>すべてのファイル (*.*)</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="90"/>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="130"/>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="145"/>
<source>Failed to save file</source>
- <translation type="unfinished"/>
+ <translation>ファイルの保存に失敗しました</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="91"/>
<source>Could not open file %1 for writing.</source>
- <translation type="unfinished"/>
+ <translation>ファイル %1 を書き込み用に開けませんでした。</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="113"/>
<source>Downloading...</source>
- <translation type="unfinished"/>
+ <translation>ダウンロード中...</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="113"/>
<source>Cancel</source>
- <translation type="unfinished"/>
+ <translation>キャンセル</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="131"/>
<source>Could not write to file %1.</source>
- <translation type="unfinished"/>
+ <translation>ファイル %1 に書き込めませんでした。</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="146"/>
<source>Could not commit to file %1.</source>
- <translation type="unfinished"/>
+ <translation>ファイル %1 にコミットできませんでした。</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="158"/>
<source>Failed to download file</source>
- <translation type="unfinished"/>
+ <translation>ファイルのダウンロードに失敗しました</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="159"/>
<source>Could not download from %1%2
Error code: %3</source>
- <translation type="unfinished"/>
+ <translation>%1%2 からダウンロードできませんでした。
+エラーコード: %3</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="172"/>
<source>Download Complete</source>
- <translation type="unfinished"/>
+ <translation>ダウンロード完了</translation>
</message>
<message>
<location filename="../../src/yuzu/updater/update_dialog.cpp" line="173"/>
<source>Successfully downloaded %1. Would you like to open it?</source>
- <translation type="unfinished"/>
+ <translation>%1 をダウンロードしました。今すぐ開きますか?</translation>
</message>
</context>
-</TS> \ No newline at end of file
+</TS>
diff --git a/docs/performance/MKW-INTEGRATION.md b/docs/performance/MKW-INTEGRATION.md
new file mode 100644
index 0000000000..72257c61aa
--- /dev/null
+++ b/docs/performance/MKW-INTEGRATION.md
@@ -0,0 +1,72 @@
+# Mario Kart World integration test
+
+Base: upstream `nx2` at `0da830e94` (updated November 2, 2026).
+
+This branch keeps upstream's current command-list copies, shader support, ASTC
+transcode fix, fiber allocation changes, and game-loading and amiibo fixes.
+It applies the remaining changes independently so they can be reviewed separately.
+
+From the snowstormxd performance fork:
+
+- Execute native macro parameters without a second FIFO copy.
+- Keep register-write history optional in release builds.
+- Reuse unchanged small uniform allocations within one live scheduler tick.
+- Batch shader invalidations, preserving every supplied write range.
+- Clear only the descriptor bitmap words exposed by the current table limit.
+
+Adapted from [Selevet performance build 4](https://github.com/sylvieistaken/selevet-eglantine/releases/tag/selevet-eglantine-performance-4):
+
+- Submit native graphics work every 128 draws to overlap GPU execution with CPU
+ recording. Other titles retain their existing submission threshold.
+- Use the current draw's actual element count when deciding whether asynchronous
+ shader compilation may skip it. Indirect counts stay unknown rather than using
+ an earlier draw's count.
+- Traverse cache and scheduler bitmaps by skipping empty words. Wider priority
+ bitsets retain the generic traversal. The slot iterator also handles an empty
+ first bitmap word correctly.
+
+Handheld mode, ScaleForce, and FXAA are the defaults. Explicit saved preferences
+are retained. Accepting the startup recommendations saves the requested settings
+and reapplies the MKW preset after per-game configuration is loaded. Declining or
+closing the prompt preserves the user's settings.
+
+The kernel TLS implementation is compiled without LTO on GCC and Clang LTO builds
+so its lookups remain outside callers that can switch between migrating fibers.
+This restores MKW startup in the tested Windows build. Non-LTO builds keep their
+existing flags. The changes use portable timing and synchronization paths.
+
+The Windows hybrid timer, Windows RAM trimming and process-priority changes are
+not included. Unsafe preemptive reads, compute-acquire bypasses, and guest SDK VRR
+patching are also excluded. Current upstream command-list copying is retained
+instead of Selevet's older command-readback implementation.
+
+## Validation
+
+The regression tests exercise the real macro JIT with FIFO reallocations, uniform
+data mutation and session resets, descriptor-table growth and invalidation,
+batched shader removal against sequential invalidation, sparse slot iteration,
+and priority ordering against an independent queue model. Priority tests include
+both 64- and 128-priority queues.
+
+The integration also retains the calibration-worker lifetime fix, repairs stale
+memory/timer test fixtures, and defines kernel TLS out of line for ThinLTO.
+Loose RomFS directory lookup accepts mixed-case names on case-sensitive hosts;
+NUP tests check that the base version metadata and archive checksum agree across
+Windows and Linux.
+
+The Windows test package uses Clang 23.1.4, the checksum-pinned Eden v020525 PGO
+profile, and ThinLTO with backend optimization disabled (`--lto-O0`), matching the
+working startup test. Selevet's MSVC-ABI profile is not substituted for the MinGW
+build's profile. Link-time optimization is a build option; other platforms can
+continue using their usual build configuration.
+
+Compare the same course and route against the prior build, using a copy of the
+same shader cache, game version, and settings. Check item icons, geometry,
+collision, audio, hitches, and stopping/closing. Asynchronous shaders can delay
+the appearance of new objects while their pipelines compile. A lower CPU counter
+alone does not establish an FPS gain. No locked-60-FPS result is claimed.
+
+`EDEN_NX2_FLUSH_DRAWS=4096` restores the previous native submission threshold for
+a controlled comparison. `EDEN_NX2_UBOREUSE=0` disables uniform allocation reuse.
+
+Assisted by Codex.
diff --git a/docs/user/Basics.md b/docs/user/Basics.md
index 2b7a99c8d0..bc7f8c7f0d 100644
--- a/docs/user/Basics.md
+++ b/docs/user/Basics.md
@@ -64,4 +64,4 @@ Then just running `chmod +x script.sh && source script.sh`.
## Compatibility list
-Eden doesn't maintain a compatibility list. However, [EmuReady](https://www.emuready.com/) has a more fine-grained compatibility information for multiple emulators/forks as well.
+Eden doesn't maintain a compatibility list. However, [EmuReady](https://www.emuready.com/) has more fine-grained compatibility information for multiple emulators/forks as well.
diff --git a/docs/user/QuickStart.md b/docs/user/QuickStart.md
index 4e553d07c9..0e73a6dc46 100644
--- a/docs/user/QuickStart.md
+++ b/docs/user/QuickStart.md
@@ -1,6 +1,6 @@
# Eden Quick Start
-Use this guide to get starting using the Eden emulator.
+Use this guide to get started using the Eden emulator.
**[See here](https://evilperson1337.notion.site/Eden-Quick-Start-2b057c2edaf6817b9859d8bcdb474017) for an illustrated guide.**
diff --git a/docs/user/Troubleshoot.md b/docs/user/Troubleshoot.md
index 76d69e38f9..65b66e5146 100644
--- a/docs/user/Troubleshoot.md
+++ b/docs/user/Troubleshoot.md
@@ -4,7 +4,7 @@
- Ensure you have the latest drivers
- Uninstall old drivers, for Windows you can use [Display Driver Uninstaller](https://www.guru3d.com/download/display-driver-uninstaller-download/)
-- Change backend manually in the settings file (set it from `0` to `1` or viceversa).
+- Change backend manually in the settings file (set it from `0` to `1` or vice versa).
- Disconnect your second monitor, if any
## This mod only works on an Emulator
diff --git a/externals/cmake-modules/DetectArchitecture.cmake b/externals/cmake-modules/DetectArchitecture.cmake
index 105963c8c2..be84109994 100644
--- a/externals/cmake-modules/DetectArchitecture.cmake
+++ b/externals/cmake-modules/DetectArchitecture.cmake
@@ -36,12 +36,17 @@ This file is based off of Yuzu and Dynarmic.
# should NOT be too dependent on the architecture
# otherwise, you may end up with duplicate code
if (CMAKE_OSX_ARCHITECTURES)
- set(MULTIARCH_BUILD 1)
+ list(LENGTH CMAKE_OSX_ARCHITECTURES architecture_count)
+ if (architecture_count GREATER 1)
+ set(MULTIARCH_BUILD 1)
+ else()
+ set(MULTIARCH_BUILD 0)
+ endif()
set(ARCHITECTURE "${CMAKE_OSX_ARCHITECTURES}")
# hope and pray the architecture names match
- foreach(ARCH IN ${CMAKE_OSX_ARCHITECTURES})
- set(ARCHITECTURE_${ARCH} 1 PARENT_SCOPE)
+ foreach(ARCH IN LISTS CMAKE_OSX_ARCHITECTURES)
+ set(ARCHITECTURE_${ARCH} 1)
add_definitions(-DARCHITECTURE_${ARCH}=1)
endforeach()
@@ -218,4 +223,4 @@ if (NOT DEFINED ARCHITECTURE)
add_definitions(-DARCHITECTURE_GENERIC=1)
endif()
-message(STATUS "[DetectArchitecture] Target architecture: ${ARCHITECTURE}") \ No newline at end of file
+message(STATUS "[DetectArchitecture] Target architecture: ${ARCHITECTURE}")
diff --git a/externals/ffmpeg/CMakeLists.txt b/externals/ffmpeg/CMakeLists.txt
index f1bf099c10..890de4303c 100644
--- a/externals/ffmpeg/CMakeLists.txt
+++ b/externals/ffmpeg/CMakeLists.txt
@@ -179,7 +179,13 @@ elseif (MANAGARM)
endif()
if (YUZU_USE_BUNDLED_FFMPEG)
- AddJsonPackage(ffmpeg-ci)
+ # Windows (MSVC) and Linux x86_64 get the same FFmpeg with libdav1d, for Switch 2 titles' AV1
+ # movies; crueter-ci's packages, used elsewhere, have no AV1 decoder.
+ if ((MSVC OR LINUX) AND CPMUTIL_AMD64)
+ AddJsonPackage(ffmpeg-ci-av1)
+ else()
+ AddJsonPackage(ffmpeg-ci)
+ endif()
set(FFmpeg_INCLUDE_DIR
"${FFmpeg_SOURCE_DIR}/include;${FFmpeg_HWACCEL_INCLUDE_DIRS}"
diff --git a/shell.nix b/shell.nix
index 786b94ccb2..384712ea88 100755
--- a/shell.nix
+++ b/shell.nix
@@ -3,25 +3,31 @@
# SPDX-License-Identifier: GPL-3.0-or-later
let
- nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05";
+ nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-25.05";
pkgs = import nixpkgs { config = {}; overlays = []; };
in
pkgs.mkShellNoCC {
packages = with pkgs; [
# essential programs
- git cmake clang gnumake patch jq pkg-config
+ git cmake clang gnumake ninja patch jq pkg-config
# libraries
- openssl boost fmt nlohmann_json lz4 zlib zstd
+ openssl boost fmt_11 nlohmann_json lz4 zlib zstd
enet libopus vulkan-headers vulkan-utility-libraries
- spirv-tools spirv-headers vulkan-loader unzip
+ spirv-tools spirv-headers vulkan-loader mesa unzip
glslang python3 httplib cpp-jwt ffmpeg-headless
libusb1 cubeb
# eden
- qt6.qtbase qt6.qtmultimedia qt6.qtwayland qt6.qttools
+ qt6.qtbase qt6.qtmultimedia qt6.qtwayland qt6.qttools qt6.qtcharts
qt6.qtwebengine qt6.qt5compat
# eden-cli
- SDL3
+ sdl3
# optional components
discord-rpc gamemode
];
+ # Host /usr Vulkan drivers can't load inside the Nix shell (their libdrm/libz deps
+ # aren't visible to the Nix dynamic linker), so point the loader at Nix's RADV.
+ VK_DRIVER_FILES = "${pkgs.mesa}/share/vulkan/icd.d/radeon_icd.x86_64.json";
+ shellHook = ''
+ export CMAKE_PREFIX_PATH="$(echo $nativeBuildInputs $buildInputs | tr ' ' ':')"
+ '';
}
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index a2abc7d1b3..c49e252ccb 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -178,6 +178,13 @@ else()
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-unused-parameter>
$<$<COMPILE_LANGUAGE:C,CXX>:-Wno-missing-field-initializers>)
+ if (ENABLE_WERROR AND (NOT MSVC))
+ add_compile_options(
+ $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-error=unused>
+ $<$<COMPILE_LANGUAGE:C,CXX>:-Wno-error=unused-function>
+ )
+ endif()
+
if (CXX_CLANG OR CXX_ICC OR CXX_APPLE) # Clang, AppleClang, or Intel C++
if (NOT MSVC)
add_compile_options(
diff --git a/src/audio_core/audio_render_manager.cpp b/src/audio_core/audio_render_manager.cpp
index 8c4e74c4be..4034de1ec3 100644
--- a/src/audio_core/audio_render_manager.cpp
+++ b/src/audio_core/audio_render_manager.cpp
@@ -33,6 +33,8 @@ SystemManager& Manager::GetSystemManager() {
Result Manager::GetWorkBufferSize(const AudioRendererParameterInternal& params,
u64& out_count) const {
if (!CheckValidRevision(params.revision)) {
+ LOG_ERROR(Service_Audio, "Audio renderer revision {:#x} ({}) is not supported",
+ params.revision, GetRevisionNum(params.revision));
return Service::Audio::ResultInvalidRevision;
}
diff --git a/src/audio_core/common/feature_support.h b/src/audio_core/common/feature_support.h
index 4585be45ef..d6571c0661 100644
--- a/src/audio_core/common/feature_support.h
+++ b/src/audio_core/common/feature_support.h
@@ -17,6 +17,16 @@
namespace AudioCore {
constexpr u32 CurrentRevision = 15;
+constexpr u32 NativeCurrentRevision = 16;
+
+constexpr bool IsNativeRevision(u32 revision) {
+ return (revision & 0xffffff) == Common::MakeMagic('R', 'E', 'v', '\0');
+}
+
+constexpr u32 EncodeRevision(u32 revision, bool native) {
+ return Common::MakeMagic('R', 'E', native ? 'v' : 'V',
+ static_cast<char>('0' + revision));
+}
enum class SupportTags {
CommandProcessingTimeEstimatorVersion4,
@@ -57,12 +67,15 @@ enum class SupportTags {
};
constexpr u32 GetRevisionNum(u32 user_revision) {
- if (user_revision >= 0x100) {
- user_revision -= Common::MakeMagic('R', 'E', 'V', '0');
- user_revision >>= 24;
+ if (user_revision < 0x100) {
+ return user_revision;
}
-
- return user_revision;
+ if (!IsNativeRevision(user_revision) &&
+ (user_revision & 0xffffff) != Common::MakeMagic('R', 'E', 'V', '\0')) {
+ return ~u32{0};
+ }
+ const u32 encoded = user_revision >> 24;
+ return encoded >= '0' ? encoded - '0' : ~u32{0};
};
constexpr bool CheckFeatureSupported(SupportTags tag, u32 user_revision) {
@@ -108,11 +121,12 @@ constexpr bool CheckFeatureSupported(SupportTags tag, u32 user_revision) {
return false;
}
user_revision = GetRevisionNum(user_revision);
- return (*feature).second <= user_revision;
+ return user_revision != ~u32{0} && (*feature).second <= user_revision;
}
constexpr bool CheckValidRevision(u32 user_revision) {
- return GetRevisionNum(user_revision) <= CurrentRevision;
+ return GetRevisionNum(user_revision) <=
+ (IsNativeRevision(user_revision) ? NativeCurrentRevision : CurrentRevision);
};
} // namespace AudioCore
diff --git a/src/audio_core/renderer/behavior/behavior_info.cpp b/src/audio_core/renderer/behavior/behavior_info.cpp
index 63b80c503a..983cfd5b85 100644
--- a/src/audio_core/renderer/behavior/behavior_info.cpp
+++ b/src/audio_core/renderer/behavior/behavior_info.cpp
@@ -16,8 +16,7 @@ u32 BehaviorInfo::GetProcessRevisionNum() const {
}
u32 BehaviorInfo::GetProcessRevision() const {
- return Common::MakeMagic('R', 'E', 'V',
- static_cast<char>(static_cast<u8>('0') + process_revision));
+ return EncodeRevision(process_revision, native_platform);
}
u32 BehaviorInfo::GetUserRevisionNum() const {
@@ -25,11 +24,12 @@ u32 BehaviorInfo::GetUserRevisionNum() const {
}
u32 BehaviorInfo::GetUserRevision() const {
- return Common::MakeMagic('R', 'E', 'V',
- static_cast<char>(static_cast<u8>('0') + user_revision));
+ return EncodeRevision(user_revision, native_platform);
}
void BehaviorInfo::SetUserLibRevision(const u32 user_revision_) {
+ native_platform = IsNativeRevision(user_revision_);
+ process_revision = native_platform ? NativeCurrentRevision : CurrentRevision;
user_revision = GetRevisionNum(user_revision_);
}
diff --git a/src/audio_core/renderer/behavior/behavior_info.h b/src/audio_core/renderer/behavior/behavior_info.h
index 757f163c5a..318d4e1ff4 100644
--- a/src/audio_core/renderer/behavior/behavior_info.h
+++ b/src/audio_core/renderer/behavior/behavior_info.h
@@ -400,6 +400,7 @@ public:
u32 process_revision;
/// User version
u32 user_revision{};
+ bool native_platform{};
/// Behaviour flags
Flags flags{};
/// Errors generated and reported during Update
diff --git a/src/audio_core/renderer/behavior/info_updater.cpp b/src/audio_core/renderer/behavior/info_updater.cpp
index ca3eaece94..a8fbcf9730 100644
--- a/src/audio_core/renderer/behavior/info_updater.cpp
+++ b/src/audio_core/renderer/behavior/info_updater.cpp
@@ -122,12 +122,21 @@ Result InfoUpdater::UpdateVoices(VoiceContext& voice_context,
u8 flush_wave_buffer_count;
u16 reserved2;
VoiceInfo::Flags flags;
+ u8 reserved_after_flags;
SrcQuality src_quality;
+ u8 crossfade_curve;
u32 external_ctx;
u32 external_ctx_size;
u32 reserved3[2];
};
+ static_assert(sizeof(VoiceInParameterV2) == 0x188);
+ static_assert(offsetof(VoiceInParameterV2, crossfade_curve) == 0x177);
const auto* vin = reinterpret_cast<const VoiceInParameterV2*>(input + i * in_stride);
+ if (behaviour.GetUserRevisionNum() >= 16 && vin->in_use && vin->crossfade_curve != 0) {
+ LOG_ERROR(Service_Audio, "Unsupported voice biquad crossfade curve {}",
+ vin->crossfade_curve);
+ return Service::Audio::ResultInvalidUpdateInfo;
+ }
local_in.id = vin->id;
local_in.node_id = vin->node_id;
local_in.is_new = vin->is_new;
diff --git a/src/audio_core/renderer/sink/circular_buffer_sink_info.cpp b/src/audio_core/renderer/sink/circular_buffer_sink_info.cpp
index 0ede02b6b4..7e25413fe1 100644
--- a/src/audio_core/renderer/sink/circular_buffer_sink_info.cpp
+++ b/src/audio_core/renderer/sink/circular_buffer_sink_info.cpp
@@ -3,7 +3,6 @@
#include "audio_core/renderer/memory/pool_mapper.h"
#include "audio_core/renderer/sink/circular_buffer_sink_info.h"
-#include "audio_core/renderer/upsampler/upsampler_manager.h"
namespace AudioCore::Renderer {
@@ -17,13 +16,9 @@ CircularBufferSinkInfo::CircularBufferSinkInfo() {
}
void CircularBufferSinkInfo::CleanUp() {
- auto state_{reinterpret_cast<DeviceState*>(state.data())};
-
- if (state_->upsampler_info) {
- state_->upsampler_info->manager->Free(state_->upsampler_info);
- state_->upsampler_info = nullptr;
- }
-
+ // Only device sinks own an upsampler. This sink's state is a CircularBufferState, whose
+ // first words are buffer positions: reading them as DeviceState::upsampler_info freed a
+ // garbage pointer when a game changed or removed its circular buffer sink.
parameter.fill(0);
type = Type::Invalid;
}
diff --git a/src/common/CMakeLists.txt b/src/common/CMakeLists.txt
index d3e7b03cd2..76c5019788 100644
--- a/src/common/CMakeLists.txt
+++ b/src/common/CMakeLists.txt
@@ -152,7 +152,7 @@ add_library(
set_source_files_properties(zbic_compression.cpp PROPERTIES
INCLUDE_DIRECTORIES "${ZBIC_INCLUDE_DIR}"
- COMPILE_OPTIONS "$<$<CXX_COMPILER_ID:Clang,GNU>:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>;$<$<CXX_COMPILER_ID:MSVC>:/wd4505;/wd5245>")
+ COMPILE_OPTIONS "$<$<CXX_COMPILER_ID:AppleClang,Clang,GNU>:-Wno-unused-function;-Wno-missing-declarations;-Wno-shadow>;$<$<CXX_COMPILER_ID:MSVC>:/wd4505;/wd5245>")
if(WIN32)
target_sources(common PRIVATE windows/timer_resolution.cpp
diff --git a/src/common/fiber.cpp b/src/common/fiber.cpp
index 0e7f28c5b9..613ac97e22 100644
--- a/src/common/fiber.cpp
+++ b/src/common/fiber.cpp
@@ -25,8 +25,10 @@ struct Fiber::FiberImpl {
FiberImpl() {}
u32 canary_1 = CANARY_VALUE;
- std::array<u8, DEFAULT_STACK_SIZE> stack{};
- std::array<u8, DEFAULT_STACK_SIZE> rewind_stack{};
+ // Left uninitialized: the host gives a stack's pages memory only as they are used. Zero-filled
+ // std::arrays (and a second stack nothing switched to) made every fiber, one per guest thread,
+ // hold 4 MiB: ~750 MiB for title 1's ~190 threads.
+ std::unique_ptr<u8[]> stack;
u32 canary_2 = CANARY_VALUE;
boost::context::detail::fcontext_t context{};
@@ -38,7 +40,6 @@ struct Fiber::FiberImpl {
std::shared_ptr<Fiber> previous_fiber;
u8* stack_limit = nullptr;
- u8* rewind_stack_limit = nullptr;
bool is_thread_fiber = false;
bool released = false;
};
@@ -49,10 +50,10 @@ void Fiber::SetRewindPoint(std::function<void()>&& rewind_func) {
Fiber::Fiber(std::function<void()>&& entry_point_func) : impl{std::make_unique<FiberImpl>()} {
impl->entry_point = std::move(entry_point_func);
- impl->stack_limit = impl->stack.data();
- impl->rewind_stack_limit = impl->rewind_stack.data();
+ impl->stack.reset(new u8[DEFAULT_STACK_SIZE]);
+ impl->stack_limit = impl->stack.get();
u8* stack_base = impl->stack_limit + DEFAULT_STACK_SIZE;
- impl->context = boost::context::detail::make_fcontext(stack_base, impl->stack.size(), [](boost::context::detail::transfer_t transfer) -> void {
+ impl->context = boost::context::detail::make_fcontext(stack_base, DEFAULT_STACK_SIZE, [](boost::context::detail::transfer_t transfer) -> void {
auto* fiber = static_cast<Fiber*>(transfer.data);
ASSERT(fiber && fiber->impl && fiber->impl->previous_fiber && fiber->impl->previous_fiber->impl);
ASSERT(fiber->impl->canary_1 == CANARY_VALUE);
diff --git a/src/common/game_presets.h b/src/common/game_presets.h
new file mode 100644
index 0000000000..83dd4d3c3f
--- /dev/null
+++ b/src/common/game_presets.h
@@ -0,0 +1,37 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "common/settings.h"
+
+namespace Settings {
+
+inline constexpr u64 MarioKartWorldProgramId = 0x0400C3F00006E000ULL;
+
+inline void SetMarioKartWorldRecommendedSettings(Values& settings, bool global) {
+ const auto session = [global](auto& setting, auto value) {
+ setting.SetGlobal(global);
+ setting.SetValue(value);
+ };
+ session(settings.renderer_backend, RendererBackend::Vulkan);
+ session(settings.resolution_setup, ResolutionSetup::Res1X);
+ session(settings.gpu_accuracy, GpuAccuracy::Low);
+ session(settings.use_asynchronous_shaders, true);
+ session(settings.astc_recompression, AstcRecompression::Bc3);
+ session(settings.vram_usage_mode, VramUsageMode::Conservative);
+ session(settings.use_reactive_flushing, false);
+ session(settings.vsync_mode, VSyncMode::FifoRelaxed);
+ session(settings.use_docked_mode, ConsoleMode::Handheld);
+ session(settings.scaling_filter, ScalingFilter::ScaleForce);
+ session(settings.anti_aliasing, AntiAliasing::Fxaa);
+}
+
+inline bool ApplyMarioKartWorldPreset(Values& settings, u64 program_id, bool accepted) {
+ if (!accepted || program_id != MarioKartWorldProgramId) {
+ return false;
+ }
+ SetMarioKartWorldRecommendedSettings(settings, false);
+ return true;
+}
+
+} // namespace Settings
diff --git a/src/common/host_memory.cpp b/src/common/host_memory.cpp
index 75a9da80e9..2ef1982cbc 100644
--- a/src/common/host_memory.cpp
+++ b/src/common/host_memory.cpp
@@ -51,11 +51,13 @@
#endif // ^^^ POSIX ^^^
+#include <cstdlib>
#include <mutex>
#include <random>
#include "common/alignment.h"
#include "common/assert.h"
+#include "common/div_ceil.h"
#include "common/free_region_manager.h"
#include "common/host_memory.h"
#include "common/logging.h"
@@ -517,6 +519,14 @@ public:
, virtual_size{virtual_size_}
{}
+#ifdef __linux__
+ bool PunchHole(size_t offset, size_t length) {
+ return fd >= 0 &&
+ fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, static_cast<off_t>(offset),
+ static_cast<off_t>(length)) == 0;
+ }
+#endif
+
bool Init() {
long page_size = sysconf(_SC_PAGESIZE);
ASSERT_MSG(page_size == 0x1000, "page size {:#x} is incompatible with 4K paging", page_size);
@@ -707,7 +717,9 @@ private:
HostMemory::HostMemory(size_t backing_size_, size_t virtual_size_)
: backing_size(backing_size_)
, virtual_size(virtual_size_)
+ , num_granules(Common::DivCeil(backing_size_, size_t{1} << GranuleShift))
{
+ granules = std::make_unique<std::atomic<u8>[]>(num_granules);
#if defined(__OPENORBIS__) || defined(__managarm__)
LOG_WARNING(HW_Memory, "Platform doesn't support fastmem");
backing_base = static_cast<u8*>(mmap(nullptr, backing_size, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
@@ -790,8 +802,98 @@ void HostMemory::Protect(size_t virtual_offset, size_t length, MemoryPermission
#endif
}
+void HostMemory::HintCold(size_t physical_offset, size_t length,
+ std::optional<size_t> virtual_offset) const {
+ if (length == 0 || backing_base == nullptr) {
+ return;
+ }
+ const auto hint = [length](u8* address) {
+#ifdef _WIN32
+ // Unlocking pages that are not locked takes them out of the working set.
+ VirtualUnlock(address, length);
+#elif defined(__linux__) && defined(MADV_PAGEOUT)
+ madvise(address, length, MADV_PAGEOUT);
+#else
+ static_cast<void>(address);
+#endif
+ };
+ hint(backing_base + physical_offset);
+ if (virtual_offset && virtual_base != nullptr && *virtual_offset + length <= virtual_size) {
+ hint(virtual_base + *virtual_offset);
+ }
+}
+
void HostMemory::ClearBackingRegion(size_t physical_offset, size_t length, u32 fill_value) {
- std::memset(backing_base + physical_offset, fill_value, length);
+ if (length == 0) {
+ return;
+ }
+ MarkUsed(physical_offset, length);
+ if (fill_value != 0) {
+ std::memset(backing_base + physical_offset, fill_value, length);
+ MarkDirty(physical_offset, length);
+ return;
+ }
+ // Fresh backing memory is zero and the host gives it no RAM until it's touched: clear only
+ // the granules that may hold data (title 1's heap made the host commit every page of it).
+ // A granule cleared in full is zero again.
+#ifdef __linux__
+ // Handing a whole granule back to the kernel frees its RAM; a memset would commit it.
+ static const bool punch = [] {
+ const char* value = std::getenv("EDEN_PUNCH_HOLE");
+ return value == nullptr || value[0] != '0';
+ }();
+#endif
+ const size_t end = physical_offset + length;
+ for (size_t offset = physical_offset; offset < end;) {
+ const size_t index = offset >> GranuleShift;
+ const size_t granule_begin = index << GranuleShift;
+ const size_t granule_end = granule_begin + (size_t{1} << GranuleShift);
+ const size_t clear_end = (std::min)(granule_end, end);
+ if ((granules[index].load(std::memory_order_acquire) & GranuleDirty) != 0) {
+ const bool whole = offset == granule_begin && clear_end == granule_end;
+ bool cleared = false;
+#ifdef __linux__
+ cleared = whole && punch && impl && impl->PunchHole(granule_begin, granule_end - granule_begin);
+#endif
+ if (!cleared) {
+ std::memset(backing_base + offset, 0, clear_end - offset);
+ }
+ if (whole) {
+ granules[index].fetch_and(static_cast<u8>(~GranuleDirty), std::memory_order_release);
+ }
+ }
+ offset = clear_end;
+ }
+}
+
+void HostMemory::MarkUsed(size_t physical_offset, size_t length) {
+ if (length == 0) {
+ return;
+ }
+ const size_t last = (physical_offset + length - 1) >> GranuleShift;
+ for (size_t index = physical_offset >> GranuleShift; index <= last; ++index) {
+ if ((granules[index].load(std::memory_order_relaxed) & GranuleUsed) == 0) {
+ granules[index].fetch_or(GranuleUsed, std::memory_order_release);
+ }
+ }
+}
+
+void HostMemory::MarkDirty(size_t physical_offset, size_t length) {
+ if (length == 0) {
+ return;
+ }
+ const size_t last = (physical_offset + length - 1) >> GranuleShift;
+ for (size_t index = physical_offset >> GranuleShift; index <= last; ++index) {
+ granules[index].fetch_or(GranuleDirty, std::memory_order_release);
+ }
+}
+
+void HostMemory::ForgetContents() {
+ for (size_t index = 0; index < num_granules; ++index) {
+ if ((granules[index].load(std::memory_order_relaxed) & GranuleUsed) != 0) {
+ granules[index].fetch_or(GranuleDirty, std::memory_order_release);
+ }
+ }
}
void HostMemory::EnableDirectMappedAddress() {
diff --git a/src/common/host_memory.h b/src/common/host_memory.h
index 8747a1850f..84315091c5 100644
--- a/src/common/host_memory.h
+++ b/src/common/host_memory.h
@@ -6,6 +6,7 @@
#pragma once
+#include <atomic>
#include <memory>
#include <optional>
#include "common/common_funcs.h"
@@ -52,8 +53,24 @@ public:
void EnableDirectMappedAddress();
+ /// Clears guest memory being handed out. Memory still zero since it was created (or since its
+ /// last full clear) is skipped: clearing it only made the host give every page real memory.
void ClearBackingRegion(size_t physical_offset, size_t length, u32 fill_value);
+ /// Memory written once and read again rarely: its pages leave the working set (backing and,
+ /// when given, its view at virtual_offset), so the OS compresses or pages them out first when
+ /// it needs RAM. The contents stay; using the pages brings them back.
+ void HintCold(size_t physical_offset, size_t length,
+ std::optional<size_t> virtual_offset) const;
+ /// Pages the kernel hands out (cleared or not).
+ void MarkUsed(size_t physical_offset, size_t length);
+
+ /// Pages given back to the kernel: their contents are no longer known to be zero.
+ void MarkDirty(size_t physical_offset, size_t length);
+
+ /// A new kernel on this memory: everything a previous one used may hold data.
+ void ForgetContents();
+
[[nodiscard]] u8* BackingBasePointer() noexcept {
return backing_base;
}
@@ -88,6 +105,12 @@ private:
u8* backing_base{};
u8* virtual_base{};
size_t virtual_base_offset{};
+
+ static constexpr size_t GranuleShift = 16; ///< 64 KiB of backing per state byte
+ static constexpr u8 GranuleUsed = 1 << 0; ///< Handed out at least once
+ static constexpr u8 GranuleDirty = 1 << 1; ///< May hold non-zero data
+ std::unique_ptr<std::atomic<u8>[]> granules;
+ size_t num_granules{};
// Windows requires it for kernels whom lack proper support for some functions!
bool fallback_buffer{false};
};
diff --git a/src/common/settings.h b/src/common/settings.h
index 97103f457d..6669f98f4c 100644
--- a/src/common/settings.h
+++ b/src/common/settings.h
@@ -358,7 +358,7 @@ struct Values {
true};
SwitchableSetting<ScalingFilter> scaling_filter{linkage,
- ScalingFilter::NearestNeighbor,
+ ScalingFilter::ScaleForce,
"scaling_filter",
Category::Renderer,
Specialization::Default,
@@ -387,7 +387,7 @@ struct Values {
true};
SwitchableSetting<AntiAliasing> anti_aliasing{linkage,
- AntiAliasing::None,
+ AntiAliasing::Fxaa,
"anti_aliasing",
Category::Renderer,
Specialization::Default,
@@ -573,8 +573,10 @@ struct Values {
true,
true};
+ // BC3 by default: GPUs without ASTC otherwise keep ASTC textures as RGBA8, four times the
+ // VRAM (Mario Kart World: 7.3-7.8 GB of an 8 GB card, the rest paged into system RAM).
SwitchableSetting<AstcRecompression, true> astc_recompression{linkage,
- AstcRecompression::Uncompressed,
+ AstcRecompression::Bc3,
"astc_recompression",
Category::RendererAdvanced};
@@ -768,11 +770,7 @@ struct Values {
Setting<s32> current_user{linkage, 0, "current_user", Category::System};
SwitchableSetting<ConsoleMode> use_docked_mode{linkage,
-#ifdef __ANDROID__
ConsoleMode::Handheld,
-#else
- ConsoleMode::Docked,
-#endif
"use_docked_mode",
Category::System,
Specialization::Radio,
diff --git a/src/common/slot_vector.h b/src/common/slot_vector.h
index f6da7a59d1..8e1c06e35a 100644
--- a/src/common/slot_vector.h
+++ b/src/common/slot_vector.h
@@ -52,12 +52,16 @@ public:
const u64* const bitset = slot_vector->stored_bitset.data();
const u32 size = static_cast<u32>(slot_vector->stored_bitset.size()) * 64;
if (id.Value() < size) {
- do {
- ++id.index;
- } while (id.Value() < size && !IsValid(bitset));
- if (id.Value() == size) {
- id = SlotId{};
+ u32 index = id.Value() + 1;
+ while (index < size) {
+ const u64 word = bitset[index / 64] >> (index % 64);
+ if (word != 0) {
+ index += static_cast<u32>(std::countr_zero(word));
+ break;
+ }
+ index = (index / 64 + 1) * 64;
}
+ id = index < size ? SlotId{index} : SlotId{};
}
return *this;
}
@@ -139,7 +143,7 @@ public:
if (it == stored_bitset.end()) {
return end();
}
- const u32 word_index = static_cast<u32>(std::distance(it, stored_bitset.begin()));
+ const u32 word_index = static_cast<u32>(std::distance(stored_bitset.begin(), it));
const SlotId first_id{word_index * 64 + static_cast<u32>(std::countr_zero(*it))};
return Iterator(this, first_id);
}
diff --git a/src/common/sparse_large_vector.cpp b/src/common/sparse_large_vector.cpp
index b96340999d..34aba73d78 100644
--- a/src/common/sparse_large_vector.cpp
+++ b/src/common/sparse_large_vector.cpp
@@ -39,12 +39,12 @@ static LONG WINAPI FakePageFaultHandler(PEXCEPTION_POINTERS info) {
std::lock_guard lock(vector_regions_mutex);
for (auto region: vector_regions) {
auto addr_shifted = exception_addr >> HostPageBits;
- if (region.first <= addr_shifted && addr_shifted <= region.second) {
+ if (region.first <= addr_shifted && addr_shifted < region.second) {
addr = addr_shifted;
}
// Page-boundary accesses
- if (auto addr_ = (exception_addr + 0x40) >> HostPageBits; addr_ != addr_shifted && region.first <= addr_ && addr_ <= region.second) {
+ if (auto addr_ = (exception_addr + 0x40) >> HostPageBits; addr_ != addr_shifted && region.first <= addr_ && addr_ < region.second) {
addr2 = addr_;
}
@@ -164,7 +164,8 @@ void FreeMemoryPages(void* base, [[maybe_unused]] std::size_t size) noexcept {
return;
#ifdef _WIN32
std::lock_guard lock(vector_regions_mutex);
- std::erase_if(vector_regions, [base](const auto& r) {return r.first == reinterpret_cast<u64>(base); });
+ // Regions are kept in host pages.
+ std::erase_if(vector_regions, [base](const auto& r) {return r.first == reinterpret_cast<u64>(base) >> HostPageBits; });
ASSERT(VirtualFree(base, 0, MEM_RELEASE));
#else
ASSERT(munmap(base, size) == 0);
diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt
index 0075838d49..1ff9f271d2 100644
--- a/src/core/CMakeLists.txt
+++ b/src/core/CMakeLists.txt
@@ -132,6 +132,10 @@ add_library(core STATIC
file_sys/program_metadata.h
file_sys/registered_cache.cpp
file_sys/registered_cache.h
+ file_sys/nup_container.cpp
+ file_sys/nup_container.h
+ file_sys/nup_writer.cpp
+ file_sys/nup_writer.h
file_sys/romfs.cpp
file_sys/romfs.h
file_sys/romfs_factory.cpp
@@ -527,6 +531,7 @@ add_library(core STATIC
hle/service/audio/audio_out.h
hle/service/audio/audio_renderer_manager.cpp
hle/service/audio/audio_renderer_manager.h
+ hle/service/audio/nx2_audio_device_manager.h
hle/service/audio/nx2_audio_user_client.h
hle/service/audio/audio_renderer.cpp
hle/service/audio/audio_renderer.h
@@ -1155,6 +1160,8 @@ add_library(core STATIC
loader/nso.h
loader/nsp.cpp
loader/nsp.h
+ loader/nup.cpp
+ loader/nup.h
loader/xci.cpp
loader/xci.h
memory.cpp
@@ -1283,4 +1290,9 @@ else()
target_sources(core PRIVATE hle/service/ssl/ssl_backend_none.cpp)
endif()
+if (ENABLE_LTO AND CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang" AND NOT MSVC)
+ # Fibers can migrate between host threads; keep kernel TLS lookups outside LTO callers.
+ set_source_files_properties(hle/kernel/kernel.cpp PROPERTIES COMPILE_OPTIONS -fno-lto)
+endif()
+
create_target_directory_groups(core)
diff --git a/src/core/arm/dynarmic/arm_dynarmic_32.cpp b/src/core/arm/dynarmic/arm_dynarmic_32.cpp
index a59aa3cac3..3a875493c6 100644
--- a/src/core/arm/dynarmic/arm_dynarmic_32.cpp
+++ b/src/core/arm/dynarmic/arm_dynarmic_32.cpp
@@ -416,6 +416,10 @@ void ArmDynarmic32::ClearInstructionCache() {
}
void ArmDynarmic32::InvalidateCacheRange(u64 addr, std::size_t size) {
+ // Drop the cached code page too: blocks recompiled after the invalidation would
+ // otherwise still decode the old instructions (e.g. a gdbstub breakpoint written
+ // into or removed from that page).
+ m_cb->last_code_addr = u64(-1);
m_jit->InvalidateCacheRange(static_cast<u32>(addr), size);
}
diff --git a/src/core/arm/dynarmic/arm_dynarmic_64.cpp b/src/core/arm/dynarmic/arm_dynarmic_64.cpp
index e65e38dd9e..c5be4d2520 100644
--- a/src/core/arm/dynarmic/arm_dynarmic_64.cpp
+++ b/src/core/arm/dynarmic/arm_dynarmic_64.cpp
@@ -481,6 +481,10 @@ void ArmDynarmic64::ClearInstructionCache() {
}
void ArmDynarmic64::InvalidateCacheRange(u64 addr, std::size_t size) {
+ // Drop the cached code page too: blocks recompiled after the invalidation would
+ // otherwise still decode the old instructions (e.g. a gdbstub breakpoint written
+ // into or removed from that page).
+ m_cb->last_code_addr = u64(-1);
m_jit->InvalidateCacheRange(addr, size);
}
diff --git a/src/core/core.cpp b/src/core/core.cpp
index 41871df870..3842ee25a8 100644
--- a/src/core/core.cpp
+++ b/src/core/core.cpp
@@ -25,6 +25,7 @@
#include "core/debugger/debugger.h"
#include "core/device_memory.h"
#include "core/file_sys/fs_filesystem.h"
+#include "core/file_sys/nup_container.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/program_metadata.h"
#include "core/file_sys/registered_cache.h"
@@ -116,9 +117,27 @@ namespace {
// Switch 2 titles boot as a loose ExeFS, with main.npdm next to main.
bool IsSwitch2Program(const FileSys::VirtualFile& file) {
+ FileSys::ProgramMetadata metadata;
+ if (FileSys::NupContainer::IsNupFile(file)) {
+ FileSys::NupContainer nup{file};
+ if (nup.GetStatus() != Loader::ResultStatus::Success) {
+ return false;
+ }
+ std::optional<std::size_t> program;
+ if (const auto base = nup.FindBasePackage();
+ base && nup.GetExeFSDir(*base) != nullptr) {
+ program = base;
+ } else if (const auto update = nup.FindBestUpdatePackage();
+ update && nup.GetExeFSDir(*update) != nullptr) {
+ program = update;
+ }
+ const auto exefs = program ? nup.GetExeFSDir(*program) : nullptr;
+ const auto npdm = exefs != nullptr ? exefs->GetFile("main.npdm") : nullptr;
+ return npdm != nullptr && metadata.Load(npdm) == Loader::ResultStatus::Success &&
+ metadata.IsSwitch2();
+ }
const auto dir = file != nullptr ? file->GetContainingDirectory() : nullptr;
const auto npdm = dir != nullptr ? dir->GetFile("main.npdm") : nullptr;
- FileSys::ProgramMetadata metadata;
return npdm != nullptr && metadata.Load(npdm) == Loader::ResultStatus::Success &&
metadata.IsSwitch2();
}
diff --git a/src/core/device_memory_manager.h b/src/core/device_memory_manager.h
index 27ff342213..d275bdbf2d 100644
--- a/src/core/device_memory_manager.h
+++ b/src/core/device_memory_manager.h
@@ -18,6 +18,7 @@
#include "common/common_types.h"
#include "common/range_mutex.h"
#include "common/scratch_buffer.h"
+#include "common/thread_worker.h"
#include "common/sparse_large_vector.h"
namespace Core {
@@ -53,6 +54,10 @@ public:
void BindInterface(DeviceInterface* device_inter);
+ /// Marks the memory behind [address, address + size) cold (Common::HostMemory::HintCold),
+ /// from a low-priority thread: data the host GPU has its own copy of, e.g. uploaded textures.
+ void HintCold(DAddr address, size_t size);
+
DAddr Allocate(size_t size);
void AllocateFixed(DAddr start, size_t size);
void Free(DAddr start, size_t size);
@@ -197,7 +202,9 @@ private:
std::unique_ptr<DeviceMemoryManagerAllocator<Traits>> impl;
const uintptr_t physical_base;
+ const DeviceMemory* device_memory_ptr;
DeviceInterface* device_inter;
+ std::unique_ptr<Common::ThreadWorker> cold_hint_worker;
struct TrackedEntry {
VAddr cpu_backing_address;
diff --git a/src/core/device_memory_manager.inc b/src/core/device_memory_manager.inc
index 83faa1b3f3..5931fd0882 100644
--- a/src/core/device_memory_manager.inc
+++ b/src/core/device_memory_manager.inc
@@ -171,6 +171,7 @@ struct DeviceMemoryManagerAllocator {
template <typename Traits>
DeviceMemoryManager<Traits>::DeviceMemoryManager(const DeviceMemory& device_memory_)
: physical_base{uintptr_t(device_memory_.buffer.BackingBasePointer())}
+ , device_memory_ptr{&device_memory_}
, device_inter{nullptr}
, compressed_device_addr(1ULL << ((Settings::values.memory_layout_mode.GetValue() == Settings::MemoryLayout::Memory_4Gb ? physical_min_bits : physical_max_bits) - Memory::YUZU_PAGEBITS))
, tracked_entries(device_as_size >> Memory::YUZU_PAGEBITS)
@@ -183,6 +184,49 @@ template <typename Traits>
DeviceMemoryManager<Traits>::~DeviceMemoryManager() = default;
template <typename Traits>
+void DeviceMemoryManager<Traits>::HintCold(DAddr address, size_t size) {
+ struct Run {
+ u64 physical_offset;
+ VAddr virtual_address; ///< 0: no CPU mapping known
+ u64 size;
+ };
+ std::vector<Run> runs;
+ const size_t page_end = Common::DivCeil(address + size, Memory::YUZU_PAGESIZE);
+ for (size_t page = address >> Memory::YUZU_PAGEBITS; page < page_end; ++page) {
+ const u32 compressed = tracked_entries[page].compressed_physical_ptr;
+ if (compressed == 0) {
+ continue;
+ }
+ const u64 physical = u64{compressed - 1} << Memory::YUZU_PAGEBITS;
+ const VAddr vaddr = ExtractCPUBacking(page).second;
+ if (!runs.empty()) {
+ Run& last = runs.back();
+ const bool contiguous_view =
+ last.virtual_address == 0 ? vaddr == 0 : last.virtual_address + last.size == vaddr;
+ if (last.physical_offset + last.size == physical && contiguous_view) {
+ last.size += Memory::YUZU_PAGESIZE;
+ continue;
+ }
+ }
+ runs.push_back({physical, vaddr, Memory::YUZU_PAGESIZE});
+ }
+ if (runs.empty()) {
+ return;
+ }
+ if (!cold_hint_worker) {
+ cold_hint_worker = std::make_unique<Common::ThreadWorker>(1, "DeviceMemory:ColdHint");
+ }
+ cold_hint_worker->QueueWork([this, runs = std::move(runs)] {
+ for (const Run& run : runs) {
+ device_memory_ptr->buffer.HintCold(
+ run.physical_offset, run.size,
+ run.virtual_address != 0 ? std::optional<size_t>{run.virtual_address}
+ : std::nullopt);
+ }
+ });
+}
+
+template <typename Traits>
void DeviceMemoryManager<Traits>::BindInterface(DeviceInterface* device_inter_) {
device_inter = device_inter_;
}
@@ -582,6 +626,11 @@ void DeviceMemoryManager<Traits>::UpdatePagesCachedCountNoLock(DAddr addr, size_
const size_t page_end = Common::DivCeil(addr + size, Memory::YUZU_PAGESIZE);
size_t page = addr >> Memory::YUZU_PAGEBITS;
auto [asid, base_vaddress] = ExtractCPUBacking(page);
+ // Sem processo registrado nao ha o que marcar, e asid.id 0 indexaria um vetor vazio. Acontece
+ // no alvo de testes, que chama UpdatePagesCachedBatch num DeviceMemoryManager recem-criado.
+ if (asid.id >= registered_processes.size()) {
+ return;
+ }
auto* memory_device_inter = registered_processes[asid.id];
const auto release_pending = [&] {
if (uncache_bytes > 0) {
diff --git a/src/core/file_sys/fsmitm_romfsbuild.cpp b/src/core/file_sys/fsmitm_romfsbuild.cpp
index 8807bbd0f7..d34e24f177 100644
--- a/src/core/file_sys/fsmitm_romfsbuild.cpp
+++ b/src/core/file_sys/fsmitm_romfsbuild.cpp
@@ -55,15 +55,18 @@ static_assert(sizeof(RomFSFileEntry) == 0x20, "RomFSFileEntry has incorrect size
struct RomFSBuildFileContext;
+// The context's directories and files own the nodes; the links between them don't, or every
+// parent and child would own each other and no build would ever be freed (title 1's loose RomFS:
+// 206k files, ~50 MiB per build).
struct RomFSBuildDirectoryContext {
std::string path;
u32 cur_path_ofs = 0;
u32 path_len = 0;
u32 entry_offset = 0;
- std::shared_ptr<RomFSBuildDirectoryContext> parent;
- std::shared_ptr<RomFSBuildDirectoryContext> child;
- std::shared_ptr<RomFSBuildDirectoryContext> sibling;
- std::shared_ptr<RomFSBuildFileContext> file;
+ RomFSBuildDirectoryContext* parent = nullptr;
+ RomFSBuildDirectoryContext* child = nullptr;
+ RomFSBuildDirectoryContext* sibling = nullptr;
+ RomFSBuildFileContext* file = nullptr;
};
struct RomFSBuildFileContext {
@@ -73,8 +76,8 @@ struct RomFSBuildFileContext {
u32 entry_offset = 0;
u64 offset = 0;
u64 size = 0;
- std::shared_ptr<RomFSBuildDirectoryContext> parent;
- std::shared_ptr<RomFSBuildFileContext> sibling;
+ RomFSBuildDirectoryContext* parent = nullptr;
+ RomFSBuildFileContext* sibling = nullptr;
VirtualFile source;
};
@@ -168,7 +171,7 @@ bool RomFSBuildContext::AddDirectory(std::shared_ptr<RomFSBuildDirectoryContext>
num_dirs++;
dir_table_size +=
sizeof(RomFSDirectoryEntry) + Common::AlignUp(dir_ctx->path_len - dir_ctx->cur_path_ofs, 4);
- dir_ctx->parent = std::move(parent_dir_ctx);
+ dir_ctx->parent = parent_dir_ctx.get();
directories.emplace_back(std::move(dir_ctx));
return true;
@@ -180,7 +183,7 @@ bool RomFSBuildContext::AddFile(std::shared_ptr<RomFSBuildDirectoryContext> pare
num_files++;
file_table_size +=
sizeof(RomFSFileEntry) + Common::AlignUp(file_ctx->path_len - file_ctx->cur_path_ofs, 4);
- file_ctx->parent = std::move(parent_dir_ctx);
+ file_ctx->parent = parent_dir_ctx.get();
files.emplace_back(std::move(file_ctx));
return true;
@@ -245,11 +248,11 @@ std::vector<std::pair<u64, VirtualFile>> RomFSBuildContext::Build() {
Common::AlignUp(cur_file->path_len - cur_file->cur_path_ofs, 4));
prev_file = cur_file;
}
- // Assign deferred parent/sibling ownership.
+ // Assign deferred parent/sibling links.
for (auto it = files.rbegin(); it != files.rend(); ++it) {
auto& cur_file = *it;
cur_file->sibling = cur_file->parent->file;
- cur_file->parent->file = cur_file;
+ cur_file->parent->file = cur_file.get();
}
// Determine directory offsets.
@@ -260,11 +263,11 @@ std::vector<std::pair<u64, VirtualFile>> RomFSBuildContext::Build() {
static_cast<u32>(sizeof(RomFSDirectoryEntry) +
Common::AlignUp(cur_dir->path_len - cur_dir->cur_path_ofs, 4));
}
- // Assign deferred parent/sibling ownership.
+ // Assign deferred parent/sibling links.
for (auto it = directories.rbegin(); (*it) != root; ++it) {
auto& cur_dir = *it;
cur_dir->sibling = cur_dir->parent->child;
- cur_dir->parent->child = cur_dir;
+ cur_dir->parent->child = cur_dir.get();
}
// Create output map.
diff --git a/src/core/file_sys/nup_container.cpp b/src/core/file_sys/nup_container.cpp
new file mode 100644
index 0000000000..27ac4486ca
--- /dev/null
+++ b/src/core/file_sys/nup_container.cpp
@@ -0,0 +1,842 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "core/file_sys/nup_container.h"
+
+#include <algorithm>
+#include <cstddef>
+#include <cstring>
+#include <list>
+#include <map>
+#include <random>
+#include <span>
+#include <unordered_map>
+
+#include <fmt/format.h>
+#include <openssl/evp.h>
+#include <zstd.h>
+
+#include "common/fs/path_util.h"
+#include "common/logging.h"
+#include "common/swap.h"
+#include "core/file_sys/vfs/vfs_offset.h"
+
+namespace FileSys {
+namespace {
+
+constexpr u32 NoDir = 0xFFFFFFFF;
+constexpr std::size_t SuperblockSize = 0x80;
+constexpr std::size_t PackageRecordSize = 0x40;
+constexpr std::size_t DirRecordSize = 0x18;
+constexpr std::size_t FileRecordSize = 0x50;
+constexpr std::size_t TocHeaderSize = 0x30;
+constexpr std::size_t TlvHeaderSize = 0x8;
+constexpr u32 TocMagic = 0x01434F54; // "TOC\x01" little-endian
+constexpr u8 CodecRaw = 0;
+constexpr u8 CodecZstd = 1;
+constexpr u8 PackageFlagStandaloneUpdate = 0x01;
+constexpr u32 MaxDirDepth = 128;
+
+#pragma pack(push, 1)
+struct Superblock {
+ std::array<u8, 4> magic;
+ u16_le header_size;
+ u16_le header_version;
+ u64_le toc_offset;
+ u64_le toc_size;
+ std::array<u8, 0x20> toc_sha256;
+ u64_le blob_offset;
+ u64_le file_size;
+ u32_le package_count;
+ u32_le flags;
+ std::array<u8, 0x10> uuid;
+ std::array<u8, 0x20> reserved;
+};
+static_assert(sizeof(Superblock) == SuperblockSize);
+
+struct TocHeader {
+ u32_le magic;
+ u32_le package_count;
+ u64_le dir_count;
+ u64_le file_count;
+ u64_le pool_offset;
+ u64_le pool_size;
+ u64_le reserved;
+};
+static_assert(sizeof(TocHeader) == TocHeaderSize);
+
+struct PackageRecord {
+ u64_le title_id; // 0x00
+ u32_le root_dir; // 0x08
+ u32_le exefs_dir; // 0x0C
+ u32_le romfs_dir; // 0x10
+ u32_le version; // 0x14
+ u8 kind; // 0x18
+ u8 flags; // 0x19
+ u16_le reserved; // 0x1A
+ u32_le reserved2; // 0x1C
+ u64_le meta_offset; // 0x20
+ u32_le meta_size; // 0x28
+ std::array<u8, 20> reserved3; // 0x2C
+};
+static_assert(sizeof(PackageRecord) == PackageRecordSize);
+static_assert(offsetof(PackageRecord, title_id) == 0x00);
+static_assert(offsetof(PackageRecord, root_dir) == 0x08);
+static_assert(offsetof(PackageRecord, exefs_dir) == 0x0C);
+static_assert(offsetof(PackageRecord, romfs_dir) == 0x10);
+static_assert(offsetof(PackageRecord, version) == 0x14);
+static_assert(offsetof(PackageRecord, kind) == 0x18);
+static_assert(offsetof(PackageRecord, flags) == 0x19);
+static_assert(offsetof(PackageRecord, reserved) == 0x1A);
+static_assert(offsetof(PackageRecord, reserved2) == 0x1C);
+static_assert(offsetof(PackageRecord, meta_offset) == 0x20);
+static_assert(offsetof(PackageRecord, meta_size) == 0x28);
+
+struct DirRecord {
+ u32_le name_offset;
+ u32_le name_length;
+ u32_le first_child;
+ u32_le child_count;
+ u64_le reserved;
+};
+static_assert(sizeof(DirRecord) == DirRecordSize);
+
+struct FileRecord {
+ u32_le name_offset; // 0x00
+ u32_le name_length; // 0x04
+ u64_le size; // 0x08
+ u64_le extent_offset; // 0x10
+ u64_le extent_size; // 0x18
+ std::array<u8, 0x20> sha256; // 0x20
+ u8 codec; // 0x40
+ u8 codec_flags; // 0x41
+ u16_le reserved2; // 0x42
+ std::array<u8, 12> reserved3; // 0x44
+};
+static_assert(sizeof(FileRecord) == FileRecordSize);
+static_assert(offsetof(FileRecord, name_offset) == 0x00);
+static_assert(offsetof(FileRecord, name_length) == 0x04);
+static_assert(offsetof(FileRecord, size) == 0x08);
+static_assert(offsetof(FileRecord, extent_offset) == 0x10);
+static_assert(offsetof(FileRecord, extent_size) == 0x18);
+static_assert(offsetof(FileRecord, sha256) == 0x20);
+static_assert(offsetof(FileRecord, codec) == 0x40);
+static_assert(offsetof(FileRecord, codec_flags) == 0x41);
+static_assert(offsetof(FileRecord, reserved2) == 0x42);
+
+struct TlvHeader {
+ u16_le type;
+ u16_le reserved;
+ u32_le size;
+};
+static_assert(sizeof(TlvHeader) == TlvHeaderSize);
+#pragma pack(pop)
+
+enum MetaType : u16 {
+ MetaNpdm = 0x0001,
+ MetaIcon = 0x0003,
+ MetaDisplayVersion = 0x0004,
+ MetaTitleName = 0x0005,
+ MetaLocalizedTitles = 0x0009,
+ MetaLocalizedIcons = 0x000A,
+};
+
+bool Sha256(const u8* data, std::size_t size, std::array<u8, 0x20>& out) {
+ unsigned int length = 0;
+ EVP_Digest(data, size, out.data(), &length, EVP_sha256(), nullptr);
+ return length == out.size();
+}
+
+/// A member file whose extent is zstd-compressed. Decompresses on first read and caches the
+/// result (LRU-evicted); a file over half the cache cap is decompressed once to a temporary file.
+class NupVfsFile final : public VfsFile {
+public:
+ NupVfsFile(VirtualFile backing_, u64 extent_offset_, u64 extent_size_, u64 uncompressed_size_,
+ std::string name_, std::shared_ptr<NupDecompressCache> cache_,
+ std::size_t cache_index_)
+ : backing(std::move(backing_)), extent_offset(extent_offset_), extent_size(extent_size_),
+ uncompressed_size(uncompressed_size_), name(std::move(name_)), cache(std::move(cache_)),
+ cache_index(cache_index_), cache_enabled(uncompressed_size <= cache->cap / 2) {}
+
+ std::string GetName() const override {
+ return name;
+ }
+
+ std::size_t GetSize() const override {
+ return static_cast<std::size_t>(uncompressed_size);
+ }
+
+ bool Resize(std::size_t) override {
+ return false;
+ }
+
+ VirtualDir GetContainingDirectory() const override {
+ return nullptr;
+ }
+
+ bool IsWritable() const override {
+ return false;
+ }
+
+ bool IsReadable() const override {
+ return true;
+ }
+
+ std::size_t Read(u8* data, std::size_t length, std::size_t offset) const override {
+ if (offset >= uncompressed_size) {
+ return 0;
+ }
+ length = std::min(length, static_cast<std::size_t>(uncompressed_size) - offset);
+
+ if (!cache_enabled) {
+ return ReadSpilled(data, length, offset);
+ }
+
+ {
+ std::scoped_lock lock{cache->mutex};
+ if (const auto* cached = cache->Find(cache_index)) {
+ std::memcpy(data, cached->data() + offset, length);
+ return length;
+ }
+ }
+
+ // Miss: decompress without holding the lock, then publish and copy.
+ std::vector<u8> scratch = Decompress();
+ if (scratch.empty()) {
+ return 0;
+ }
+ {
+ std::scoped_lock lock{cache->mutex};
+ cache->Insert(cache_index, scratch);
+ if (const auto* cached = cache->Find(cache_index)) {
+ std::memcpy(data, cached->data() + offset, length);
+ return length;
+ }
+ }
+ std::memcpy(data, scratch.data() + offset, length);
+ return length;
+ }
+
+ std::size_t Write(const u8*, std::size_t, std::size_t) override {
+ return 0;
+ }
+
+ bool Rename(std::string_view new_name) override {
+ name = std::string(new_name);
+ return true;
+ }
+
+private:
+ std::size_t ReadSpilled(u8* data, std::size_t length, std::size_t offset) const {
+ std::shared_ptr<NupSpillFile> spill;
+ {
+ std::scoped_lock lock{cache->mutex};
+ spill = cache->GetSpill(cache_index);
+ }
+ std::scoped_lock lock{spill->mutex};
+ if (!spill->attempted) {
+ spill->attempted = true;
+ DecompressToFile(*spill);
+ }
+ if (!spill->file.IsOpen() || !spill->file.Seek(static_cast<s64>(offset))) {
+ return 0;
+ }
+ return spill->file.ReadSpan(std::span{data, length});
+ }
+
+ void DecompressToFile(NupSpillFile& spill) const {
+ const auto fail = [&](const char* reason) {
+ LOG_ERROR(Loader, "NUP zstd decompression of {} to a temporary file failed: {}", name,
+ reason);
+ spill.file.Close();
+ if (!spill.path.empty()) {
+ std::error_code ec;
+ std::filesystem::remove(spill.path, ec);
+ }
+ };
+ if (extent_size > backing->GetSize() || extent_offset > backing->GetSize() - extent_size) {
+ return fail("extent out of range");
+ }
+
+ std::random_device random;
+ std::error_code ec;
+ std::filesystem::create_directories(cache->spill_dir, ec);
+ spill.path =
+ cache->spill_dir / fmt::format("{:08x}{:08x}-{}.tmp", random(), random(), cache_index);
+ Common::FS::IOFile out{spill.path, Common::FS::FileAccessMode::Write};
+ if (!out.IsOpen()) {
+ return fail("cannot create the temporary file");
+ }
+
+ std::unique_ptr<ZSTD_DCtx, decltype(&ZSTD_freeDCtx)> dctx{ZSTD_createDCtx(),
+ &ZSTD_freeDCtx};
+ std::vector<u8> in_buffer(ZSTD_DStreamInSize());
+ std::vector<u8> out_buffer(ZSTD_DStreamOutSize());
+ u64 read = 0;
+ u64 written = 0;
+ bool frame_done = false;
+ while (!frame_done && read < extent_size) {
+ const auto chunk =
+ static_cast<std::size_t>(std::min<u64>(in_buffer.size(), extent_size - read));
+ if (backing->Read(in_buffer.data(), chunk,
+ static_cast<std::size_t>(extent_offset + read)) != chunk) {
+ return fail("short read of the compressed data");
+ }
+ read += chunk;
+ ZSTD_inBuffer input{in_buffer.data(), chunk, 0};
+ ZSTD_outBuffer output{};
+ do {
+ output = {out_buffer.data(), out_buffer.size(), 0};
+ const std::size_t result = ZSTD_decompressStream(dctx.get(), &output, &input);
+ if (ZSTD_isError(result) || output.pos > uncompressed_size - written) {
+ return fail("corrupt or oversized data");
+ }
+ if (out.WriteSpan(std::span<const u8>{out_buffer.data(), output.pos}) !=
+ output.pos) {
+ return fail("cannot write the temporary file");
+ }
+ written += output.pos;
+ frame_done = result == 0;
+ } while (!frame_done && (input.pos < input.size || output.pos == output.size));
+ }
+ if (!frame_done || written != uncompressed_size) {
+ return fail("size mismatch");
+ }
+ out.Close();
+ spill.file.Open(spill.path, Common::FS::FileAccessMode::Read);
+ if (!spill.file.IsOpen()) {
+ fail("cannot reopen the temporary file");
+ }
+ }
+
+ std::vector<u8> Decompress() const {
+ if (extent_size > backing->GetSize() || extent_offset > backing->GetSize() - extent_size) {
+ return {};
+ }
+ const auto compressed = backing->ReadBytes(static_cast<std::size_t>(extent_size),
+ static_cast<std::size_t>(extent_offset));
+ if (compressed.size() != extent_size) {
+ return {};
+ }
+ std::vector<u8> out(static_cast<std::size_t>(uncompressed_size));
+ const std::size_t result =
+ ZSTD_decompress(out.data(), out.size(), compressed.data(), compressed.size());
+ if (ZSTD_isError(result) || result != out.size()) {
+ LOG_ERROR(Loader, "NUP zstd decompression failed for {}", name);
+ return {};
+ }
+ return out;
+ }
+
+ VirtualFile backing;
+ u64 extent_offset;
+ u64 extent_size;
+ u64 uncompressed_size;
+ std::string name;
+ std::shared_ptr<NupDecompressCache> cache;
+ std::size_t cache_index;
+ bool cache_enabled;
+};
+
+} // namespace
+
+NupSpillFile::~NupSpillFile() {
+ file.Close();
+ if (!path.empty()) {
+ std::error_code ec;
+ std::filesystem::remove(path, ec);
+ }
+}
+
+bool NupContainer::IsNupFile(const VirtualFile& file) {
+ if (file == nullptr || file->GetSize() < SuperblockSize) {
+ return false;
+ }
+ std::array<u8, 4> magic{};
+ file->ReadObject(&magic, 0);
+ return magic == HeaderMagic;
+}
+
+NupContainer::NupContainer(VirtualFile file_, std::size_t memory_cache_cap,
+ std::filesystem::path spill_dir_)
+ : file(std::move(file_)), cache(std::make_shared<NupDecompressCache>()) {
+ cache->cap = memory_cache_cap;
+ cache->spill_dir = spill_dir_.empty()
+ ? Common::FS::GetEdenPath(Common::FS::EdenPath::CacheDir) / "nup"
+ : std::move(spill_dir_);
+ if (file == nullptr) {
+ return;
+ }
+ if (Parse()) {
+ status = Loader::ResultStatus::Success;
+ } else if (status == Loader::ResultStatus::ErrorNullFile) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ }
+}
+
+NupContainer::~NupContainer() = default;
+
+bool NupContainer::Parse() {
+ if (file->GetSize() < SuperblockSize) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ Superblock super{};
+ if (file->ReadObject(&super, 0) != sizeof(Superblock)) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ if (super.magic != HeaderMagic || super.header_size < SuperblockSize || super.flags != 0) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ toc_offset = super.toc_offset;
+ toc_size = super.toc_size;
+ blob_offset = super.blob_offset;
+ file_size = super.file_size;
+ if (file_size != file->GetSize() || toc_offset < SuperblockSize || blob_offset > file_size ||
+ toc_offset > blob_offset || toc_size > blob_offset - toc_offset ||
+ blob_offset % 0x10 != 0) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+
+ const auto toc = file->ReadBytes(static_cast<std::size_t>(toc_size),
+ static_cast<std::size_t>(toc_offset));
+ if (toc.size() != toc_size) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ std::array<u8, 0x20> toc_hash{};
+ if (!Sha256(toc.data(), toc.size(), toc_hash) || toc_hash != super.toc_sha256) {
+ LOG_ERROR(Loader, "NUP TOC SHA-256 mismatch, the file is corrupt or was modified");
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+
+ TocHeader header{};
+ if (toc.size() < sizeof(header)) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ std::memcpy(&header, toc.data(), sizeof(header));
+ if (header.magic != TocMagic || header.package_count == 0 ||
+ static_cast<u32>(header.package_count) != super.package_count) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+
+ // The counts come from the file, so bound them by the TOC size before multiplying.
+ if (header.dir_count > toc.size() / DirRecordSize ||
+ header.file_count > toc.size() / FileRecordSize) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ const u64 needed = TocHeaderSize + static_cast<u64>(header.package_count) * PackageRecordSize +
+ header.dir_count * DirRecordSize + header.file_count * FileRecordSize;
+ if (needed > toc.size() || header.pool_offset < needed || header.pool_offset > toc.size() ||
+ header.pool_size > toc.size() - header.pool_offset ||
+ (header.pool_offset - needed) % sizeof(u32) != 0) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ const u64 children_offset = needed;
+ const u64 children_bytes = header.pool_offset - needed;
+ const u8* base = toc.data();
+
+ std::size_t pos = TocHeaderSize;
+ packages.reserve(header.package_count);
+ for (u32 i = 0; i < header.package_count; ++i) {
+ PackageRecord record{};
+ std::memcpy(&record, base + pos, sizeof(record));
+ pos += PackageRecordSize;
+ if (record.kind > static_cast<u8>(PackageKind::Addon) ||
+ (record.flags & ~PackageFlagStandaloneUpdate) != 0 ||
+ record.root_dir >= header.dir_count ||
+ (record.exefs_dir != NoDir && record.exefs_dir >= header.dir_count) ||
+ (record.romfs_dir != NoDir && record.romfs_dir >= header.dir_count)) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ PackageInfo package;
+ package.title_id = record.title_id;
+ package.version = record.version;
+ package.kind = static_cast<PackageKind>(record.kind);
+ package.standalone_update = (record.flags & PackageFlagStandaloneUpdate) != 0;
+ package.root_dir = record.root_dir;
+ package.exefs_dir = record.exefs_dir;
+ package.romfs_dir = record.romfs_dir;
+ if (record.meta_size > 0 && record.meta_offset != 0 && record.meta_offset <= file_size &&
+ record.meta_size <= file_size - record.meta_offset) {
+ package.meta_offset = record.meta_offset;
+ package.meta_size = record.meta_size;
+ }
+ packages.push_back(std::move(package));
+ }
+
+ dirs.resize(header.dir_count);
+ for (u64 i = 0; i < header.dir_count; ++i) {
+ DirRecord record{};
+ std::memcpy(&record, base + pos, sizeof(record));
+ pos += DirRecordSize;
+ if (static_cast<u64>(record.first_child) + record.child_count >
+ children_bytes / sizeof(u32) ||
+ static_cast<u64>(record.name_offset) + record.name_length > header.pool_size) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ dirs[i].name = std::string(reinterpret_cast<const char*>(base) + header.pool_offset +
+ record.name_offset,
+ record.name_length);
+ dirs[i].first_child = record.first_child;
+ dirs[i].child_count = record.child_count;
+ }
+
+ files.resize(header.file_count);
+ for (u64 i = 0; i < header.file_count; ++i) {
+ FileRecord record{};
+ std::memcpy(&record, base + pos, sizeof(record));
+ pos += FileRecordSize;
+ if (record.codec > CodecZstd || record.codec_flags != 0 ||
+ (record.size == 0 && (record.extent_offset != 0 || record.extent_size != 0)) ||
+ (record.size != 0 &&
+ (record.extent_size == 0 || record.extent_offset < blob_offset ||
+ record.extent_offset % 0x10 != 0 || record.extent_offset > file_size ||
+ record.extent_size > file_size - record.extent_offset)) ||
+ static_cast<u64>(record.name_offset) + record.name_length > header.pool_size) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ files[i].name = std::string(reinterpret_cast<const char*>(base) + header.pool_offset +
+ record.name_offset,
+ record.name_length);
+ files[i].size = record.size;
+ files[i].sha256 = record.sha256;
+ files[i].extent_offset = record.extent_offset;
+ files[i].extent_size = record.extent_size;
+ files[i].codec = record.codec;
+ }
+
+ children.resize(children_bytes / sizeof(u32));
+ std::memcpy(children.data(), base + children_offset, children_bytes);
+ for (const u32 child : children) {
+ if (child >= header.dir_count + header.file_count) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ }
+
+ if (!ValidateDirTree()) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+
+ for (auto& package : packages) {
+ if (!ParseMeta(package)) {
+ status = Loader::ResultStatus::ErrorNUPBadFormat;
+ return false;
+ }
+ }
+ return true;
+}
+
+bool NupContainer::ValidateDirTree() const {
+ enum : u8 { Unvisited, InProgress, Done };
+ struct Frame {
+ u32 dir;
+ u32 next;
+ };
+ std::vector<u8> state(dirs.size(), Unvisited);
+ std::vector<u32> height(dirs.size(), 0);
+ std::vector<Frame> stack;
+ for (u32 start = 0; start < dirs.size(); ++start) {
+ if (state[start] != Unvisited) {
+ continue;
+ }
+ state[start] = InProgress;
+ stack.push_back({start, 0});
+ while (!stack.empty()) {
+ const Frame frame = stack.back();
+ const DirNode& node = dirs[frame.dir];
+ if (frame.next == node.child_count) {
+ state[frame.dir] = Done;
+ stack.pop_back();
+ if (!stack.empty()) {
+ u32& parent_height = height[stack.back().dir];
+ parent_height = std::max(parent_height, height[frame.dir] + 1);
+ }
+ continue;
+ }
+ ++stack.back().next;
+ const u32 child = children[static_cast<std::size_t>(node.first_child) + frame.next];
+ if (child >= dirs.size()) {
+ continue;
+ }
+ if (state[child] == InProgress) {
+ LOG_ERROR(Loader, "NUP directory tree contains a cycle");
+ return false;
+ }
+ if (state[child] == Done) {
+ height[frame.dir] = std::max(height[frame.dir], height[child] + 1);
+ } else {
+ state[child] = InProgress;
+ stack.push_back({child, 0});
+ }
+ if (stack.size() + height[child] > MaxDirDepth) {
+ LOG_ERROR(Loader, "NUP directory tree is nested too deeply");
+ return false;
+ }
+ }
+ }
+ return true;
+}
+
+bool NupContainer::ParseMeta(PackageInfo& package) {
+ if (package.meta_offset == 0) {
+ return true;
+ }
+ const auto block = file->ReadBytes(static_cast<std::size_t>(package.meta_size),
+ static_cast<std::size_t>(package.meta_offset));
+ if (block.size() != package.meta_size) {
+ return false;
+ }
+ std::size_t pos = 0;
+ while (pos + TlvHeaderSize <= block.size()) {
+ TlvHeader header{};
+ std::memcpy(&header, block.data() + pos, sizeof(header));
+ pos += TlvHeaderSize;
+ if (pos + header.size > block.size()) {
+ return false;
+ }
+ const u8* payload = block.data() + pos;
+ pos += header.size;
+ switch (header.type) {
+ case MetaNpdm:
+ package.npdm.assign(payload, payload + header.size);
+ break;
+ case MetaIcon:
+ package.icon.assign(payload, payload + header.size);
+ break;
+ case MetaTitleName:
+ package.title_name = std::string(reinterpret_cast<const char*>(payload), header.size);
+ break;
+ case MetaDisplayVersion:
+ package.display_version =
+ std::string(reinterpret_cast<const char*>(payload), header.size);
+ break;
+ case MetaLocalizedTitles:
+ case MetaLocalizedIcons: {
+ const bool is_title = header.type == MetaLocalizedTitles;
+ std::size_t record_pos = 0;
+ while (record_pos < header.size) {
+ const u8 lang_length = payload[record_pos++];
+ if (record_pos + lang_length + sizeof(u32) > header.size) {
+ return false;
+ }
+ const std::string lang(reinterpret_cast<const char*>(payload) + record_pos,
+ lang_length);
+ record_pos += lang_length;
+ u32 data_length = 0;
+ std::memcpy(&data_length, payload + record_pos, sizeof(data_length));
+ record_pos += sizeof(data_length);
+ if (record_pos + data_length > header.size) {
+ return false;
+ }
+ if (is_title) {
+ package.localized_titles.emplace_back(
+ lang, std::string(reinterpret_cast<const char*>(payload) + record_pos,
+ data_length));
+ } else {
+ package.localized_icons.emplace_back(
+ lang, std::vector<u8>(payload + record_pos,
+ payload + record_pos + data_length));
+ }
+ record_pos += data_length;
+ }
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ return true;
+}
+
+std::optional<std::size_t> NupContainer::FindPackage(PackageKind kind,
+ std::optional<u64> title_id) const {
+ for (std::size_t i = 0; i < packages.size(); ++i) {
+ if (packages[i].kind != kind) {
+ continue;
+ }
+ if (title_id && packages[i].title_id != *title_id) {
+ continue;
+ }
+ return i;
+ }
+ return std::nullopt;
+}
+
+std::optional<std::size_t> NupContainer::FindBasePackage() const {
+ return FindPackage(PackageKind::Base);
+}
+
+std::optional<std::size_t> NupContainer::FindBestUpdatePackage() const {
+ std::optional<std::size_t> best;
+ for (std::size_t i = 0; i < packages.size(); ++i) {
+ if (packages[i].kind != PackageKind::Update) {
+ continue;
+ }
+ if (!best || packages[i].version > packages[*best].version) {
+ best = i;
+ }
+ }
+ return best;
+}
+
+VirtualFile NupContainer::OpenFile(std::size_t file_index) {
+ if (file_index >= files.size()) {
+ return nullptr;
+ }
+ const FileNode& node = files[file_index];
+ if (node.size == 0) {
+ return std::make_shared<VectorVfsFile>(std::vector<u8>{}, node.name);
+ }
+ if (node.codec == CodecRaw) {
+ return std::make_shared<OffsetVfsFile>(
+ file, static_cast<std::size_t>(node.extent_size),
+ static_cast<std::size_t>(node.extent_offset), node.name);
+ }
+ return std::make_shared<NupVfsFile>(file, node.extent_offset, node.extent_size, node.size,
+ node.name, cache, file_index);
+}
+
+void NupContainer::BuildDirTree(u32 dir_index, VectorVfsDirectory& out) {
+ const DirNode& node = dirs[dir_index];
+ const u64 end = static_cast<u64>(node.first_child) + node.child_count;
+ for (u64 slot = node.first_child; slot < end; ++slot) {
+ const u32 child = children[slot];
+ if (child >= dirs.size()) {
+ out.AddFile(OpenFile(child - static_cast<u32>(dirs.size())));
+ } else {
+ auto sub = std::make_shared<VectorVfsDirectory>(std::vector<VirtualFile>{},
+ std::vector<VirtualDir>{},
+ dirs[child].name);
+ BuildDirTree(child, *sub);
+ out.AddDirectory(sub);
+ }
+ }
+}
+
+VirtualDir NupContainer::BuildPackageDir(u32 dir_index) {
+ auto out = std::make_shared<VectorVfsDirectory>(std::vector<VirtualFile>{},
+ std::vector<VirtualDir>{}, dirs[dir_index].name);
+ BuildDirTree(dir_index, *out);
+ return out;
+}
+
+VirtualDir NupContainer::GetExeFSDir(std::size_t package_index) {
+ if (package_index >= packages.size() || packages[package_index].exefs_dir == NoDir) {
+ return nullptr;
+ }
+ if (exefs_dir_cache.size() <= package_index) {
+ exefs_dir_cache.resize(packages.size());
+ }
+ if (exefs_dir_cache[package_index] == nullptr) {
+ exefs_dir_cache[package_index] = BuildPackageDir(packages[package_index].exefs_dir);
+ }
+ return exefs_dir_cache[package_index];
+}
+
+VirtualDir NupContainer::GetRomFSDir(std::size_t package_index) {
+ if (package_index >= packages.size() || packages[package_index].romfs_dir == NoDir) {
+ return nullptr;
+ }
+ if (romfs_dir_cache.size() <= package_index) {
+ romfs_dir_cache.resize(packages.size());
+ }
+ if (romfs_dir_cache[package_index] == nullptr) {
+ romfs_dir_cache[package_index] = BuildPackageDir(packages[package_index].romfs_dir);
+ }
+ return romfs_dir_cache[package_index];
+}
+
+VirtualDir NupContainer::MergeShadowDirs(const VirtualDir& base, const VirtualDir& overlay) {
+ if (base == nullptr) {
+ return overlay;
+ }
+ if (overlay == nullptr) {
+ return base;
+ }
+
+ std::map<std::string, VirtualFile, std::less<>> file_map;
+ for (const auto& entry : base->GetFiles()) {
+ file_map.insert_or_assign(entry->GetName(), entry);
+ }
+ for (const auto& entry : overlay->GetFiles()) {
+ file_map.insert_or_assign(entry->GetName(), entry);
+ }
+
+ std::map<std::string, VirtualDir, std::less<>> dir_map;
+ for (const auto& entry : base->GetSubdirectories()) {
+ dir_map.insert_or_assign(entry->GetName(), entry);
+ }
+ for (const auto& entry : overlay->GetSubdirectories()) {
+ const auto it = dir_map.find(entry->GetName());
+ if (it != dir_map.end()) {
+ dir_map.insert_or_assign(entry->GetName(), MergeShadowDirs(it->second, entry));
+ } else {
+ dir_map.insert_or_assign(entry->GetName(), entry);
+ }
+ }
+
+ std::vector<VirtualFile> merged_files;
+ merged_files.reserve(file_map.size());
+ for (const auto& [_, entry] : file_map) {
+ merged_files.push_back(entry);
+ }
+ std::vector<VirtualDir> merged_dirs;
+ merged_dirs.reserve(dir_map.size());
+ for (const auto& [_, entry] : dir_map) {
+ merged_dirs.push_back(entry);
+ }
+ return std::make_shared<VectorVfsDirectory>(std::move(merged_files), std::move(merged_dirs),
+ base->GetName());
+}
+
+std::vector<u8> NupContainer::ReadExtentRaw(const FileNode& node) const {
+ return file->ReadBytes(static_cast<std::size_t>(node.extent_size),
+ static_cast<std::size_t>(node.extent_offset));
+}
+
+Loader::ResultStatus NupContainer::VerifyIntegrity(
+ std::function<bool(std::size_t, std::size_t)> progress_callback) const {
+ const std::size_t total = files.size();
+ for (std::size_t i = 0; i < total; ++i) {
+ const FileNode& node = files[i];
+ std::vector<u8> content;
+ if (node.size == 0) {
+ content = ReadExtentRaw(node);
+ } else if (node.codec == CodecRaw) {
+ content = ReadExtentRaw(node);
+ } else {
+ NupVfsFile view(file, node.extent_offset, node.extent_size, node.size, node.name,
+ cache, i);
+ content = view.ReadAllBytes();
+ }
+ if (content.size() != node.size) {
+ LOG_ERROR(Loader, "NUP integrity check failed for {}: short read", node.name);
+ return Loader::ResultStatus::ErrorIntegrityVerificationFailed;
+ }
+ std::array<u8, 0x20> hash{};
+ if (!Sha256(content.data(), content.size(), hash) || hash != node.sha256) {
+ LOG_ERROR(Loader, "NUP integrity check failed for {}: SHA-256 mismatch", node.name);
+ return Loader::ResultStatus::ErrorIntegrityVerificationFailed;
+ }
+ if (progress_callback && !progress_callback(i + 1, total)) {
+ return Loader::ResultStatus::ErrorIntegrityVerificationFailed;
+ }
+ }
+ return Loader::ResultStatus::Success;
+}
+
+} // namespace FileSys
diff --git a/src/core/file_sys/nup_container.h b/src/core/file_sys/nup_container.h
new file mode 100644
index 0000000000..8787717d47
--- /dev/null
+++ b/src/core/file_sys/nup_container.h
@@ -0,0 +1,211 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <array>
+#include <filesystem>
+#include <functional>
+#include <list>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "common/common_types.h"
+#include "common/fs/file.h"
+#include "core/file_sys/vfs/vfs_types.h"
+#include "core/file_sys/vfs/vfs_vector.h"
+#include "core/loader/loader.h"
+
+namespace FileSys {
+
+/// A zstd member too large for the in-memory cache, decompressed once into a temporary file.
+struct NupSpillFile {
+ ~NupSpillFile();
+
+ std::mutex mutex;
+ std::filesystem::path path;
+ Common::FS::IOFile file;
+ bool attempted{};
+};
+
+/// LRU over decompressed zstd extents, shared by the files of one container.
+struct NupDecompressCache {
+ static constexpr std::size_t DefaultCap = 512 * 1024 * 1024;
+
+ using Entry = std::pair<std::size_t, std::vector<u8>>;
+
+ std::mutex mutex;
+ std::list<Entry> lru;
+ std::unordered_map<std::size_t, std::list<Entry>::iterator> index;
+ std::size_t bytes{};
+ std::size_t cap{DefaultCap};
+ std::filesystem::path spill_dir;
+ std::unordered_map<std::size_t, std::shared_ptr<NupSpillFile>> spills;
+
+ std::vector<u8>* Find(std::size_t file_index) {
+ const auto it = index.find(file_index);
+ if (it == index.end()) {
+ return nullptr;
+ }
+ lru.splice(lru.begin(), lru, it->second);
+ return &it->second->second;
+ }
+
+ /// Takes ownership of `data` only when it is inserted.
+ void Insert(std::size_t file_index, std::vector<u8>& data) {
+ if (index.contains(file_index)) {
+ return;
+ }
+ while (bytes + data.size() > cap && !lru.empty()) {
+ bytes -= lru.back().second.size();
+ index.erase(lru.back().first);
+ lru.pop_back();
+ }
+ if (bytes + data.size() > cap) {
+ return;
+ }
+ bytes += data.size();
+ lru.emplace_front(file_index, std::move(data));
+ index.emplace(file_index, lru.begin());
+ }
+
+ std::shared_ptr<NupSpillFile> GetSpill(std::size_t file_index) {
+ auto& spill = spills[file_index];
+ if (!spill) {
+ spill = std::make_shared<NupSpillFile>();
+ }
+ return spill;
+ }
+};
+
+/**
+ * NUP (NX Package) container: stores loose ExeFS/RomFS trees for a base program,
+ * its updates and add-on content in one file. Layout:
+ *
+ * Superblock (0x80 bytes, magic "NUP\x01")
+ * Table of Contents (packages, dir/file nodes, children array, string pool)
+ * Blob region (per-file extents, raw or zstd codec, 16-byte aligned)
+ *
+ * Per-package meta TLVs carry NPDM bytes, title names (default + localized),
+ * icons and a display version. All integers little-endian.
+ */
+class NupContainer {
+public:
+ static constexpr std::array<u8, 4> HeaderMagic{'N', 'U', 'P', 0x01};
+
+ enum class PackageKind : u8 {
+ Base = 0,
+ Update = 1,
+ Addon = 2,
+ };
+
+
+ struct PackageInfo {
+ u64 title_id{};
+ u32 version{};
+ PackageKind kind{};
+ bool standalone_update{};
+ u32 root_dir{};
+ u32 exefs_dir{};
+ u32 romfs_dir{};
+ u64 meta_offset{};
+ u32 meta_size{};
+
+ std::vector<u8> npdm;
+ std::string title_name;
+ std::vector<std::pair<std::string, std::string>> localized_titles;
+ std::vector<u8> icon;
+ std::vector<std::pair<std::string, std::vector<u8>>> localized_icons;
+ std::string display_version;
+ };
+
+ /// Quick magic check without a full parse.
+ static bool IsNupFile(const VirtualFile& file);
+
+ /// Recursively merges overlay into a copy of base; overlay paths win.
+ static VirtualDir MergeShadowDirs(const VirtualDir& base, const VirtualDir& overlay);
+
+ /// Zstd members larger than half of `memory_cache_cap` are decompressed to a temporary file
+ /// in `spill_dir` (the cache directory by default) instead of being held in memory.
+ explicit NupContainer(VirtualFile file_,
+ std::size_t memory_cache_cap = NupDecompressCache::DefaultCap,
+ std::filesystem::path spill_dir_ = {});
+ ~NupContainer();
+
+ Loader::ResultStatus GetStatus() const {
+ return status;
+ }
+
+ const std::vector<PackageInfo>& GetPackages() const {
+ return packages;
+ }
+
+ std::optional<std::size_t> FindPackage(PackageKind kind,
+ std::optional<u64> title_id = std::nullopt) const;
+ std::optional<std::size_t> FindBasePackage() const;
+ /// Highest version among Update packages, if any.
+ std::optional<std::size_t> FindBestUpdatePackage() const;
+
+ /// Opens a member file by node index; raw extents are zero-copy views into
+ /// the container, zstd extents are lazily decompressed, cached in memory
+ /// or, when large, in a temporary file.
+ VirtualFile OpenFile(std::size_t file_index);
+
+ VirtualDir GetExeFSDir(std::size_t package_index);
+ VirtualDir GetRomFSDir(std::size_t package_index);
+
+ /// Stream-hashes every member file against its stored SHA-256.
+ Loader::ResultStatus VerifyIntegrity(
+ std::function<bool(std::size_t, std::size_t)> progress_callback) const;
+
+ std::size_t GetFileCount() const {
+ return files.size();
+ }
+
+private:
+ struct DirNode {
+ std::string name;
+ u32 first_child{};
+ u32 child_count{};
+ };
+
+ struct FileNode {
+ std::string name;
+ u64 size{};
+ std::array<u8, 0x20> sha256{};
+ u64 extent_offset{};
+ u64 extent_size{};
+ u8 codec{};
+ };
+
+ bool Parse();
+ bool ParseMeta(PackageInfo& package);
+ bool ValidateDirTree() const;
+ void BuildDirTree(u32 dir_index, VectorVfsDirectory& out);
+ VirtualDir BuildPackageDir(u32 dir_index);
+ std::vector<u8> ReadExtentRaw(const FileNode& node) const;
+
+ VirtualFile file;
+ Loader::ResultStatus status{Loader::ResultStatus::ErrorNullFile};
+
+ u64 toc_offset{};
+ u64 toc_size{};
+ u64 blob_offset{};
+ u64 file_size{};
+
+ std::vector<DirNode> dirs;
+ std::vector<FileNode> files;
+ std::vector<u32> children;
+ std::vector<PackageInfo> packages;
+
+ std::vector<VirtualDir> exefs_dir_cache;
+ std::vector<VirtualDir> romfs_dir_cache;
+
+ std::shared_ptr<NupDecompressCache> cache;
+};
+
+} // namespace FileSys
diff --git a/src/core/file_sys/nup_writer.cpp b/src/core/file_sys/nup_writer.cpp
new file mode 100644
index 0000000000..4161969f98
--- /dev/null
+++ b/src/core/file_sys/nup_writer.cpp
@@ -0,0 +1,1552 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "core/file_sys/nup_writer.h"
+
+#include <algorithm>
+#include <array>
+#include <cctype>
+#include <charconv>
+#include <cstring>
+#include <functional>
+#include <limits>
+#include <map>
+#include <memory>
+#include <optional>
+#include <span>
+#include <stdexcept>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
+
+#include <nlohmann/json.hpp>
+#include <openssl/evp.h>
+#include <zstd.h>
+
+#include "common/fs/file.h"
+#include "common/fs/path_util.h"
+#include "core/file_sys/control_metadata.h"
+#include "core/file_sys/vfs/vfs_real.h"
+#include "core/loader/deconstructed_rom_directory.h"
+
+namespace FileSys::Nup {
+namespace {
+
+using Json = nlohmann::json;
+namespace fs = std::filesystem;
+
+constexpr std::array<u8, 4> Magic{'N', 'U', 'P', 0x01};
+constexpr std::array<u8, 4> TocMagic{'T', 'O', 'C', 0x01};
+constexpr u64 SuperblockSize = 0x80;
+constexpr u64 TocHeaderSize = 0x30;
+constexpr u64 PackageRecordSize = 0x40;
+constexpr u64 DirRecordSize = 0x18;
+constexpr u64 FileRecordSize = 0x50;
+constexpr u64 ExtentAlignment = 16;
+constexpr u32 NoDir = 0xFFFFFFFF;
+
+constexpr u8 KindBase = 0;
+constexpr u8 KindUpdate = 1;
+constexpr u8 KindAddon = 2;
+constexpr u8 FlagStandaloneUpdate = 0x01;
+constexpr u8 CodecRaw = 0;
+constexpr u8 CodecZstd = 1;
+
+constexpr u16 TlvNpdm = 0x0001;
+constexpr u16 TlvNacp = 0x0002;
+constexpr u16 TlvIcon = 0x0003;
+constexpr u16 TlvDisplayVersion = 0x0004;
+constexpr u16 TlvTitleName = 0x0005;
+constexpr u16 TlvPublisher = 0x0006;
+constexpr u16 TlvSourceDump = 0x0008;
+
+constexpr int JournalVersion = 1;
+constexpr u64 ProgressStep = 64ULL * 1024 * 1024;
+constexpr size_t MaxChunkFiles = 4096;
+constexpr size_t ReadBufferSize = 4 * 1024 * 1024;
+
+class PackError : public std::runtime_error {
+public:
+ PackError(PackResult result_, const std::string& message)
+ : std::runtime_error(message), result{result_} {}
+ PackResult result;
+};
+
+u64 AlignUp(u64 value, u64 alignment) {
+ return (value + alignment - 1) / alignment * alignment;
+}
+
+std::string ToUtf8(const fs::path& path) {
+ const auto u8 = path.generic_u8string();
+ return {u8.begin(), u8.end()};
+}
+
+fs::path FromUtf8(const std::string& value) {
+ return fs::path{std::u8string{value.begin(), value.end()}};
+}
+
+std::string Lower(std::string value) {
+ std::ranges::transform(value, value.begin(),
+ [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
+ return value;
+}
+
+fs::path WithSuffix(fs::path path, std::string_view suffix) {
+ path += suffix;
+ return path;
+}
+
+bool IsFile(const fs::path& path) {
+ std::error_code ec;
+ return fs::is_regular_file(path, ec);
+}
+
+fs::path FindDirectory(const fs::path& parent, std::string_view name) {
+ std::error_code ec;
+ if (parent.empty() || !fs::is_directory(parent, ec)) {
+ return {};
+ }
+ const auto wanted = Lower(std::string{name});
+ for (const auto& entry : fs::directory_iterator(parent, ec)) {
+ if (entry.is_directory(ec) && Lower(ToUtf8(entry.path().filename())) == wanted) {
+ return entry.path();
+ }
+ }
+ return {};
+}
+
+std::optional<u64> ParseDecimal(const std::string& name) {
+ u64 value{};
+ const auto* end = name.data() + name.size();
+ const auto [ptr, ec] = std::from_chars(name.data(), end, value);
+ if (name.empty() || ec != std::errc{} || ptr != end) {
+ return std::nullopt;
+ }
+ return value;
+}
+
+// The exefs folder a dump boots from: the newest updates/<version>, else base, else the root.
+// Each must hold main, main.npdm and a romfs folder next to it or under base.
+bool HasBootableExeFS(const fs::path& root) {
+ const auto bootable = [](const fs::path& exefs) {
+ return !exefs.empty() && IsFile(exefs / "main") && IsFile(exefs / "main.npdm");
+ };
+ fs::path exefs;
+ std::optional<u64> best_version;
+ if (const auto updates = FindDirectory(root, "updates"); !updates.empty()) {
+ std::error_code ec;
+ for (const auto& entry : fs::directory_iterator(updates, ec)) {
+ const auto version = ParseDecimal(ToUtf8(entry.path().filename()));
+ const auto candidate = FindDirectory(entry.path(), "exefs");
+ if (version && bootable(candidate) && (!best_version || *version > *best_version)) {
+ best_version = version;
+ exefs = candidate;
+ }
+ }
+ }
+ const auto base = FindDirectory(root, "base");
+ if (exefs.empty()) {
+ exefs = FindDirectory(base, "exefs");
+ }
+ if (exefs.empty()) {
+ exefs = FindDirectory(root, "exefs");
+ }
+ if (!bootable(exefs)) {
+ return false;
+ }
+ return !FindDirectory(exefs.parent_path(), "romfs").empty() ||
+ !FindDirectory(base, "romfs").empty();
+}
+
+std::optional<u64> ParseTitleId(const std::string& name) {
+ u64 value{};
+ const auto* end = name.data() + name.size();
+ const auto [ptr, ec] = std::from_chars(name.data(), end, value, 16);
+ if (name.size() != 16 || ec != std::errc{} || ptr != end) {
+ return std::nullopt;
+ }
+ return value;
+}
+
+std::string ToHex(std::span<const u8> data) {
+ static constexpr char digits[] = "0123456789abcdef";
+ std::string out;
+ out.reserve(data.size() * 2);
+ for (const u8 byte : data) {
+ out += digits[byte >> 4];
+ out += digits[byte & 0xF];
+ }
+ return out;
+}
+
+std::vector<u8> FromHex(const std::string& hex) {
+ if (hex.size() % 2 != 0) {
+ throw PackError(PackResult::IoError, "malformed journal");
+ }
+ std::vector<u8> out(hex.size() / 2);
+ for (size_t i = 0; i < out.size(); ++i) {
+ const auto [ptr, ec] = std::from_chars(hex.data() + i * 2, hex.data() + i * 2 + 2, out[i], 16);
+ if (ec != std::errc{} || ptr != hex.data() + i * 2 + 2) {
+ throw PackError(PackResult::IoError, "malformed journal");
+ }
+ }
+ return out;
+}
+
+std::vector<u8> ReadWholeFile(const fs::path& path) {
+ Common::FS::IOFile in{path, Common::FS::FileAccessMode::Read};
+ if (!in.IsOpen()) {
+ return {};
+ }
+ std::vector<u8> data(in.GetSize());
+ if (in.ReadSpan(std::span{data}) != data.size()) {
+ return {};
+ }
+ return data;
+}
+
+// Same as pack.py: the program ID at ACI0 + 0x10, whatever the NPDM header revision.
+std::optional<u64> NpdmProgramId(std::span<const u8> npdm) {
+ static constexpr std::array<u8, 4> aci0{'A', 'C', 'I', '0'};
+ const auto it = std::search(npdm.begin(), npdm.end(), aci0.begin(), aci0.end());
+ const auto offset = static_cast<size_t>(it - npdm.begin());
+ if (it == npdm.end() || offset + 0x18 > npdm.size()) {
+ return std::nullopt;
+ }
+ u64 id = 0;
+ for (size_t i = 0; i < 8; ++i) {
+ id |= static_cast<u64>(npdm[offset + 0x10 + i]) << (8 * i);
+ }
+ return id;
+}
+
+void PutLE(std::vector<u8>& buffer, size_t offset, u64 value, size_t bytes) {
+ for (size_t i = 0; i < bytes; ++i) {
+ buffer[offset + i] = static_cast<u8>(value >> (8 * i));
+ }
+}
+
+void PutBytes(std::vector<u8>& buffer, size_t offset, std::span<const u8> bytes) {
+ std::memcpy(buffer.data() + offset, bytes.data(), bytes.size());
+}
+
+using Digest = std::array<u8, 32>;
+
+class Sha256 {
+public:
+ Sha256() : ctx{EVP_MD_CTX_new()} {
+ if (!ctx) {
+ throw PackError(PackResult::IoError, "cannot initialise SHA-256");
+ }
+ Reset();
+ }
+ ~Sha256() {
+ EVP_MD_CTX_free(ctx);
+ }
+ Sha256(const Sha256&) = delete;
+ Sha256& operator=(const Sha256&) = delete;
+
+ void Reset() {
+ if (EVP_DigestInit_ex(ctx, EVP_sha256(), nullptr) != 1) {
+ throw PackError(PackResult::IoError, "cannot initialise SHA-256");
+ }
+ }
+
+ void Update(const void* data, size_t size) {
+ EVP_DigestUpdate(ctx, data, size);
+ }
+
+ Digest Final() {
+ Digest digest{};
+ unsigned int length{};
+ EVP_DigestFinal_ex(ctx, digest.data(), &length);
+ return digest;
+ }
+
+private:
+ EVP_MD_CTX* ctx;
+};
+
+class ZstdContext {
+public:
+ // Archives must be byte-identical across platforms, so compression is always
+ // single-threaded (zstd's multithreaded frames differ) and every frame parameter is set
+ // explicitly instead of relying on library defaults.
+ explicit ZstdContext(int level) : cctx{ZSTD_createCCtx()} {
+ if (!cctx || ZSTD_isError(ZSTD_CCtx_setParameter(cctx, ZSTD_c_compressionLevel, level)) ||
+ ZSTD_isError(ZSTD_CCtx_setParameter(cctx, ZSTD_c_nbWorkers, 0)) ||
+ ZSTD_isError(ZSTD_CCtx_setParameter(cctx, ZSTD_c_contentSizeFlag, 1)) ||
+ ZSTD_isError(ZSTD_CCtx_setParameter(cctx, ZSTD_c_checksumFlag, 0)) ||
+ ZSTD_isError(ZSTD_CCtx_setParameter(cctx, ZSTD_c_dictIDFlag, 0))) {
+ throw PackError(PackResult::IoError, "cannot initialise zstd");
+ }
+ }
+ ~ZstdContext() {
+ ZSTD_freeCCtx(cctx);
+ }
+ ZstdContext(const ZstdContext&) = delete;
+ ZstdContext& operator=(const ZstdContext&) = delete;
+
+ // The pledged size makes zstd record the content size in the frame header, which the
+ // format requires.
+ void Begin(u64 size) {
+ if (ZSTD_isError(ZSTD_CCtx_reset(cctx, ZSTD_reset_session_only)) ||
+ ZSTD_isError(ZSTD_CCtx_setPledgedSrcSize(cctx, size))) {
+ throw PackError(PackResult::IoError, "cannot start zstd frame");
+ }
+ }
+
+ ZSTD_CCtx* get() const {
+ return cctx;
+ }
+
+private:
+ ZSTD_CCtx* cctx;
+};
+
+// One package and the metadata it is written with. The plan is everything that must survive a
+// low-storage run deleting its sources, so it is what the journal stores.
+struct PlanPackage {
+ u8 kind{};
+ u8 flags{};
+ u64 title_id{};
+ u32 version{};
+ std::vector<u8> meta;
+};
+
+struct PlanEntry {
+ u32 package{};
+ std::string path; // inside the package tree, e.g. "romfs/Data/file.bin"
+ std::string source; // relative to the dump root
+ bool directory{};
+};
+
+struct Plan {
+ std::vector<PlanPackage> packages;
+ std::vector<PlanEntry> entries;
+};
+
+struct Extent {
+ u64 size{};
+ Digest sha256{};
+ u64 offset{};
+ u64 stored_size{};
+ u8 codec{};
+};
+
+struct Layout {
+ struct Dir {
+ std::string name;
+ u32 first_child{};
+ u32 child_count{};
+ };
+ struct File {
+ std::string name;
+ size_t entry{};
+ };
+ struct Package {
+ u32 root_dir{};
+ u32 exefs_dir = NoDir;
+ u32 romfs_dir = NoDir;
+ };
+ std::vector<Dir> dirs;
+ std::vector<File> files;
+ std::vector<u64> children; // FileTag | file index, or dir index
+ std::vector<Package> packages;
+ std::string pool;
+ std::unordered_map<std::string, u32> pool_index;
+ u64 toc_size{};
+ u64 blob_offset{};
+
+ static constexpr u64 FileTag = 1ULL << 32;
+
+ u32 Intern(const std::string& name) {
+ const auto [it, inserted] = pool_index.try_emplace(name, static_cast<u32>(pool.size()));
+ if (inserted) {
+ pool += name;
+ }
+ return it->second;
+ }
+};
+
+// Children sorted by the bytes of their names: std::string compares as unsigned char.
+struct TreeDir {
+ struct Child {
+ std::unique_ptr<TreeDir> dir;
+ size_t file_entry{};
+ };
+ std::string name;
+ std::map<std::string, Child> children;
+
+ TreeDir* Descend(const std::string& component) {
+ auto& child = children[component];
+ if (!child.dir) {
+ child.dir = std::make_unique<TreeDir>();
+ child.dir->name = component;
+ }
+ return child.dir.get();
+ }
+};
+
+// Lays out nodes as pack.py does: directories in pre-order, each directory's child slots
+// contiguous, its files numbered in slot order before its subdirectories are visited.
+u32 AssignDir(TreeDir& dir, Layout& layout) {
+ const auto id = static_cast<u32>(layout.dirs.size());
+ layout.dirs.push_back({dir.name, static_cast<u32>(layout.children.size()),
+ static_cast<u32>(dir.children.size())});
+ const size_t slot_base = layout.children.size();
+ layout.children.resize(slot_base + dir.children.size());
+ size_t slot = slot_base;
+ for (auto& [name, child] : dir.children) {
+ if (!child.dir) {
+ layout.children[slot] = Layout::FileTag | layout.files.size();
+ layout.files.push_back({name, child.file_entry});
+ }
+ ++slot;
+ }
+ slot = slot_base;
+ for (auto& [name, child] : dir.children) {
+ if (child.dir) {
+ layout.children[slot] = AssignDir(*child.dir, layout);
+ }
+ ++slot;
+ }
+ return id;
+}
+
+Layout BuildLayout(const Plan& plan) {
+ std::vector<TreeDir> roots(plan.packages.size());
+ for (size_t i = 0; i < plan.entries.size(); ++i) {
+ const auto& entry = plan.entries[i];
+ TreeDir* dir = &roots.at(entry.package);
+ size_t start = 0;
+ while (true) {
+ const auto slash = entry.path.find('/', start);
+ const auto component = entry.path.substr(start, slash - start);
+ if (slash == std::string::npos) {
+ if (entry.directory) {
+ dir->Descend(component);
+ } else {
+ auto& child = dir->children[component];
+ if (child.dir) {
+ throw PackError(PackResult::InvalidSource,
+ "a file and a folder share the name " + entry.path);
+ }
+ child.file_entry = i;
+ }
+ break;
+ }
+ dir = dir->Descend(component);
+ start = slash + 1;
+ }
+ }
+
+ Layout layout;
+ for (auto& root : roots) {
+ Layout::Package package;
+ package.root_dir = AssignDir(root, layout);
+ const auto& root_dir = layout.dirs[package.root_dir];
+ for (u32 slot = root_dir.first_child; slot < root_dir.first_child + root_dir.child_count;
+ ++slot) {
+ const u64 child = layout.children[slot];
+ if (child & Layout::FileTag) {
+ continue;
+ }
+ const auto id = static_cast<u32>(child);
+ if (layout.dirs[id].name == "exefs") {
+ package.exefs_dir = id;
+ } else if (layout.dirs[id].name == "romfs") {
+ package.romfs_dir = id;
+ }
+ }
+ layout.packages.push_back(package);
+ }
+
+ if (layout.dirs.size() + layout.files.size() >= NoDir) {
+ throw PackError(PackResult::InvalidSource, "the dump has too many files");
+ }
+ for (const auto& dir : layout.dirs) {
+ layout.Intern(dir.name);
+ }
+ for (const auto& file : layout.files) {
+ layout.Intern(file.name);
+ }
+ if (layout.pool.size() > std::numeric_limits<u32>::max()) {
+ throw PackError(PackResult::InvalidSource, "the dump's file names are too long");
+ }
+
+ const u64 unpadded = TocHeaderSize + plan.packages.size() * PackageRecordSize +
+ layout.dirs.size() * DirRecordSize + layout.files.size() * FileRecordSize +
+ layout.children.size() * sizeof(u32) + layout.pool.size();
+ layout.toc_size = AlignUp(unpadded, ExtentAlignment);
+ layout.blob_offset = AlignUp(SuperblockSize + layout.toc_size, ExtentAlignment);
+ return layout;
+}
+
+std::vector<u8> SerializeTlvs(std::vector<std::pair<u16, std::vector<u8>>> records) {
+ std::ranges::stable_sort(records, {}, &std::pair<u16, std::vector<u8>>::first);
+ std::vector<u8> out;
+ for (const auto& [type, payload] : records) {
+ const size_t at = out.size();
+ out.resize(at + 8 + payload.size());
+ PutLE(out, at, type, 2);
+ PutLE(out, at + 4, payload.size(), 4);
+ PutBytes(out, at + 8, payload);
+ }
+ return out;
+}
+
+std::vector<u8> StringBytes(const std::string& value) {
+ return {value.begin(), value.end()};
+}
+
+// The dump's own loader supplies control data and the display version, the same way a loose
+// dump reports them when it boots (control.nacp beside it, else the RomFS's version tag).
+std::unique_ptr<Loader::AppLoader_DeconstructedRomDirectory> OpenLoader(
+ RealVfsFilesystem& vfs, const fs::path& package_dir) {
+ const auto exefs = FindDirectory(package_dir, "exefs");
+ if (exefs.empty() || !IsFile(exefs / "main")) {
+ return nullptr;
+ }
+ auto main = vfs.OpenFile(Common::FS::PathToUTF8String(exefs / "main"), OpenMode::Read);
+ if (!main) {
+ return nullptr;
+ }
+ return std::make_unique<Loader::AppLoader_DeconstructedRomDirectory>(std::move(main));
+}
+
+class Discovery {
+public:
+ Discovery(const fs::path& dump_root_, std::vector<fs::path> excluded_,
+ const Metadata& overrides_)
+ : dump_root{dump_root_}, excluded{std::move(excluded_)}, overrides{overrides_} {}
+
+ Plan Run() {
+ const auto base = FindDirectory(dump_root, "base");
+ const auto updates = FindDirectory(dump_root, "updates");
+ const auto dlc = FindDirectory(dump_root, "dlc");
+
+ std::optional<u64> base_title_id;
+ if (!base.empty() && HasContent(base)) {
+ base_title_id = AddPackage(KindBase, base, 0, {});
+ } else if (updates.empty() && HasContent(dump_root)) {
+ base_title_id = AddPackage(KindBase, dump_root, 0, {"base", "updates", "dlc"});
+ }
+
+ std::vector<std::pair<u64, fs::path>> versions;
+ std::error_code ec;
+ if (!updates.empty()) {
+ for (const auto& entry : fs::directory_iterator(updates, ec)) {
+ const auto version = ParseDecimal(ToUtf8(entry.path().filename()));
+ if (version && entry.is_directory(ec) && HasContent(entry.path())) {
+ if (*version > std::numeric_limits<u32>::max()) {
+ throw PackError(PackResult::InvalidSource,
+ "update version out of range: " + ToUtf8(entry.path()));
+ }
+ versions.emplace_back(*version, entry.path());
+ }
+ }
+ }
+ std::ranges::sort(versions);
+ const u8 update_flags = base_title_id ? 0 : FlagStandaloneUpdate;
+ for (const auto& [version, path] : versions) {
+ AddPackage(KindUpdate, path, static_cast<u32>(version), {}, update_flags,
+ base_title_id.value_or(0));
+ }
+
+ if (!dlc.empty()) {
+ std::vector<std::pair<std::string, fs::path>> addons;
+ for (const auto& title : fs::directory_iterator(dlc, ec)) {
+ const auto title_name = ToUtf8(title.path().filename());
+ if (!ParseTitleId(title_name) || !title.is_directory(ec)) {
+ continue;
+ }
+ for (const auto& content : fs::directory_iterator(title.path(), ec)) {
+ if (content.is_directory(ec) && HasContent(content.path())) {
+ addons.emplace_back(title_name + "/" + ToUtf8(content.path().filename()),
+ content.path());
+ }
+ }
+ }
+ std::ranges::sort(addons);
+ for (const auto& [name, path] : addons) {
+ AddPackage(KindAddon, path, 0, {}, 0, *ParseTitleId(name.substr(0, 16)));
+ }
+ }
+
+ if (plan.packages.empty()) {
+ throw PackError(PackResult::InvalidSource, "no packages found in the dump");
+ }
+ AddApplicationMetadata();
+ for (size_t i = 0; i < plan.packages.size(); ++i) {
+ auto& tlvs = package_tlvs[i];
+ tlvs.emplace_back(TlvSourceDump, StringBytes(ToUtf8(dump_root.filename())));
+ plan.packages[i].meta = SerializeTlvs(std::move(tlvs));
+ }
+ return std::move(plan);
+ }
+
+private:
+ static bool HasContent(const fs::path& dir) {
+ return !FindDirectory(dir, "exefs").empty() || !FindDirectory(dir, "romfs").empty();
+ }
+
+ // Returns the package's title ID: the NPDM program ID, else the folder's or base's.
+ u64 AddPackage(u8 kind, const fs::path& dir, u32 version,
+ const std::vector<std::string>& skip, u8 flags = 0, u64 fallback_id = 0) {
+ const auto index = static_cast<u32>(plan.packages.size());
+ PlanPackage package{kind, flags, fallback_id, version, {}};
+ auto& tlvs = package_tlvs.emplace_back();
+
+ const auto exefs = FindDirectory(dir, "exefs");
+ const auto romfs = FindDirectory(dir, "romfs");
+ fs::path npdm_path = exefs.empty() ? fs::path{} : exefs / "main.npdm";
+ if (!IsFile(npdm_path)) {
+ npdm_path = dir / "main.npdm";
+ }
+ if (IsFile(npdm_path)) {
+ auto npdm = ReadWholeFile(npdm_path);
+ if (const auto id = NpdmProgramId(npdm); kind != KindAddon && id && *id != 0) {
+ package.title_id = *id;
+ }
+ tlvs.emplace_back(TlvNpdm, std::move(npdm));
+ }
+
+ if (kind != KindAddon) {
+ if (auto loader = OpenLoader(vfs, dir)) {
+ std::string display_version;
+ if (loader->ReadDisplayVersion(display_version) == Loader::ResultStatus::Success &&
+ !display_version.empty()) {
+ tlvs.emplace_back(TlvDisplayVersion, StringBytes(display_version));
+ }
+ loaders.emplace_back(index, std::move(loader));
+ }
+ }
+ plan.packages.push_back(std::move(package));
+
+ if (!exefs.empty()) {
+ CollectTree(index, exefs, "exefs");
+ }
+ if (!romfs.empty()) {
+ CollectTree(index, romfs, "romfs");
+ }
+ std::error_code ec;
+ for (const auto& entry : fs::directory_iterator(dir, ec)) {
+ const auto name = ToUtf8(entry.path().filename());
+ const auto lower = Lower(name);
+ if (entry.path() == exefs || entry.path() == romfs ||
+ std::ranges::find(skip, lower) != skip.end()) {
+ continue;
+ }
+ if (entry.is_directory(ec)) {
+ CollectTree(index, entry.path(), "data/" + name);
+ } else {
+ AddFile(index, entry, "data/" + name);
+ }
+ }
+ return plan.packages.back().title_id;
+ }
+
+ // Names, icon and control data belong to the base package, or the first one without a base.
+ // Our dumps rarely carry a control folder; when one does, the base's own wins, then updates.
+ void AddApplicationMetadata() {
+ const auto app = static_cast<u32>(std::distance(
+ plan.packages.begin(), std::ranges::find(plan.packages, KindBase, &PlanPackage::kind)));
+ const u32 target = app < plan.packages.size() ? app : 0;
+ std::ranges::stable_sort(loaders, [target](const auto& a, const auto& b) {
+ return (a.first == target) > (b.first == target);
+ });
+
+ auto& tlvs = package_tlvs[target];
+ bool have_control = false;
+ bool have_icon = false;
+ for (const auto& [index, loader] : loaders) {
+ NACP control;
+ if (!have_control &&
+ loader->ReadControlData(control) == Loader::ResultStatus::Success) {
+ have_control = true;
+ tlvs.emplace_back(TlvNacp, control.GetRawBytes());
+ if (const auto name = control.GetApplicationName(); !name.empty()) {
+ tlvs.emplace_back(TlvTitleName, StringBytes(name));
+ }
+ if (const auto publisher = control.GetDeveloperName(); !publisher.empty()) {
+ tlvs.emplace_back(TlvPublisher, StringBytes(publisher));
+ }
+ }
+ std::vector<u8> icon;
+ if (!have_icon && loader->ReadIcon(icon) == Loader::ResultStatus::Success &&
+ !icon.empty()) {
+ have_icon = true;
+ tlvs.emplace_back(TlvIcon, std::move(icon));
+ }
+ }
+
+ if (!overrides.title_name.empty()) {
+ SetTlv(tlvs, TlvTitleName, StringBytes(overrides.title_name));
+ }
+ if (!overrides.icon.empty()) {
+ SetTlv(tlvs, TlvIcon, overrides.icon);
+ }
+ if (!overrides.display_version.empty()) {
+ // The loader reports the newest update's version when there is one.
+ SetTlv(tlvs, TlvDisplayVersion, StringBytes(overrides.display_version));
+ std::optional<u32> newest;
+ for (u32 i = 0; i < plan.packages.size(); ++i) {
+ if (plan.packages[i].kind == KindUpdate &&
+ (!newest || plan.packages[i].version >= plan.packages[*newest].version)) {
+ newest = i;
+ }
+ }
+ if (newest && *newest != target) {
+ SetTlv(package_tlvs[*newest], TlvDisplayVersion,
+ StringBytes(overrides.display_version));
+ }
+ }
+ }
+
+ static void SetTlv(std::vector<std::pair<u16, std::vector<u8>>>& tlvs, u16 type,
+ std::vector<u8> payload) {
+ std::erase_if(tlvs, [type](const auto& tlv) { return tlv.first == type; });
+ tlvs.emplace_back(type, std::move(payload));
+ }
+
+ bool IsExcluded(const fs::path& path) const {
+ const auto normal = fs::absolute(path).lexically_normal();
+ return std::ranges::find(excluded, normal) != excluded.end();
+ }
+
+ void AddFile(u32 package, const fs::directory_entry& entry, const std::string& path) {
+ std::error_code ec;
+ if (entry.is_symlink(ec)) {
+ throw PackError(PackResult::InvalidSource,
+ "symlinks are not supported: " + ToUtf8(entry.path()));
+ }
+ if (!entry.is_regular_file(ec)) {
+ throw PackError(PackResult::InvalidSource,
+ "unsupported file type: " + ToUtf8(entry.path()));
+ }
+ // Dump-tool listings are excluded by the spec.
+ if (entry.path().filename() == ".listing" || IsExcluded(entry.path())) {
+ return;
+ }
+ plan.entries.push_back({package, path, Relative(entry.path()), false});
+ }
+
+ void CollectTree(u32 package, const fs::path& root, const std::string& prefix) {
+ plan.entries.push_back({package, prefix, Relative(root), true});
+ for (auto it = fs::recursive_directory_iterator(root);
+ it != fs::recursive_directory_iterator(); ++it) {
+ const auto& entry = *it;
+ const auto path = prefix + "/" + ToUtf8(fs::relative(entry.path(), root));
+ if (!entry.is_symlink() && entry.is_directory()) {
+ plan.entries.push_back({package, path, Relative(entry.path()), true});
+ } else {
+ AddFile(package, entry, path);
+ }
+ }
+ }
+
+ std::string Relative(const fs::path& path) const {
+ return ToUtf8(fs::relative(path, dump_root));
+ }
+
+ fs::path dump_root;
+ std::vector<fs::path> excluded;
+ Metadata overrides;
+ RealVfsFilesystem vfs;
+ Plan plan;
+ std::vector<std::vector<std::pair<u16, std::vector<u8>>>> package_tlvs;
+ std::vector<std::pair<u32, std::unique_ptr<Loader::AppLoader_DeconstructedRomDirectory>>>
+ loaders;
+};
+
+Json PlanToJson(const Plan& plan) {
+ Json packages = Json::array();
+ for (const auto& package : plan.packages) {
+ packages.push_back(Json{{"kind", package.kind},
+ {"flags", package.flags},
+ {"title_id", package.title_id},
+ {"version", package.version},
+ {"meta", ToHex(package.meta)}});
+ }
+ Json entries = Json::array();
+ for (const auto& entry : plan.entries) {
+ entries.push_back(Json{{"package", entry.package},
+ {"path", entry.path},
+ {"source", entry.source},
+ {"directory", entry.directory}});
+ }
+ return Json{{"format", "nup-journal"},
+ {"version", JournalVersion},
+ {"packages", packages},
+ {"entries", entries}};
+}
+
+Plan PlanFromJson(const Json& json) {
+ if (json.at("format") != "nup-journal" || json.at("version") != JournalVersion) {
+ throw PackError(PackResult::IoError, "unsupported journal");
+ }
+ Plan plan;
+ for (const auto& package : json.at("packages")) {
+ plan.packages.push_back({package.at("kind").get<u8>(), package.at("flags").get<u8>(),
+ package.at("title_id").get<u64>(),
+ package.at("version").get<u32>(),
+ FromHex(package.at("meta").get<std::string>())});
+ }
+ for (const auto& entry : json.at("entries")) {
+ plan.entries.push_back({entry.at("package").get<u32>(), entry.at("path").get<std::string>(),
+ entry.at("source").get<std::string>(),
+ entry.at("directory").get<bool>()});
+ }
+ return plan;
+}
+
+void RemoveFile(const fs::path& path) {
+ std::error_code ec;
+ fs::remove(path, ec);
+}
+
+bool IsEmptyDirectory(const fs::path& path) {
+ std::error_code ec;
+ return fs::is_directory(path, ec) && fs::is_empty(path, ec) && !ec;
+}
+
+void RemoveEmptyTree(const fs::path& root) {
+ std::error_code ec;
+ std::vector<fs::path> directories;
+ for (auto it = fs::recursive_directory_iterator(root, ec);
+ !ec && it != fs::recursive_directory_iterator(); it.increment(ec)) {
+ if (it->is_directory(ec)) {
+ directories.push_back(it->path());
+ }
+ }
+ for (auto it = directories.rbegin(); it != directories.rend(); ++it) {
+ if (IsEmptyDirectory(*it)) {
+ fs::remove(*it, ec);
+ }
+ }
+ if (IsEmptyDirectory(root)) {
+ fs::remove(root, ec);
+ }
+}
+
+void RemoveEmptyParents(fs::path path, const fs::path& dump_root) {
+ while (!path.empty() && path.has_relative_path()) {
+ std::error_code ec;
+ if (fs::equivalent(path, dump_root, ec) || ec || !IsEmptyDirectory(path)) {
+ break;
+ }
+ fs::remove(path, ec);
+ path = path.parent_path();
+ }
+}
+
+class Packer {
+public:
+ Packer(const fs::path& dump_root_, const fs::path& archive_, const PackOptions& options_,
+ const std::function<void(const PackProgress&)>& progress_,
+ const std::atomic_bool* cancel_)
+ : dump_root{dump_root_}, archive{archive_}, part{WithSuffix(archive_, ".part")},
+ journal{WithSuffix(archive_, ".journal")}, options{options_}, progress_cb{progress_},
+ cancel{cancel_} {}
+
+ PackResult Run() {
+ std::error_code ec;
+ if (fs::exists(archive, ec)) {
+ throw PackError(PackResult::IoError, "archive already exists: " + ToUtf8(archive));
+ }
+ // A journal without a complete plan line was cut short before anything was deleted.
+ const bool resume =
+ options.delete_sources && HasResumableJournal(archive) && LoadJournal();
+ if (resume) {
+ OpenForResume();
+ } else {
+ if (!IsLooseDump(dump_root)) {
+ throw PackError(PackResult::InvalidSource,
+ "expected exefs/main, exefs/main.npdm and a romfs folder");
+ }
+ std::vector<fs::path> excluded;
+ for (const auto& path : {archive, part, journal}) {
+ excluded.push_back(fs::absolute(path).lexically_normal());
+ }
+ plan = Discovery{dump_root, std::move(excluded), options.metadata}.Run();
+ layout = BuildLayout(plan);
+ OpenNew();
+ }
+ extents.resize(layout.files.size());
+ done.resize(layout.files.size());
+ for (const auto& [index, extent] : journaled) {
+ extents[index] = extent;
+ done[index] = true;
+ Remember(extent);
+ }
+ CountTotals();
+ Report();
+
+ for (size_t i = 0; i < layout.files.size(); ++i) {
+ if (done[i]) {
+ continue;
+ }
+ PackFile(i);
+ if (options.delete_sources &&
+ (chunk_bytes >= options.chunk_bytes || chunk.size() >= MaxChunkFiles)) {
+ Checkpoint();
+ }
+ }
+ if (options.delete_sources) {
+ Checkpoint();
+ }
+
+ Finish();
+ out.Close();
+ fs::rename(part, archive, ec);
+ if (ec) {
+ throw PackError(PackResult::IoError, "cannot rename archive: " + ec.message());
+ }
+
+ if (options.delete_sources) {
+ RemoveFile(journal);
+ RemoveSourceFolders();
+ }
+ return PackResult::Success;
+ }
+
+ void Abort() {
+ out.Close();
+ if (!options.delete_sources) {
+ RemoveFile(part);
+ }
+ }
+
+private:
+ void OpenNew() {
+ RemoveFile(journal);
+ out.Open(part, Common::FS::FileAccessMode::Write);
+ if (!out.IsOpen()) {
+ throw PackError(PackResult::IoError, "cannot create " + ToUtf8(part));
+ }
+ WriteZeros(layout.blob_offset);
+ cursor = layout.blob_offset;
+ if (options.delete_sources) {
+ AppendJournal(PlanToJson(plan).dump() + '\n');
+ }
+ }
+
+ bool LoadJournal() {
+ const auto contents =
+ Common::FS::ReadStringFromFile(journal, Common::FS::FileType::BinaryFile);
+ size_t start = 0;
+ bool have_plan = false;
+ while (start < contents.size()) {
+ const auto line_end = contents.find('\n', start);
+ if (line_end == std::string::npos) {
+ break;
+ }
+ const auto line = std::string_view{contents}.substr(start, line_end - start);
+ start = line_end + 1;
+ Json json;
+ try {
+ json = Json::parse(line);
+ } catch (const Json::exception&) {
+ // Only the tail can be torn: the journal is synced before sources are deleted.
+ break;
+ }
+ if (!have_plan) {
+ plan = PlanFromJson(json);
+ layout = BuildLayout(plan);
+ resume_end = layout.blob_offset;
+ have_plan = true;
+ continue;
+ }
+ const auto index = json.at("file").get<size_t>();
+ const auto sha = FromHex(json.at("sha256").get<std::string>());
+ if (index >= layout.files.size() || sha.size() != 32) {
+ throw PackError(PackResult::IoError, "malformed journal");
+ }
+ Extent extent{json.at("size").get<u64>(), {}, json.at("offset").get<u64>(),
+ json.at("stored_size").get<u64>(), json.at("codec").get<u8>()};
+ std::ranges::copy(sha, extent.sha256.begin());
+ resume_end = std::max(resume_end, extent.offset + extent.stored_size);
+ journaled.emplace_back(index, extent);
+ }
+ return have_plan;
+ }
+
+ void OpenForResume() {
+ const u64 end = resume_end;
+ out.Open(part, Common::FS::FileAccessMode::ReadWrite);
+ if (!out.IsOpen()) {
+ throw PackError(PackResult::IoError, "cannot open " + ToUtf8(part));
+ }
+ if (out.GetSize() < end) {
+ throw PackError(PackResult::IoError, "partial archive is shorter than its journal; "
+ "delete " +
+ ToUtf8(part) + " and " + ToUtf8(journal) +
+ " to start over");
+ }
+ if (!out.SetSize(end) || !out.Seek(static_cast<s64>(end))) {
+ throw PackError(PackResult::IoError, "cannot truncate " + ToUtf8(part));
+ }
+ cursor = end;
+
+ // Journaled before an interruption, but the delete never happened.
+ for (const auto& [index, extent] : journaled) {
+ RemoveFile(SourceOf(index));
+ }
+ }
+
+ fs::path SourceOf(size_t file_index) const {
+ return dump_root / FromUtf8(plan.entries[layout.files[file_index].entry].source);
+ }
+
+ void CountTotals() {
+ for (size_t i = 0; i < layout.files.size(); ++i) {
+ ++progress.files_total;
+ if (done[i]) {
+ progress.bytes_total += extents[i].size;
+ progress.bytes_done += extents[i].size;
+ ++progress.files_done;
+ } else {
+ std::error_code ec;
+ const auto size = fs::file_size(SourceOf(i), ec);
+ if (ec) {
+ throw PackError(PackResult::InvalidSource,
+ "missing source file: " + ToUtf8(SourceOf(i)));
+ }
+ progress.bytes_total += size;
+ }
+ }
+ }
+
+ void Remember(const Extent& extent) {
+ if (extent.size == 0) {
+ return;
+ }
+ sizes_seen.insert(extent.size);
+ by_hash.try_emplace(extent.sha256, extent);
+ }
+
+ bool Cancelled() const {
+ return cancel && cancel->load();
+ }
+
+ void WriteZeros(u64 count) {
+ static constexpr std::array<u8, 4096> zeros{};
+ while (count > 0) {
+ const auto n = static_cast<size_t>(std::min<u64>(count, zeros.size()));
+ Write(zeros.data(), n);
+ count -= n;
+ }
+ }
+
+ void Write(const u8* data, size_t size) {
+ if (out.WriteSpan(std::span<const u8>{data, size}) != size) {
+ throw PackError(PackResult::IoError, "write failed, the disk may be full");
+ }
+ }
+
+ void AddProgress(u64 bytes) {
+ progress.bytes_done += bytes;
+ if (progress.bytes_done - last_report >= ProgressStep) {
+ Report();
+ }
+ }
+
+ // Reads the whole source through `sink`, checking that it still has the expected size. A file
+ // read more than once (hashed for dedup, or compressed and then stored raw) counts once.
+ template <typename Sink>
+ void ReadSource(Common::FS::IOFile& in, u64 size, Sink&& sink) {
+ if (!in.Seek(0)) {
+ throw PackError(PackResult::IoError, "cannot seek source");
+ }
+ buffer.resize(ReadBufferSize);
+ u64 total = 0;
+ while (true) {
+ if (Cancelled()) {
+ throw PackError(PackResult::Cancelled, "cancelled");
+ }
+ const auto read = in.ReadSpan(std::span{buffer});
+ if (read == 0) {
+ break;
+ }
+ total += read;
+ if (total > size) {
+ break;
+ }
+ if (total > file_counted) {
+ AddProgress(total - file_counted);
+ file_counted = total;
+ }
+ if (!sink(buffer.data(), read)) {
+ return;
+ }
+ }
+ if (total != size) {
+ throw PackError(PackResult::IoError, "a source file changed while packing");
+ }
+ }
+
+ Digest HashSource(Common::FS::IOFile& in, u64 size) {
+ hash.Reset();
+ ReadSource(in, size, [this](const u8* data, size_t n) {
+ hash.Update(data, n);
+ return true;
+ });
+ return hash.Final();
+ }
+
+ ZstdContext& Compressor() {
+ if (!compressor) {
+ compressor = std::make_unique<ZstdContext>(options.zstd_level);
+ }
+ return *compressor;
+ }
+
+ // Streams one zstd frame into the archive. Gives up (returns false) as soon as the output is
+ // no smaller than the input.
+ bool WriteCompressed(Common::FS::IOFile& in, u64 size, u64& stored) {
+ auto& zstd = Compressor();
+ zstd.Begin(size);
+ out_buffer.resize(ZSTD_CStreamOutSize());
+ stored = 0;
+ bool fits = true;
+ const auto drain = [&](ZSTD_inBuffer& input, ZSTD_EndDirective mode) {
+ while (true) {
+ ZSTD_outBuffer output{out_buffer.data(), out_buffer.size(), 0};
+ const size_t remaining = ZSTD_compressStream2(zstd.get(), &output, &input, mode);
+ if (ZSTD_isError(remaining)) {
+ throw PackError(PackResult::IoError,
+ std::string{"zstd: "} + ZSTD_getErrorName(remaining));
+ }
+ stored += output.pos;
+ if (stored >= size) {
+ return false;
+ }
+ Write(out_buffer.data(), output.pos);
+ const bool finished = mode == ZSTD_e_end ? remaining == 0
+ : input.pos == input.size;
+ if (finished) {
+ return true;
+ }
+ }
+ };
+ hash.Reset();
+ ReadSource(in, size, [&](const u8* data, size_t n) {
+ hash.Update(data, n);
+ ZSTD_inBuffer input{data, n, 0};
+ fits = drain(input, ZSTD_e_continue);
+ return fits;
+ });
+ if (!fits) {
+ return false;
+ }
+ ZSTD_inBuffer empty{nullptr, 0, 0};
+ return drain(empty, ZSTD_e_end);
+ }
+
+ void PackFile(size_t index) {
+ const auto source = SourceOf(index);
+ Common::FS::IOFile in{source, Common::FS::FileAccessMode::Read};
+ if (!in.IsOpen()) {
+ throw PackError(PackResult::IoError, "cannot read " + ToUtf8(source));
+ }
+ Extent extent;
+ extent.size = in.GetSize();
+ file_counted = 0;
+
+ std::optional<Digest> known_hash;
+ if (extent.size == 0) {
+ known_hash = HashSource(in, 0);
+ } else if (sizes_seen.contains(extent.size)) {
+ // Only a file whose size matches an earlier extent can be a duplicate; hash it before
+ // writing anything so the shared extent costs no output.
+ known_hash = HashSource(in, extent.size);
+ if (const auto it = by_hash.find(*known_hash);
+ it != by_hash.end() && it->second.size == extent.size) {
+ extent = it->second;
+ Complete(index, extent);
+ return;
+ }
+ }
+
+ if (extent.size == 0) {
+ extent.sha256 = *known_hash;
+ Complete(index, extent);
+ return;
+ }
+
+ const u64 start = AlignUp(cursor, ExtentAlignment);
+ Seek(cursor);
+ WriteZeros(start - cursor);
+ extent.offset = start;
+
+ bool compressed = false;
+ if (extent.size <= options.raw_threshold && options.zstd_level > 0) {
+ u64 stored = 0;
+ compressed = WriteCompressed(in, extent.size, stored);
+ if (compressed) {
+ extent.codec = CodecZstd;
+ extent.stored_size = stored;
+ extent.sha256 = hash.Final();
+ }
+ }
+ if (!compressed) {
+ Seek(start);
+ hash.Reset();
+ ReadSource(in, extent.size, [this](const u8* data, size_t n) {
+ hash.Update(data, n);
+ Write(data, n);
+ return true;
+ });
+ extent.codec = CodecRaw;
+ extent.stored_size = extent.size;
+ extent.sha256 = hash.Final();
+ }
+ cursor = start + extent.stored_size;
+ Remember(extent);
+ Complete(index, extent);
+ }
+
+ void Seek(u64 offset) {
+ if (!out.Seek(static_cast<s64>(offset))) {
+ throw PackError(PackResult::IoError, "cannot seek " + ToUtf8(part));
+ }
+ }
+
+ void Complete(size_t index, const Extent& extent) {
+ extents[index] = extent;
+ done[index] = true;
+ if (options.delete_sources) {
+ chunk.push_back(index);
+ chunk_bytes += extent.size;
+ }
+ ++progress.files_done;
+ Report();
+ }
+
+ void AppendJournal(const std::string& lines) {
+ Common::FS::IOFile log{journal, Common::FS::FileAccessMode::Append};
+ if (!log.IsOpen() || log.WriteString(lines) != lines.size() || !log.Commit()) {
+ throw PackError(PackResult::IoError, "cannot write " + ToUtf8(journal));
+ }
+ }
+
+ // Archive data first, then the journal, then the deletes, so an interruption loses nothing.
+ void Checkpoint() {
+ if (chunk.empty()) {
+ return;
+ }
+ if (!out.Commit()) {
+ throw PackError(PackResult::IoError, "cannot sync " + ToUtf8(part));
+ }
+ std::string lines;
+ for (const auto index : chunk) {
+ const auto& extent = extents[index];
+ lines += Json{{"file", index},
+ {"size", extent.size},
+ {"sha256", ToHex(extent.sha256)},
+ {"offset", extent.offset},
+ {"stored_size", extent.stored_size},
+ {"codec", extent.codec}}
+ .dump();
+ lines += '\n';
+ }
+ AppendJournal(lines);
+ for (const auto index : chunk) {
+ RemoveFile(SourceOf(index));
+ }
+ chunk.clear();
+ chunk_bytes = 0;
+ }
+
+ void Finish() {
+ // Meta blocks follow the last extent, as pack.py writes them.
+ Seek(cursor);
+ std::vector<std::pair<u64, u32>> meta;
+ for (const auto& package : plan.packages) {
+ if (package.meta.size() > std::numeric_limits<u32>::max()) {
+ throw PackError(PackResult::IoError, "package metadata is too large");
+ }
+ meta.emplace_back(package.meta.empty() ? 0 : cursor,
+ static_cast<u32>(package.meta.size()));
+ Write(package.meta.data(), package.meta.size());
+ cursor += package.meta.size();
+ }
+ const u64 file_size = cursor;
+
+ const auto toc = BuildToc(meta);
+ std::array<u8, 32> toc_hash{};
+ unsigned int hash_length{};
+ EVP_Digest(toc.data(), toc.size(), toc_hash.data(), &hash_length, EVP_sha256(), nullptr);
+
+ // UUID: SHA-256 over title_id (u64), version (u32) and kind (u8) of each package.
+ Sha256 uuid_hash;
+ for (const auto& package : plan.packages) {
+ std::vector<u8> identity(13);
+ PutLE(identity, 0, package.title_id, 8);
+ PutLE(identity, 8, package.version, 4);
+ identity[12] = package.kind;
+ uuid_hash.Update(identity.data(), identity.size());
+ }
+ const auto uuid = uuid_hash.Final();
+
+ std::vector<u8> super(SuperblockSize);
+ PutBytes(super, 0x00, Magic);
+ PutLE(super, 0x04, SuperblockSize, 2);
+ PutLE(super, 0x06, 1, 2);
+ PutLE(super, 0x08, SuperblockSize, 8);
+ PutLE(super, 0x10, toc.size(), 8);
+ PutBytes(super, 0x18, toc_hash);
+ PutLE(super, 0x38, layout.blob_offset, 8);
+ PutLE(super, 0x40, file_size, 8);
+ PutLE(super, 0x48, plan.packages.size(), 4);
+ PutBytes(super, 0x50, std::span{uuid}.first(16));
+
+ Seek(0);
+ Write(super.data(), super.size());
+ Write(toc.data(), toc.size());
+ if (!out.SetSize(file_size) || !out.Commit()) {
+ throw PackError(PackResult::IoError, "cannot finish " + ToUtf8(part));
+ }
+ }
+
+ // Every field is written little-endian at its documented offset. The package record is
+ // 0x40 bytes with 20 reserved bytes at 0x2C, as NupContainer reads it.
+ std::vector<u8> BuildToc(const std::vector<std::pair<u64, u32>>& meta) const {
+ std::vector<u8> toc(layout.toc_size);
+ const u64 dir_count = layout.dirs.size();
+ PutBytes(toc, 0x00, TocMagic);
+ PutLE(toc, 0x04, plan.packages.size(), 4);
+ PutLE(toc, 0x08, dir_count, 8);
+ PutLE(toc, 0x10, layout.files.size(), 8);
+
+ size_t pos = TocHeaderSize;
+ for (size_t i = 0; i < plan.packages.size(); ++i) {
+ const auto& package = plan.packages[i];
+ const auto& record = layout.packages[i];
+ PutLE(toc, pos + 0x00, package.title_id, 8);
+ PutLE(toc, pos + 0x08, record.root_dir, 4);
+ PutLE(toc, pos + 0x0C, record.exefs_dir, 4);
+ PutLE(toc, pos + 0x10, record.romfs_dir, 4);
+ PutLE(toc, pos + 0x14, package.version, 4);
+ toc[pos + 0x18] = package.kind;
+ toc[pos + 0x19] = package.flags;
+ PutLE(toc, pos + 0x20, meta[i].first, 8);
+ PutLE(toc, pos + 0x28, meta[i].second, 4);
+ pos += PackageRecordSize;
+ }
+ for (const auto& dir : layout.dirs) {
+ PutLE(toc, pos + 0x00, layout.pool_index.at(dir.name), 4);
+ PutLE(toc, pos + 0x04, dir.name.size(), 4);
+ PutLE(toc, pos + 0x08, dir.first_child, 4);
+ PutLE(toc, pos + 0x0C, dir.child_count, 4);
+ pos += DirRecordSize;
+ }
+ for (size_t i = 0; i < layout.files.size(); ++i) {
+ const auto& file = layout.files[i];
+ const auto& extent = extents[i];
+ PutLE(toc, pos + 0x00, layout.pool_index.at(file.name), 4);
+ PutLE(toc, pos + 0x04, file.name.size(), 4);
+ PutLE(toc, pos + 0x08, extent.size, 8);
+ // Empty files have no extent: offset, size and codec are all zero.
+ if (extent.size != 0) {
+ PutLE(toc, pos + 0x10, extent.offset, 8);
+ PutLE(toc, pos + 0x18, extent.stored_size, 8);
+ toc[pos + 0x40] = extent.codec;
+ }
+ PutBytes(toc, pos + 0x20, extent.sha256);
+ pos += FileRecordSize;
+ }
+ for (const u64 child : layout.children) {
+ const u64 id = (child & Layout::FileTag) ? dir_count + static_cast<u32>(child) : child;
+ PutLE(toc, pos, id, 4);
+ pos += sizeof(u32);
+ }
+ PutLE(toc, 0x18, pos, 8);
+ PutLE(toc, 0x20, layout.pool.size(), 8);
+ std::memcpy(toc.data() + pos, layout.pool.data(), layout.pool.size());
+ return toc;
+ }
+
+ void RemoveSourceFolders() {
+ for (const auto& entry : plan.entries) {
+ const auto depth = std::ranges::count(entry.path, '/');
+ const bool top = entry.directory &&
+ (depth == 0 || (depth == 1 && entry.path.starts_with("data/")));
+ if (top) {
+ const auto source = dump_root / FromUtf8(entry.source);
+ RemoveEmptyTree(source);
+ RemoveEmptyParents(source.parent_path(), dump_root);
+ }
+ }
+ }
+
+ void Report() {
+ last_report = progress.bytes_done;
+ if (progress_cb) {
+ progress_cb(progress);
+ }
+ }
+
+ fs::path dump_root;
+ fs::path archive;
+ fs::path part;
+ fs::path journal;
+ PackOptions options;
+ std::function<void(const PackProgress&)> progress_cb;
+ const std::atomic_bool* cancel;
+
+ Plan plan;
+ Layout layout;
+ std::vector<Extent> extents;
+ std::vector<bool> done;
+ std::vector<std::pair<size_t, Extent>> journaled;
+
+ struct DigestHash {
+ size_t operator()(const Digest& digest) const {
+ size_t value;
+ std::memcpy(&value, digest.data(), sizeof(value));
+ return value;
+ }
+ };
+ std::unordered_map<Digest, Extent, DigestHash> by_hash;
+ std::unordered_set<u64> sizes_seen;
+
+ Common::FS::IOFile out;
+ u64 cursor = 0;
+ u64 resume_end = 0;
+ std::vector<size_t> chunk;
+ u64 chunk_bytes = 0;
+ std::vector<u8> buffer;
+ std::vector<u8> out_buffer;
+ Sha256 hash;
+ std::unique_ptr<ZstdContext> compressor;
+ PackProgress progress{};
+ u64 last_report = 0;
+ u64 file_counted = 0;
+};
+
+} // namespace
+
+bool IsLooseDump(const fs::path& dir) {
+ return HasBootableExeFS(dir);
+}
+
+std::optional<fs::path> DumpRootFromMain(const fs::path& main) {
+ // The Qt game list stores the exefs folder rather than exefs/main.
+ std::error_code ec;
+ const auto exefs = fs::is_directory(main, ec) ? main : main.parent_path();
+ if (Lower(ToUtf8(exefs.filename())) != "exefs") {
+ return std::nullopt;
+ }
+ auto root = exefs.parent_path();
+ const auto name = Lower(ToUtf8(root.filename()));
+ if (name == "base") {
+ root = root.parent_path();
+ } else if (ParseDecimal(name) && Lower(ToUtf8(root.parent_path().filename())) == "updates") {
+ root = root.parent_path().parent_path();
+ }
+ if (!IsLooseDump(root)) {
+ return std::nullopt;
+ }
+ return root;
+}
+
+Metadata ProbeMetadata(const fs::path& dump_root) {
+ RealVfsFilesystem vfs;
+ const auto base = FindDirectory(dump_root, "base");
+ const auto updates = FindDirectory(dump_root, "updates");
+
+ // Newest update first, then the base (or a bare dump's root).
+ std::vector<std::pair<u64, fs::path>> update_dirs;
+ std::error_code ec;
+ if (!updates.empty()) {
+ for (const auto& entry : fs::directory_iterator(updates, ec)) {
+ const auto version = ParseDecimal(ToUtf8(entry.path().filename()));
+ if (version && entry.is_directory(ec)) {
+ update_dirs.emplace_back(*version, entry.path());
+ }
+ }
+ }
+ std::ranges::sort(update_dirs, std::greater<>{});
+ const fs::path base_dir = !base.empty() ? base : (updates.empty() ? dump_root : fs::path{});
+
+ Metadata result;
+ const auto probe = [&](const fs::path& dir, const std::string& label, bool names) {
+ const auto loader = OpenLoader(vfs, dir);
+ if (!loader) {
+ return;
+ }
+ if (result.display_version.empty()) {
+ std::string version;
+ if (loader->ReadDisplayVersion(version) == Loader::ResultStatus::Success &&
+ !version.empty()) {
+ result.display_version = version;
+ result.version_source = label;
+ }
+ }
+ if (names) {
+ NACP control;
+ if (result.title_name.empty() &&
+ loader->ReadControlData(control) == Loader::ResultStatus::Success) {
+ result.title_name = control.GetApplicationName();
+ }
+ std::vector<u8> icon;
+ if (result.icon.empty() &&
+ loader->ReadIcon(icon) == Loader::ResultStatus::Success) {
+ result.icon = std::move(icon);
+ }
+ }
+ };
+ for (const auto& [version, dir] : update_dirs) {
+ probe(dir, "update " + std::to_string(version), false);
+ }
+ if (!base_dir.empty()) {
+ probe(base_dir, "base", true);
+ }
+ // Names and icons fall back to the updates when the base has none.
+ for (const auto& [version, dir] : update_dirs) {
+ if (!result.title_name.empty() && !result.icon.empty()) {
+ break;
+ }
+ probe(dir, "update " + std::to_string(version), true);
+ }
+ return result;
+}
+
+fs::path DefaultArchivePath(const fs::path& dump_root) {
+ auto root = dump_root;
+ if (!root.has_filename()) {
+ root = root.parent_path();
+ }
+ return root.parent_path() / (root.filename().u8string() + u8".nup");
+}
+
+bool HasResumableJournal(const fs::path& archive) {
+ std::error_code ec;
+ return fs::exists(WithSuffix(archive, ".part"), ec) &&
+ fs::exists(WithSuffix(archive, ".journal"), ec);
+}
+
+PackResult PackDump(const fs::path& dump_root, const fs::path& archive, const PackOptions& options,
+ const std::function<void(const PackProgress&)>& progress,
+ const std::atomic_bool* cancel, std::string* error_out) {
+ Packer packer{dump_root, archive, options, progress, cancel};
+ try {
+ return packer.Run();
+ } catch (const PackError& e) {
+ packer.Abort();
+ if (error_out) {
+ *error_out = e.what();
+ }
+ return e.result;
+ } catch (const std::exception& e) {
+ packer.Abort();
+ if (error_out) {
+ *error_out = e.what();
+ }
+ return PackResult::IoError;
+ }
+}
+
+} // namespace FileSys::Nup
diff --git a/src/core/file_sys/nup_writer.h b/src/core/file_sys/nup_writer.h
new file mode 100644
index 0000000000..dd7b7e0883
--- /dev/null
+++ b/src/core/file_sys/nup_writer.h
@@ -0,0 +1,76 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <atomic>
+#include <filesystem>
+#include <functional>
+#include <optional>
+#include <string>
+#include <vector>
+
+#include "common/common_types.h"
+
+// Packs a loose dump into a NUP container. Every package is kept: base/, each
+// updates/<version>/ and each dlc/<titleid>/<n>/.
+namespace FileSys::Nup {
+
+struct PackProgress {
+ u64 bytes_done{};
+ u64 bytes_total{};
+ u64 files_done{};
+ u64 files_total{};
+};
+
+enum class PackResult { Success, Cancelled, InvalidSource, IoError };
+
+// NupContainer keeps a zstd file in memory only while it fits half of its 512 MiB decompression
+// cache; a larger one is decompressed to a temporary file on first read, which costs time and
+// disk space. Files above this size are therefore stored raw.
+constexpr u64 DefaultRawThreshold = 256ULL * 1024 * 1024;
+
+// Application metadata written to the base package (or the first package when there is no base).
+// In PackOptions an empty field keeps what the dump supplies; ProbeMetadata fills all of them.
+struct Metadata {
+ std::string title_name;
+ // JPEG or PNG bytes; 256x256 JPEG as distributed on-console.
+ std::vector<u8> icon;
+ std::string display_version;
+ // Where ProbeMetadata found display_version, e.g. "update 458752" or "base".
+ std::string version_source;
+};
+
+struct PackOptions {
+ // Low-storage mode: delete each source file once its extent is synced and journaled.
+ bool delete_sources = false;
+ u64 chunk_bytes = 256ULL * 1024 * 1024;
+ int zstd_level = 3;
+ // Files larger than this are always stored raw.
+ u64 raw_threshold = DefaultRawThreshold;
+ // Overrides for the name, icon and display version found in the dump.
+ Metadata metadata;
+};
+
+// A loose dump holds exefs/main and main.npdm, directly or under base/ or updates/<version>/,
+// and a romfs folder. Folder names are matched case-insensitively.
+bool IsLooseDump(const std::filesystem::path& dir);
+// The dump folder a game list entry's exefs/main (or exefs folder) belongs to, if it is a loose
+// dump.
+std::optional<std::filesystem::path> DumpRootFromMain(const std::filesystem::path& main);
+// Reads the name, icon and display version a dump would be packed with. The version comes from
+// the newest update that reports one, else the base.
+Metadata ProbeMetadata(const std::filesystem::path& dump_root);
+std::filesystem::path DefaultArchivePath(const std::filesystem::path& dump_root);
+
+// Writes <archive>.part and renames it when done; an existing archive is never overwritten. A
+// cancelled or failed low-storage run keeps its .part and .journal, and calling this again with
+// the same arguments resumes it.
+PackResult PackDump(const std::filesystem::path& dump_root, const std::filesystem::path& archive,
+ const PackOptions& options = {},
+ const std::function<void(const PackProgress&)>& progress = {},
+ const std::atomic_bool* cancel = nullptr, std::string* error_out = nullptr);
+
+bool HasResumableJournal(const std::filesystem::path& archive);
+
+} // namespace FileSys::Nup
diff --git a/src/core/file_sys/system_archive/system_archive.cpp b/src/core/file_sys/system_archive/system_archive.cpp
index 1743d01b12..c3931b6f62 100644
--- a/src/core/file_sys/system_archive/system_archive.cpp
+++ b/src/core/file_sys/system_archive/system_archive.cpp
@@ -4,6 +4,8 @@
// SPDX-FileCopyrightText: Copyright 2018 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <cstdlib>
+
#include "common/logging.h"
#include "core/file_sys/romfs.h"
#include "core/file_sys/system_archive/mii_model.h"
@@ -12,6 +14,7 @@
#include "core/file_sys/system_archive/system_archive.h"
#include "core/file_sys/system_archive/system_version.h"
#include "core/file_sys/system_archive/time_zone_binary.h"
+#include "core/file_sys/vfs/vfs_vector.h"
namespace FileSys::SystemArchive {
@@ -76,6 +79,22 @@ constexpr inline SystemArchiveDescriptor GetSystemArchive(u64 title_id) {
VirtualFile SynthesizeSystemArchive(const u64 title_id) {
auto const desc = GetSystemArchive(title_id);
+ if (desc.name == nullptr) {
+ // Not a system archive Eden knows (fmt throws on a null string, ending the process).
+ // Switch 2 system data (ids 04...) is not installed with Switch 1 firmware, and titles
+ // abort when it cannot be mounted (Nintendo Switch Sports Resort, 0400000000000849):
+ // serve an empty archive. EDEN_NX2_EMPTY_SYSTEM_DATA=0 fails the request instead.
+ static const bool empty_nx2 = [] {
+ const char* setting = std::getenv("EDEN_NX2_EMPTY_SYSTEM_DATA");
+ return setting == nullptr || setting[0] != '0';
+ }();
+ if (empty_nx2 && (title_id >> 56) == 0x04) {
+ LOG_WARNING(Service_FS, "Empty archive for Switch 2 system data {:#016x}", title_id);
+ return CreateRomFS(std::make_shared<VectorVfsDirectory>());
+ }
+ LOG_WARNING(Service_FS, "No system archive {:#016x} to synthesize", title_id);
+ return nullptr;
+ }
LOG_INFO(Service_FS, "Synthesizing system archive '{}' ({:#016x}).", desc.name, title_id);
if (desc.supplier != nullptr) {
if (auto const dir = desc.supplier(); dir != nullptr) {
diff --git a/src/core/file_sys/vfs/vfs_real.cpp b/src/core/file_sys/vfs/vfs_real.cpp
index 5b5b54017c..7cc5315a70 100644
--- a/src/core/file_sys/vfs/vfs_real.cpp
+++ b/src/core/file_sys/vfs/vfs_real.cpp
@@ -114,7 +114,6 @@ VfsEntryType RealVfsFilesystem::GetEntryType(std::string_view path_) const {
}
VirtualFile RealVfsFilesystem::OpenFileFromEntry(std::string_view path_, std::optional<u64> size,
- std::optional<std::string> parent_path,
OpenMode perms) {
const auto path = FS::SanitizePath(path_, FS::DirectorySeparator::PlatformDefault);
std::scoped_lock lk{list_lock};
@@ -132,14 +131,14 @@ VirtualFile RealVfsFilesystem::OpenFileFromEntry(std::string_view path_, std::op
auto reference = std::make_unique<FileReference>();
this->InsertReferenceIntoListLocked(*reference);
- auto file = std::make_shared<RealVfsFile>(*this, std::move(reference), path, perms, size, std::move(parent_path));
+ auto file = std::make_shared<RealVfsFile>(*this, std::move(reference), path, perms, size);
cache[path] = file;
return file;
}
VirtualFile RealVfsFilesystem::OpenFile(std::string_view path_, OpenMode perms) {
- return OpenFileFromEntry(path_, {}, {}, perms);
+ return OpenFileFromEntry(path_, {}, perms);
}
VirtualFile RealVfsFilesystem::CreateFile(std::string_view path_, OpenMode perms) {
@@ -257,7 +256,8 @@ std::unique_lock<std::mutex> RealVfsFilesystem::RefreshReference(const std::stri
return lk;
}
-void RealVfsFilesystem::DropReference(std::unique_ptr<FileReference>&& reference) {
+void RealVfsFilesystem::DropReference(std::unique_ptr<FileReference>&& reference,
+ const std::string& path) {
if (in_dtor)
return;
@@ -266,6 +266,11 @@ void RealVfsFilesystem::DropReference(std::unique_ptr<FileReference>&& reference
// Remove from list.
this->RemoveReferenceFromListLocked(*reference);
+ // Forget the path, unless it was opened again since this file's last reference went away.
+ if (const auto it = cache.find(path); it != cache.end() && it->second.expired()) {
+ cache.erase(it);
+ }
+
// Close the file.
if (reference->file) {
reference->file.reset();
@@ -309,14 +314,11 @@ void RealVfsFilesystem::RemoveReferenceFromListLocked(FileReference& reference)
}
RealVfsFile::RealVfsFile(RealVfsFilesystem& base_, std::unique_ptr<FileReference> reference_,
- const std::string& path_, OpenMode perms_, std::optional<u64> size_,
- std::optional<std::string> parent_path_)
- : base(base_), reference(std::move(reference_)), path(path_),
- parent_path(parent_path_ ? std::move(*parent_path_) : FS::GetParentPath(path_)),
- path_components(FS::SplitPathComponentsCopy(path_)), size(size_), perms(perms_) {}
+ const std::string& path_, OpenMode perms_, std::optional<u64> size_)
+ : base(base_), reference(std::move(reference_)), path(path_), size(size_), perms(perms_) {}
RealVfsFile::~RealVfsFile() {
- base.DropReference(std::move(reference));
+ base.DropReference(std::move(reference), path);
}
std::string RealVfsFile::GetName() const {
@@ -325,7 +327,14 @@ std::string RealVfsFile::GetName() const {
return FS::Android::GetFilename(path);
}
#endif
- return path_components.empty() ? "" : std::string(path_components.back());
+ // The last path component.
+ const auto end = path.find_last_not_of("\\/");
+ if (end == std::string::npos) {
+ return "";
+ }
+ const auto separator = path.find_last_of("\\/", end);
+ const auto begin = separator == std::string::npos ? 0 : separator + 1;
+ return path.substr(begin, end + 1 - begin);
}
std::size_t RealVfsFile::GetSize() const {
@@ -343,7 +352,7 @@ bool RealVfsFile::Resize(std::size_t new_size) {
}
VirtualDir RealVfsFile::GetContainingDirectory() const {
- return base.OpenDirectory(parent_path, perms);
+ return base.OpenDirectory(FS::GetParentPath(path), perms);
}
bool RealVfsFile::IsWritable() const {
@@ -372,7 +381,7 @@ std::size_t RealVfsFile::Write(const u8* data, std::size_t length, std::size_t o
}
bool RealVfsFile::Rename(std::string_view name) {
- return base.MoveFile(path, parent_path + '/' + std::string(name)) != nullptr;
+ return base.MoveFile(path, FS::GetParentPath(path) + '/' + std::string(name)) != nullptr;
}
// TODO(DarkLordZach): MSVC would not let me combine the following two functions using 'if
@@ -390,7 +399,7 @@ std::vector<VirtualFile> RealVfsDirectory::IterateEntries<RealVfsFile, VfsFile>(
&out](const std::filesystem::directory_entry& entry) {
const auto full_path_string = FS::PathToUTF8String(entry.path());
- out.emplace_back(base.OpenFileFromEntry(full_path_string, entry.file_size(), path, perms));
+ out.emplace_back(base.OpenFileFromEntry(full_path_string, entry.file_size(), perms));
return true;
};
diff --git a/src/core/file_sys/vfs/vfs_real.h b/src/core/file_sys/vfs/vfs_real.h
index a97f7d32ea..2df125b7eb 100644
--- a/src/core/file_sys/vfs/vfs_real.h
+++ b/src/core/file_sys/vfs/vfs_real.h
@@ -62,12 +62,11 @@ private:
friend class RealVfsFile;
std::unique_lock<std::mutex> RefreshReference(const std::string& path, OpenMode perms,
FileReference& reference);
- void DropReference(std::unique_ptr<FileReference>&& reference);
+ void DropReference(std::unique_ptr<FileReference>&& reference, const std::string& path);
private:
friend class RealVfsDirectory;
VirtualFile OpenFileFromEntry(std::string_view path, std::optional<u64> size,
- std::optional<std::string> parent_path,
OpenMode perms = OpenMode::Read);
private:
@@ -84,7 +83,7 @@ class RealVfsFile : public VfsFile {
public:
RealVfsFile(RealVfsFilesystem& base, std::unique_ptr<FileReference> reference,
const std::string& path, OpenMode perms = OpenMode::Read,
- std::optional<u64> size = {}, std::optional<std::string> parent_path = {});
+ std::optional<u64> size = {});
~RealVfsFile() override;
std::string GetName() const override;
@@ -101,9 +100,8 @@ private:
RealVfsFilesystem& base;
std::unique_ptr<FileReference> reference;
+ // Only the path: a loose RomFS keeps one of these per file (title 1: 206k).
std::string path;
- std::string parent_path;
- std::vector<std::string> path_components;
std::optional<u64> size;
OpenMode perms;
};
diff --git a/src/core/hle/kernel/board/nintendo/nx/k_system_control.h b/src/core/hle/kernel/board/nintendo/nx/k_system_control.h
index cc5572bde8..9aa527adb1 100644
--- a/src/core/hle/kernel/board/nintendo/nx/k_system_control.h
+++ b/src/core/hle/kernel/board/nintendo/nx/k_system_control.h
@@ -23,7 +23,10 @@ public:
// Extra DRAM on the 12 GB layout for memory the Switch 2 graphics server keeps itself (nx2:gpu
// 214 GPU-only regions). It sits in the non-secure system pool, outside the application pool.
- static constexpr size_t Nx2GraphicsServerMemorySize = 8ULL * 1024 * 1024 * 1024; // 8_GiB
+ // GPU-only regions get memory only where guest memory is written and sparse regions take
+ // their tiles from the game's own memory (216), so much less of it is used (title 1: ~1.1 GiB,
+ // all of it at boot); all of it counts against the host's commit limit from boot.
+ static constexpr size_t Nx2GraphicsServerMemorySize = 2ULL * 1024 * 1024 * 1024; // 2_GiB
public:
class Init {
diff --git a/src/core/hle/kernel/k_memory_manager.cpp b/src/core/hle/kernel/k_memory_manager.cpp
index 743e290541..bed3dd819d 100644
--- a/src/core/hle/kernel/k_memory_manager.cpp
+++ b/src/core/hle/kernel/k_memory_manager.cpp
@@ -48,6 +48,8 @@ KMemoryManager::KMemoryManager(Core::System& system)
} {}
void KMemoryManager::Initialize(KVirtualAddress management_region, size_t management_region_size) {
+ // Memory a previous kernel handed out may still hold its data.
+ m_system.DeviceMemory().buffer.ForgetContents();
// Clear the management region to zero.
const KVirtualAddress management_region_end = management_region + management_region_size;
@@ -242,10 +244,21 @@ KPhysicalAddress KMemoryManager::AllocateAndOpenContinuous(size_t num_pages, siz
// Open the first reference to the pages.
chosen_manager->OpenFirst(allocated_block, num_pages);
+ this->MarkUsed(allocated_block, num_pages);
return allocated_block;
}
+void KMemoryManager::MarkUsed(KPhysicalAddress address, size_t num_pages) {
+ m_system.DeviceMemory().buffer.MarkUsed(GetInteger(address) - Core::DramMemoryMap::Base,
+ num_pages * PageSize);
+}
+
+void KMemoryManager::MarkDirty(KPhysicalAddress address, size_t num_pages) {
+ m_system.DeviceMemory().buffer.MarkDirty(GetInteger(address) - Core::DramMemoryMap::Base,
+ num_pages * PageSize);
+}
+
Result KMemoryManager::AllocatePageGroupImpl(KPageGroup* out, size_t num_pages, Pool pool,
Direction dir, bool unoptimized, bool random) {
// Choose a heap based on our page size request.
@@ -297,6 +310,10 @@ Result KMemoryManager::AllocatePageGroupImpl(KPageGroup* out, size_t num_pages,
// Only succeed if we allocated as many pages as we wanted.
R_UNLESS(num_pages == 0, ResultOutOfMemory);
+ for (const auto& block : *out) {
+ this->MarkUsed(block.GetAddress(), block.GetNumPages());
+ }
+
// We succeeded!
R_SUCCEED();
}
@@ -525,9 +542,10 @@ bool KMemoryManager::Impl::ProcessOptimizedAllocation(KernelCore& kernel, KPhysi
// If not, it's new.
any_new = true;
- // Fill the page.
- auto* ptr = device_memory.GetPointer<u8>(m_heap.GetAddress());
- std::memset(ptr + offset * PageSize, fill_pattern, PageSize);
+ // Fill the page (a zero fill skips pages that are still zero).
+ device_memory.buffer.ClearBackingRegion(
+ GetInteger(m_heap.GetAddress()) + offset * PageSize - Core::DramMemoryMap::Base,
+ PageSize, fill_pattern);
}
offset++;
diff --git a/src/core/hle/kernel/k_memory_manager.h b/src/core/hle/kernel/k_memory_manager.h
index 2f4ae7f04f..66b97fef8a 100644
--- a/src/core/hle/kernel/k_memory_manager.h
+++ b/src/core/hle/kernel/k_memory_manager.h
@@ -100,6 +100,9 @@ public:
}
void Close(KPhysicalAddress address, size_t num_pages) {
+ // Pages that may be freed here may hold data: the next allocation clears them.
+ this->MarkDirty(address, num_pages);
+
// Repeatedly close references until we've done so for all pages.
while (num_pages) {
auto& manager = this->GetManager(address);
@@ -333,6 +336,9 @@ private:
};
private:
+ void MarkUsed(KPhysicalAddress address, size_t num_pages);
+ void MarkDirty(KPhysicalAddress address, size_t num_pages);
+
Impl& GetManager(KPhysicalAddress address) {
return m_managers[m_memory_layout.GetPhysicalLinearRegion(address).GetAttributes()];
}
diff --git a/src/core/hle/kernel/k_priority_queue.h b/src/core/hle/kernel/k_priority_queue.h
index 89a0c5bbea..5d93455d33 100644
--- a/src/core/hle/kernel/k_priority_queue.h
+++ b/src/core/hle/kernel/k_priority_queue.h
@@ -11,6 +11,7 @@
#include <bitset>
#include <concepts>
#include <cstddef>
+#include <type_traits>
#include "common/alignment.h"
#include "common/assert.h"
@@ -196,12 +197,7 @@ public:
constexpr Member* GetFront(s32 core) const {
ASSERT(IsValidCore(core));
- const s32 priority = s32([](auto const& e) {
- for (size_t i = 0; i < e.size(); ++i)
- if (e[i])
- return i;
- return e.size();
- }(m_available_priorities[core]));
+ const s32 priority = s32(GetNextSet(m_available_priorities[core], size_t(-1)));
if (priority <= LowestPriority) {
return m_queues[priority].GetFront(core);
} else {
@@ -222,7 +218,17 @@ public:
template<size_t N>
constexpr size_t GetNextSet(std::bitset<N> const& bit, size_t n) const {
- for (size_t i = n + 1; i < bit.size(); i++)
+ const size_t first = n + 1;
+ if (first >= bit.size()) {
+ return bit.size();
+ }
+ if constexpr (N <= 64) {
+ if (!std::is_constant_evaluated()) {
+ const u64 remaining = bit.to_ullong() >> first;
+ return remaining != 0 ? first + size_t(std::countr_zero(remaining)) : bit.size();
+ }
+ }
+ for (size_t i = first; i < bit.size(); i++)
if (bit[i])
return i;
return bit.size();
diff --git a/src/core/hle/kernel/k_shared_memory.cpp b/src/core/hle/kernel/k_shared_memory.cpp
index 61f2872560..cb8877b8b2 100644
--- a/src/core/hle/kernel/k_shared_memory.cpp
+++ b/src/core/hle/kernel/k_shared_memory.cpp
@@ -59,9 +59,10 @@ Result KSharedMemory::Initialize(KernelCore& kernel, Core::DeviceMemory& device_
// Mark initialized.
m_is_initialized = true;
- // Clear all pages in the memory.
+ // Clear all pages in the memory (pages still zero are skipped).
for (const auto& block : *m_page_group) {
- std::memset(m_device_memory->GetPointer<void>(block.GetAddress()), 0, block.GetSize());
+ m_device_memory->buffer.ClearBackingRegion(
+ GetInteger(block.GetAddress()) - Core::DramMemoryMap::Base, block.GetSize(), 0);
}
R_SUCCEED();
diff --git a/src/core/hle/kernel/kernel.cpp b/src/core/hle/kernel/kernel.cpp
index f006d129a7..9e6ccc2d89 100644
--- a/src/core/hle/kernel/kernel.cpp
+++ b/src/core/hle/kernel/kernel.cpp
@@ -79,7 +79,7 @@ struct KernelCore::Impl {
// And we have the guarantee that the data won't move out of the way so we can safely
// take a reference to it. This isn't always universally true but this is "global" data
// so it will be statically given a TLS slot anyways.
- static inline thread_local ThreadLocalData tls_data = {};
+ static thread_local ThreadLocalData tls_data;
explicit Impl(Core::System& system_, KernelCore& kernel_) : system{system_} {
tls_data.lock = true;
@@ -861,6 +861,8 @@ struct KernelCore::Impl {
Core::System& system;
};
+thread_local ThreadLocalData KernelCore::Impl::tls_data{};
+
KernelCore::KernelCore(Core::System& system) : impl{std::make_unique<Impl>(system, *this)} {}
KernelCore::~KernelCore() = default;
diff --git a/src/core/hle/kernel/svc.cpp b/src/core/hle/kernel/svc.cpp
index 53d964ecbc..925eafc472 100644
--- a/src/core/hle/kernel/svc.cpp
+++ b/src/core/hle/kernel/svc.cpp
@@ -14,6 +14,8 @@
#include "core/core.h"
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/svc.h"
+#include "core/hle/kernel/k_readable_event.h"
+#include "core/hle/service/kernel_helpers.h"
namespace Kernel::Svc {
@@ -2522,6 +2524,21 @@ void Call(Core::System& system, u32 imm) {
LOG_INFO(Kernel_SVC, "svc {:#04x} in t{} [0]={:#x} [1]={:#x} [2]={:#x} [3]={:#x}", imm,
GetCurrentThread(kernel).GetThreadId(),
GetArg64(args, 0), GetArg64(args, 1), GetArg64(args, 2), GetArg64(args, 3));
+ if (imm == 0x18) {
+ // WaitSynchronization: the handles, with the names services gave their events.
+ std::string list;
+ const s32 count = static_cast<s32>(GetArg64(args, 2));
+ for (s32 i = 0; i < std::min(count, 8); ++i) {
+ const u32 handle = process.GetMemory().Read32(GetArg64(args, 1) + 4 * i);
+ KScopedAutoObject event =
+ process.GetHandleTable().GetObject<KReadableEvent>(kernel, handle);
+ list += fmt::format(" {:#x}{}", handle,
+ event.IsNotNull()
+ ? "=" + Service::KernelHelpers::DebugEventName(event.GetPointerUnsafe())
+ : std::string{});
+ }
+ LOG_INFO(Kernel_SVC, "svc 0x18 t{} waits on{}", GetCurrentThread(kernel).GetThreadId(), list);
+ }
}
if (imm == 0x80) {
// Switch 2 only: svcSetGpuMemoryAttribute(address, size, mask, attr). nn::gpu::sdk and
diff --git a/src/core/hle/service/acc/acc.cpp b/src/core/hle/service/acc/acc.cpp
index d68b6bde0d..772da97ef6 100644
--- a/src/core/hle/service/acc/acc.cpp
+++ b/src/core/hle/service/acc/acc.cpp
@@ -696,9 +696,9 @@ public:
{4, &IManagerForApplication::LoadIdTokenCache, "LoadIdTokenCache"},
{130, &IManagerForApplication::GetNintendoAccountUserResourceCacheForApplication, "GetNintendoAccountUserResourceCacheForApplication"},
{136, &IManagerForApplication::GetNintendoAccountUserResourceCacheForApplication, "GetNintendoAccountUserResourceCache"}, // 19.0.0+
- {150, nullptr, "CreateAuthorizationRequest"},
+ {150, &IManagerForApplication::StubReplyInterface, "CreateAuthorizationRequest"},
{160, &IManagerForApplication::StoreOpenContext, "StoreOpenContext"},
- {170, nullptr, "LoadNetworkServiceLicenseKindAsync"},
+ {170, &IManagerForApplication::StubReplyInterface, "LoadNetworkServiceLicenseKindAsync"},
};
// clang-format on
diff --git a/src/core/hle/service/am/frontend/applet_software_keyboard.cpp b/src/core/hle/service/am/frontend/applet_software_keyboard.cpp
index fe07d72912..061394bff8 100644
--- a/src/core/hle/service/am/frontend/applet_software_keyboard.cpp
+++ b/src/core/hle/service/am/frontend/applet_software_keyboard.cpp
@@ -213,6 +213,23 @@ void SoftwareKeyboard::InitializeForeground() {
std::memcpy(&swkbd_config_new, swkbd_config_data.data() + sizeof(SwkbdConfigCommon),
sizeof(SwkbdConfigNew));
break;
+ case SwkbdAppletVersion::Version1310736:
+ // The common and 8.0.0 parts keep their layout; the 0x68 bytes after them are unknown.
+ // This revision exchanges UTF-8 text: the game's text check replies are UTF-8, and a
+ // UTF-16 result reached it as its first character only.
+ ASSERT(swkbd_config_data.size() >= sizeof(SwkbdConfigCommon) + sizeof(SwkbdConfigNew));
+ std::memcpy(&swkbd_config_new, swkbd_config_data.data() + sizeof(SwkbdConfigCommon),
+ sizeof(SwkbdConfigNew));
+ swkbd_config_common.use_utf8 = true;
+ break;
+ case SwkbdAppletVersion::Version1441810: {
+ // Fire Emblem: Fortune's Weave asks for UTF-8 and keeps the reply up to the first NUL, so the
+ // Switch 1 layout's use_utf8 (a byte of the native initial_string_offset) left one letter.
+ const bool decoded =
+ DecodeNativeSwkbdConfig(swkbd_config_data, swkbd_config_common, swkbd_config_new);
+ ASSERT(decoded);
+ break;
+ }
default:
UNIMPLEMENTED_MSG("Unknown SwkbdConfig revision={} with size={}", swkbd_applet_version,
swkbd_config_data.size());
diff --git a/src/core/hle/service/am/frontend/applet_software_keyboard_types.h b/src/core/hle/service/am/frontend/applet_software_keyboard_types.h
index 6bb8f437d3..8ff8391aa1 100644
--- a/src/core/hle/service/am/frontend/applet_software_keyboard_types.h
+++ b/src/core/hle/service/am/frontend/applet_software_keyboard_types.h
@@ -7,6 +7,9 @@
#pragma once
#include <array>
+#include <cstddef>
+#include <cstring>
+#include <span>
#include "common/bit_field.h"
#include "common/common_funcs.h"
@@ -30,6 +33,8 @@ enum class SwkbdAppletVersion : u32_le {
Version327689 = 0x50009, // 5.0.0 - 5.1.0
Version393227 = 0x6000B, // 6.0.0 - 7.0.1
Version524301 = 0x8000D, // 8.0.0+
+ Version1310736 = 0x140010, // Switch 2 (Pokemon Pokopia): 0x530-byte config, UTF-8 text
+ Version1441810 = 0x160012, // Switch 2 (Fire Emblem: Fortune's Weave): native 0x530-byte config
};
enum class SwkbdType : u32 {
@@ -183,8 +188,64 @@ struct SwkbdConfigNew {
};
static_assert(sizeof(SwkbdConfigNew) == 0x4C8 - sizeof(SwkbdConfigCommon),
"SwkbdConfigNew has incorrect size.");
+
+// SwkbdAppletVersion 0x160012: the text prefix of the common part keeps its layout, the flags,
+// lengths, string offsets and dictionary fields move (checked against
+// nn::swkbd::keyboardConfig2kbdConfig in Fire Emblem: Fortune's Weave's sdk).
+struct SwkbdConfigNative {
+ std::array<u8, 0x3AA> common_prefix{};
+ bool use_utf8{};
+ bool use_blur_background{};
+ u32 max_text_length{};
+ u32 min_text_length{};
+ SwkbdPasswordMode password_mode{};
+ SwkbdTextDrawType text_draw_type{};
+ u32 initial_string_offset{};
+ u32 initial_string_length{};
+ u32 user_dictionary_offset{};
+ u32 user_dictionary_entries{};
+ std::array<u64, 24> customized_dictionary_set_entries{};
+ u8 total_customized_dictionary_set_entries{};
+ bool use_text_check{};
+ bool disable_cancel_button{};
+ u8 trigger{};
+ std::array<u32, 8> text_grouping{};
+ INSERT_PADDING_BYTES(0x80);
+};
+static_assert(sizeof(SwkbdConfigNative) == 0x530);
+static_assert(offsetof(SwkbdConfigNative, use_text_check) == 0x48D);
+static_assert(offsetof(SwkbdConfigNative, text_grouping) == 0x490);
#pragma pack(pop)
+inline bool DecodeNativeSwkbdConfig(std::span<const u8> data, SwkbdConfigCommon& common,
+ SwkbdConfigNew& config) {
+ if (data.size() != sizeof(SwkbdConfigNative)) {
+ return false;
+ }
+ SwkbdConfigNative native{};
+ std::memcpy(&native, data.data(), sizeof(native));
+ common = {};
+ config = {};
+ std::memcpy(static_cast<void*>(&common), native.common_prefix.data(), native.common_prefix.size());
+ common.enable_return_button = native.common_prefix[0x1B] != 0;
+ common.use_utf8 = native.use_utf8;
+ common.use_blur_background = native.use_blur_background;
+ common.max_text_length = native.max_text_length;
+ common.min_text_length = native.min_text_length;
+ common.password_mode = native.password_mode;
+ common.text_draw_type = native.text_draw_type;
+ common.initial_string_offset = native.initial_string_offset;
+ common.initial_string_length = native.initial_string_length;
+ common.user_dictionary_offset = native.user_dictionary_offset;
+ common.user_dictionary_entries = native.user_dictionary_entries;
+ common.use_text_check = native.use_text_check;
+ config.text_grouping = native.text_grouping;
+ config.customized_dictionary_set_entries = native.customized_dictionary_set_entries;
+ config.total_customized_dictionary_set_entries = native.total_customized_dictionary_set_entries;
+ config.disable_cancel_button = native.disable_cancel_button;
+ return true;
+}
+
struct SwkbdTextCheck {
SwkbdTextCheckResult text_check_result{};
std::array<char16_t, STRING_BUFFER_SIZE / 2> text_check_message{};
diff --git a/src/core/hle/service/am/frontend/applets.cpp b/src/core/hle/service/am/frontend/applets.cpp
index c59e4b9c6b..c1a0a1dd29 100644
--- a/src/core/hle/service/am/frontend/applets.cpp
+++ b/src/core/hle/service/am/frontend/applets.cpp
@@ -253,7 +253,7 @@ std::shared_ptr<FrontendApplet> FrontendAppletHolder::GetApplet(std::shared_ptr<
case AppletId::None:
return nullptr;
default:
- UNIMPLEMENTED_MSG("No frontend implementation exists for applet_id={:02X} program_id={:016X}! Using stub applet.", static_cast<u8>(id), applet->program_id);
+ UNIMPLEMENTED_MSG("No frontend implementation exists for applet_id={:#x} program_id={:016X}! Using stub applet.", static_cast<u32>(id), applet->program_id);
return std::make_shared<StubApplet>(system, applet, id, mode);
}
}
diff --git a/src/core/hle/service/am/service/application_functions.cpp b/src/core/hle/service/am/service/application_functions.cpp
index 37dfbfa344..c63d29b4e9 100644
--- a/src/core/hle/service/am/service/application_functions.cpp
+++ b/src/core/hle/service/am/service/application_functions.cpp
@@ -58,18 +58,19 @@ IApplicationFunctions::IApplicationFunctions(Core::System& system_, std::shared_
: ServiceFramework{system_, "IApplicationFunctions"}, m_applet{std::move(applet)} {
// clang-format off
static const FunctionInfo functions[] = {
+ {0, &IApplicationFunctions::StubReplyZeroed, "Unknown0"},
{1, D<&IApplicationFunctions::PopLaunchParameter>, "PopLaunchParameter"},
- {10, nullptr, "CreateApplicationAndPushAndRequestToStart"},
- {11, nullptr, "CreateApplicationAndPushAndRequestToStartForQuest"},
+ {10, &IApplicationFunctions::StubReplyZeroed, "CreateApplicationAndPushAndRequestToStart"},
+ {11, &IApplicationFunctions::StubReplyZeroed, "CreateApplicationAndPushAndRequestToStartForQuest"},
{12, D<&IApplicationFunctions::CreateApplicationAndRequestToStart>, "CreateApplicationAndRequestToStart"},
- {13, nullptr, "CreateApplicationAndRequestToStartForQuest"},
- {14, nullptr, "CreateApplicationWithAttributeAndPushAndRequestToStartForQuest"},
- {15, nullptr, "CreateApplicationWithAttributeAndRequestToStartForQuest"},
+ {13, &IApplicationFunctions::StubReplyZeroed, "CreateApplicationAndRequestToStartForQuest"},
+ {14, &IApplicationFunctions::StubReplyZeroed, "CreateApplicationWithAttributeAndPushAndRequestToStartForQuest"},
+ {15, &IApplicationFunctions::StubReplyZeroed, "CreateApplicationWithAttributeAndRequestToStartForQuest"},
{20, D<&IApplicationFunctions::EnsureSaveData>, "EnsureSaveData"},
{21, D<&IApplicationFunctions::GetDesiredLanguage>, "GetDesiredLanguage"},
{22, D<&IApplicationFunctions::SetTerminateResult>, "SetTerminateResult"},
{23, D<&IApplicationFunctions::GetDisplayVersion>, "GetDisplayVersion"},
- {24, nullptr, "GetLaunchStorageInfoForDebug"},
+ {24, &IApplicationFunctions::StubReplyZeroed, "GetLaunchStorageInfoForDebug"},
{25, D<&IApplicationFunctions::ExtendSaveData>, "ExtendSaveData"},
{26, D<&IApplicationFunctions::GetSaveDataSize>, "GetSaveDataSize"},
{27, D<&IApplicationFunctions::CreateCacheStorage>, "CreateCacheStorage"},
@@ -80,53 +81,57 @@ IApplicationFunctions::IApplicationFunctions(Core::System& system_, std::shared_
{32, D<&IApplicationFunctions::BeginBlockingHomeButton>, "BeginBlockingHomeButton"},
{33, D<&IApplicationFunctions::EndBlockingHomeButton>, "EndBlockingHomeButton"},
{34, nullptr, "SelectApplicationLicense"},
- {35, nullptr, "GetDeviceSaveDataSizeMax"},
- {36, nullptr, "GetLimitedApplicationLicense"},
- {37, nullptr, "GetLimitedApplicationLicenseUpgradableEvent"},
+ {35, &IApplicationFunctions::StubReplyZeroed, "GetDeviceSaveDataSizeMax"},
+ {36, &IApplicationFunctions::StubReplyZeroed, "GetLimitedApplicationLicense"},
+ {37, &IApplicationFunctions::StubReplyEvent, "GetLimitedApplicationLicenseUpgradableEvent"},
{40, D<&IApplicationFunctions::NotifyRunning>, "NotifyRunning"},
{50, D<&IApplicationFunctions::GetPseudoDeviceId>, "GetPseudoDeviceId"},
{60, D<&IApplicationFunctions::SetMediaPlaybackStateForApplication>, "SetMediaPlaybackStateForApplication"},
{65, D<&IApplicationFunctions::IsGamePlayRecordingSupported>, "IsGamePlayRecordingSupported"},
{66, D<&IApplicationFunctions::InitializeGamePlayRecording>, "InitializeGamePlayRecording"},
{67, D<&IApplicationFunctions::SetGamePlayRecordingState>, "SetGamePlayRecordingState"},
- {68, nullptr, "RequestFlushGamePlayingMovieForDebug"},
- {70, nullptr, "RequestToShutdown"},
- {71, nullptr, "RequestToReboot"},
- {72, nullptr, "RequestToSleep"},
- {80, nullptr, "ExitAndRequestToShowThanksMessage"},
+ {68, &IApplicationFunctions::StubReplyZeroed, "RequestFlushGamePlayingMovieForDebug"},
+ {70, &IApplicationFunctions::StubReplyZeroed, "RequestToShutdown"},
+ {71, &IApplicationFunctions::StubReplyZeroed, "RequestToReboot"},
+ {72, &IApplicationFunctions::StubReplyZeroed, "RequestToSleep"},
+ {80, &IApplicationFunctions::StubReplyZeroed, "ExitAndRequestToShowThanksMessage"},
{90, D<&IApplicationFunctions::EnableApplicationCrashReport>, "EnableApplicationCrashReport"},
{100, D<&IApplicationFunctions::InitializeApplicationCopyrightFrameBuffer>, "InitializeApplicationCopyrightFrameBuffer"},
{101, D<&IApplicationFunctions::SetApplicationCopyrightImage>, "SetApplicationCopyrightImage"},
{102, D<&IApplicationFunctions::SetApplicationCopyrightVisibility>, "SetApplicationCopyrightVisibility"},
{110, D<&IApplicationFunctions::QueryApplicationPlayStatistics>, "QueryApplicationPlayStatistics"},
{111, D<&IApplicationFunctions::QueryApplicationPlayStatisticsByUid>, "QueryApplicationPlayStatisticsByUid"},
+ {112, &IApplicationFunctions::StubReplyZeroed, "Unknown112"},
+ {113, &IApplicationFunctions::StubReplyZeroed, "Unknown113"},
{120, D<&IApplicationFunctions::ExecuteProgram>, "ExecuteProgram"},
{121, D<&IApplicationFunctions::ClearUserChannel>, "ClearUserChannel"},
{122, D<&IApplicationFunctions::UnpopToUserChannel>, "UnpopToUserChannel"},
{123, D<&IApplicationFunctions::GetPreviousProgramIndex>, "GetPreviousProgramIndex"},
- {124, nullptr, "EnableApplicationAllThreadDumpOnCrash"},
+ {124, &IApplicationFunctions::StubReplyZeroed, "EnableApplicationAllThreadDumpOnCrash"},
{130, D<&IApplicationFunctions::GetGpuErrorDetectedSystemEvent>, "GetGpuErrorDetectedSystemEvent"},
- {131, nullptr, "SetDelayTimeToAbortOnGpuError"},
+ {131, &IApplicationFunctions::StubReplyZeroed, "SetDelayTimeToAbortOnGpuError"},
{140, D<&IApplicationFunctions::GetFriendInvitationStorageChannelEvent>, "GetFriendInvitationStorageChannelEvent"},
{141, D<&IApplicationFunctions::TryPopFromFriendInvitationStorageChannel>, "TryPopFromFriendInvitationStorageChannel"},
{150, D<&IApplicationFunctions::GetNotificationStorageChannelEvent>, "GetNotificationStorageChannelEvent"},
- {151, nullptr, "TryPopFromNotificationStorageChannel"},
+ {151, &IApplicationFunctions::StubReplyInterface, "TryPopFromNotificationStorageChannel"},
{160, D<&IApplicationFunctions::GetHealthWarningDisappearedSystemEvent>, "GetHealthWarningDisappearedSystemEvent"},
- {170, nullptr, "SetHdcpAuthenticationActivated"},
- {180, nullptr, "GetLaunchRequiredVersion"},
- {181, nullptr, "UpgradeLaunchRequiredVersion"},
- {190, nullptr, "SendServerMaintenanceOverlayNotification"},
- {200, nullptr, "GetLastApplicationExitReason"},
+ {170, &IApplicationFunctions::StubReplyZeroed, "SetHdcpAuthenticationActivated"},
+ {180, &IApplicationFunctions::StubReplyZeroed, "GetLaunchRequiredVersion"},
+ {181, &IApplicationFunctions::StubReplyZeroed, "UpgradeLaunchRequiredVersion"},
+ {190, &IApplicationFunctions::StubReplyZeroed, "SendServerMaintenanceOverlayNotification"},
+ {200, &IApplicationFunctions::StubReplyZeroed, "GetLastApplicationExitReason"},
{210, D<&IApplicationFunctions::GetOperationModeRefreshEvent>, "GetOperationModeRefreshEvent (guessed)"},
- {220, nullptr, "Unknown220"}, // [20.0.0+]
- {300, nullptr, "CreateMovieWriter"}, // [19.0.0+]
- {310, nullptr, "Unknown310"}, // [20.0.0+]
+ {220, &IApplicationFunctions::StubReplyZeroed, "Unknown220"}, // [20.0.0+]
+ {230, &IApplicationFunctions::StubReplyZeroed, "Unknown230"},
+ {300, &IApplicationFunctions::StubReplyInterface, "CreateMovieWriter"}, // [19.0.0+]
+ {310, &IApplicationFunctions::StubReplyZeroed, "Unknown310"}, // [20.0.0+]
{320, D<&IApplicationFunctions::Unknown320>, "Unknown320"}, // [20.0.0+]
{321, D<&IApplicationFunctions::Unknown321>, "Unknown321"}, // Switch 2
{330, D<&IApplicationFunctions::Unknown330>, "Unknown330"}, // [20.0.0+]
- {500, nullptr, "StartContinuousRecordingFlushForDebug"},
- {1000, nullptr, "CreateMovieMaker"},
+ {500, &IApplicationFunctions::StubReplyEvent, "StartContinuousRecordingFlushForDebug"},
+ {1000, &IApplicationFunctions::StubReplyInterface, "CreateMovieMaker"},
{1001, D<&IApplicationFunctions::PrepareForJit>, "PrepareForJit"},
+ {2000, &IApplicationFunctions::StubReplyZeroed, "Unknown2000"},
};
// clang-format on
@@ -571,15 +576,18 @@ Result IApplicationFunctions::GetOperationModeRefreshEvent(
R_SUCCEED();
}
-Result IApplicationFunctions::Unknown320() {
- // Called by Switch 2 titles during nn::oe::Initialize; arguments unknown.
+Result IApplicationFunctions::Unknown320(Out<u16> out) {
+ // Called by Switch 2 titles during nn::oe::Initialize. It returns two bytes of data (meaning
+ // unknown); a reply without them left the caller reading stale message words.
LOG_WARNING(Service_AM, "(STUBBED) called");
+ *out = 0;
R_SUCCEED();
}
-Result IApplicationFunctions::Unknown321() {
- // Called by Switch 2 titles right after 320; arguments unknown.
+Result IApplicationFunctions::Unknown321(Out<u8> out) {
+ // Called by Switch 2 titles right after 320; returns one byte (meaning unknown).
LOG_WARNING(Service_AM, "(STUBBED) called");
+ *out = 0;
R_SUCCEED();
}
diff --git a/src/core/hle/service/am/service/application_functions.h b/src/core/hle/service/am/service/application_functions.h
index 44b5fadff0..aca2f8c694 100644
--- a/src/core/hle/service/am/service/application_functions.h
+++ b/src/core/hle/service/am/service/application_functions.h
@@ -83,8 +83,8 @@ private:
Result GetHealthWarningDisappearedSystemEvent(OutCopyHandle<Kernel::KReadableEvent> out_event);
Result CreateApplicationAndRequestToStart(u64 application_id);
Result GetOperationModeRefreshEvent(OutCopyHandle<Kernel::KReadableEvent> out_event);
- Result Unknown320();
- Result Unknown321();
+ Result Unknown320(Out<u16> out);
+ Result Unknown321(Out<u8> out);
Result Unknown330(Out<u8> out);
Result PrepareForJit();
diff --git a/src/core/hle/service/am/service/application_proxy_service.cpp b/src/core/hle/service/am/service/application_proxy_service.cpp
index 67306aadc0..9d332798b4 100644
--- a/src/core/hle/service/am/service/application_proxy_service.cpp
+++ b/src/core/hle/service/am/service/application_proxy_service.cpp
@@ -21,6 +21,7 @@ IApplicationProxyService::IApplicationProxyService(Core::System& system_,
{0, D<&IApplicationProxyService::OpenApplicationProxy>, "OpenApplicationProxy"},
// Switch 2 SDKs open the proxy with command 10, same arguments.
{10, D<&IApplicationProxyService::OpenApplicationProxy>, "OpenApplicationProxy (Switch 2)"},
+ {2000, &IApplicationProxyService::StubReplyInterface, "Unknown2000"},
};
RegisterHandlers(functions);
}
diff --git a/src/core/hle/service/am/service/audio_controller.cpp b/src/core/hle/service/am/service/audio_controller.cpp
index 4e9af756f2..7901f67c27 100644
--- a/src/core/hle/service/am/service/audio_controller.cpp
+++ b/src/core/hle/service/am/service/audio_controller.cpp
@@ -18,7 +18,7 @@ IAudioController::IAudioController(Core::System& system_)
{2, D<&IAudioController::GetLibraryAppletExpectedMasterVolume>, "GetLibraryAppletExpectedMasterVolume"},
{3, D<&IAudioController::ChangeMainAppletMasterVolume>, "ChangeMainAppletMasterVolume"},
{4, D<&IAudioController::SetTransparentVolumeRate>, "SetTransparentVolumeRate"},
- {5, nullptr, "Unknown5"}, //20.0.0+
+ {5, &IAudioController::StubReplyZeroed, "Unknown5"}, //20.0.0+
};
// clang-format on
diff --git a/src/core/hle/service/am/service/common_state_getter.cpp b/src/core/hle/service/am/service/common_state_getter.cpp
index 044929e434..2c5e536414 100644
--- a/src/core/hle/service/am/service/common_state_getter.cpp
+++ b/src/core/hle/service/am/service/common_state_getter.cpp
@@ -24,67 +24,73 @@ ICommonStateGetter::ICommonStateGetter(Core::System& system_, std::shared_ptr<Ap
static const FunctionInfo functions[] = {
{0, D<&ICommonStateGetter::GetEventHandle>, "GetEventHandle"},
{1, D<&ICommonStateGetter::ReceiveMessage>, "ReceiveMessage"},
- {2, nullptr, "GetThisAppletKind"},
- {3, nullptr, "AllowToEnterSleep"},
- {4, nullptr, "DisallowToEnterSleep"},
+ {2, &ICommonStateGetter::StubReplyZeroed, "GetThisAppletKind"},
+ {3, &ICommonStateGetter::StubReplyZeroed, "AllowToEnterSleep"},
+ {4, &ICommonStateGetter::StubReplyZeroed, "DisallowToEnterSleep"},
{5, D<&ICommonStateGetter::GetOperationMode>, "GetOperationMode"},
{6, D<&ICommonStateGetter::GetPerformanceMode>, "GetPerformanceMode"},
- {7, nullptr, "GetCradleStatus"},
+ {7, &ICommonStateGetter::StubReplyZeroed, "GetCradleStatus"},
{8, D<&ICommonStateGetter::GetBootMode>, "GetBootMode"},
{9, D<&ICommonStateGetter::GetCurrentFocusState>, "GetCurrentFocusState"},
{10, D<&ICommonStateGetter::RequestToAcquireSleepLock>, "RequestToAcquireSleepLock"},
{11, D<&ICommonStateGetter::ReleaseSleepLock>, "ReleaseSleepLock"},
{12, D<&ICommonStateGetter::ReleaseSleepLockTransiently>, "ReleaseSleepLockTransiently"},
{13, D<&ICommonStateGetter::GetAcquiredSleepLockEvent>, "GetAcquiredSleepLockEvent"},
- {14, nullptr, "GetWakeupCount"}, //11.0.0+
- {15, nullptr, "Unknown15"}, //19.0.0+
+ {14, &ICommonStateGetter::StubReplyZeroed, "GetWakeupCount"}, //11.0.0+
+ {15, &ICommonStateGetter::StubReplyEvent, "Unknown15"}, //19.0.0+
{20, D<&ICommonStateGetter::PushToGeneralChannel>, "PushToGeneralChannel"},
{30, D<&ICommonStateGetter::GetHomeButtonReaderLockAccessor>, "GetHomeButtonReaderLockAccessor"},
{31, D<&ICommonStateGetter::GetReaderLockAccessorEx>, "GetReaderLockAccessorEx"}, //2.0.0+
{32, D<&ICommonStateGetter::GetWriterLockAccessorEx>, "GetWriterLockAccessorEx"}, //7.0.0+
- {40, nullptr, "GetCradleFwVersion"}, //2.0.0+
+ {40, &ICommonStateGetter::StubReplyZeroed, "GetCradleFwVersion"}, //2.0.0+
{50, D<&ICommonStateGetter::IsVrModeEnabled>, "IsVrModeEnabled"}, //3.0.0+
{51, D<&ICommonStateGetter::SetVrModeEnabled>, "SetVrModeEnabled"}, //3.0.0+
{52, D<&ICommonStateGetter::SetLcdBacklighOffEnabled>, "SetLcdBacklighOffEnabled"}, //4.0.0+
{53, D<&ICommonStateGetter::BeginVrModeEx>, "BeginVrModeEx"}, //7.0.0+
{54, D<&ICommonStateGetter::EndVrModeEx>, "EndVrModeEx"}, //7.0.0+
{55, D<&ICommonStateGetter::IsInControllerFirmwareUpdateSection>, "IsInControllerFirmwareUpdateSection"}, //3.0.0+
- {59, nullptr, "SetVrPositionForDebug"}, //1.0.0+
+ {59, &ICommonStateGetter::StubReplyZeroed, "SetVrPositionForDebug"}, //1.0.0+
{60, D<&ICommonStateGetter::GetDefaultDisplayResolution>, "GetDefaultDisplayResolution"},
{61, D<&ICommonStateGetter::GetDefaultDisplayResolutionChangeEvent>, "GetDefaultDisplayResolutionChangeEvent"},
{62, D<&ICommonStateGetter::GetHdcpAuthenticationState>, "GetHdcpAuthenticationState"},
{63, D<&ICommonStateGetter::GetHdcpAuthenticationStateChangeEvent>, "GetHdcpAuthenticationStateChangeEvent"},
- {64, nullptr, "SetTvPowerStateMatchingMode"},
- {65, nullptr, "GetApplicationIdByContentActionName"},
+ {64, &ICommonStateGetter::StubReplyZeroed, "SetTvPowerStateMatchingMode"},
+ {65, &ICommonStateGetter::StubReplyZeroed, "GetApplicationIdByContentActionName"},
{66, &ICommonStateGetter::SetCpuBoostMode, "SetCpuBoostMode"},
- {67, nullptr, "CancelCpuBoostMode"},
+ {67, &ICommonStateGetter::StubReplyZeroed, "CancelCpuBoostMode"},
{68, D<&ICommonStateGetter::GetBuiltInDisplayType>, "GetBuiltInDisplayType"},
{80, D<&ICommonStateGetter::PerformSystemButtonPressingIfInFocus>, "PerformSystemButtonPressingIfInFocus"},
- {90, nullptr, "SetPerformanceConfigurationChangedNotification"},
- {91, nullptr, "GetCurrentPerformanceConfiguration"},
+ {90, &ICommonStateGetter::StubReplyZeroed, "SetPerformanceConfigurationChangedNotification"},
+ {91, &ICommonStateGetter::StubReplyZeroed, "GetCurrentPerformanceConfiguration"},
{100, D<&ICommonStateGetter::SetHandlingHomeButtonShortPressedEnabled>, "SetHandlingHomeButtonShortPressedEnabled"},
- {110, nullptr, "OpenMyGpuErrorHandler"},
+ {110, &ICommonStateGetter::StubReplyInterface, "OpenMyGpuErrorHandler"},
{120, D<&ICommonStateGetter::GetAppletLaunchedHistory>, "GetAppletLaunchedHistory"}, //13.0.0+
{130, D<&ICommonStateGetter::EnableStartupLogoDisappearedMessage>, "EnableStartupLogoDisappearedMessage"}, //21.0.0+
{200, D<&ICommonStateGetter::GetOperationModeSystemInfo>, "GetOperationModeSystemInfo"},
{300, D<&ICommonStateGetter::GetSettingsPlatformRegion>, "GetSettingsPlatformRegion"},
- {400, nullptr, "ActivateMigrationService"},
- {401, nullptr, "DeactivateMigrationService"},
- {500, nullptr, "DisableSleepTillShutdown"},
- {501, nullptr, "SuppressDisablingSleepTemporarily"},
- {502, nullptr, "IsSleepEnabled"},
- {503, nullptr, "IsDisablingSleepSuppressed"},
- {600, nullptr, "SetHidInputMagnificationForApplication"}, //20.0.0+
+ {310, &ICommonStateGetter::StubReplyZeroed, "Unknown310"},
+ {320, &ICommonStateGetter::StubReplyZeroed, "Unknown320"},
+ {400, &ICommonStateGetter::StubReplyZeroed, "ActivateMigrationService"},
+ {401, &ICommonStateGetter::StubReplyZeroed, "DeactivateMigrationService"},
+ {500, &ICommonStateGetter::StubReplyZeroed, "DisableSleepTillShutdown"},
+ {501, &ICommonStateGetter::StubReplyZeroed, "SuppressDisablingSleepTemporarily"},
+ {502, &ICommonStateGetter::StubReplyZeroed, "IsSleepEnabled"},
+ {503, &ICommonStateGetter::StubReplyZeroed, "IsDisablingSleepSuppressed"},
+ {600, &ICommonStateGetter::StubReplyZeroed, "SetHidInputMagnificationForApplication"}, //20.0.0+
{610, D<&ICommonStateGetter::Unknown610>, "Unknown610"}, //21.0.0+
{611, D<&ICommonStateGetter::Unknown611>, "Unknown611"}, //22.0.0+
+ {650, &ICommonStateGetter::StubReplyZeroed, "Unknown650"},
+ {651, &ICommonStateGetter::StubReplyZeroed, "Unknown651"},
{900, D<&ICommonStateGetter::SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled>, "SetRequestExitToLibraryAppletAtExecuteNextProgramEnabled"}, //11.0.0+
- {910, nullptr, "GetLaunchRequiredTick"}, //17.0.0+
+ {910, &ICommonStateGetter::StubReplyZeroed, "GetLaunchRequiredTick"}, //17.0.0+
{1000, D<&ICommonStateGetter::BeginVrMode3d>, "BeginVrMode3d"}, //19.0.0+
{1001, D<&ICommonStateGetter::EndVrMode3d>, "EndVrMode3d"}, //19.0.0+
{1002, D<&ICommonStateGetter::IsVrModeEnabled3d>, "IsVrModeEnabled3d"}, //19.0.0+
{1003, D<&ICommonStateGetter::GetVrLaboGoggleViewport>, "GetVrLaboGoggleViewport"}, //21.0.0+
{1004, D<&ICommonStateGetter::GetPanelPhysicalSizeForSpecificTitle>, "GetPanelPhysicalSizeForSpecificTitle"}, //21.0.0+
{1005, D<&ICommonStateGetter::GetPanelResolutionForSpecificTitle>, "GetPanelResolutionForSpecificTitle"}, //21.0.0+
+ {2000, &ICommonStateGetter::StubReplyZeroed, "Unknown2000"},
+ {2010, &ICommonStateGetter::StubReplyZeroed, "Unknown2010"},
};
// clang-format on
diff --git a/src/core/hle/service/am/service/display_controller.cpp b/src/core/hle/service/am/service/display_controller.cpp
index 8f87327b17..7e7db23708 100644
--- a/src/core/hle/service/am/service/display_controller.cpp
+++ b/src/core/hle/service/am/service/display_controller.cpp
@@ -16,34 +16,35 @@ IDisplayController::IDisplayController(Core::System& system_, std::shared_ptr<Ap
: ServiceFramework{system_, "IDisplayController"}, applet(std::move(applet_)) {
// clang-format off
static const FunctionInfo functions[] = {
- {0, nullptr, "GetLastForegroundCaptureImage"},
- {1, nullptr, "UpdateLastForegroundCaptureImage"},
- {2, nullptr, "GetLastApplicationCaptureImage"},
- {3, nullptr, "GetCallerAppletCaptureImage"},
- {4, nullptr, "UpdateCallerAppletCaptureImage"},
+ {0, &IDisplayController::StubReplyZeroed, "GetLastForegroundCaptureImage"},
+ {1, &IDisplayController::StubReplyZeroed, "UpdateLastForegroundCaptureImage"},
+ {2, &IDisplayController::StubReplyZeroed, "GetLastApplicationCaptureImage"},
+ {3, &IDisplayController::StubReplyZeroed, "GetCallerAppletCaptureImage"},
+ {4, &IDisplayController::StubReplyZeroed, "UpdateCallerAppletCaptureImage"},
{5, D<&IDisplayController::GetLastForegroundCaptureImageEx>, "GetLastForegroundCaptureImageEx"},
- {6, nullptr, "GetLastApplicationCaptureImageEx"},
+ {6, &IDisplayController::StubReplyZeroed, "GetLastApplicationCaptureImageEx"},
{7, D<&IDisplayController::GetCallerAppletCaptureImageEx>, "GetCallerAppletCaptureImageEx"},
{8, D<&IDisplayController::TakeScreenShotOfOwnLayer>, "TakeScreenShotOfOwnLayer"},
- {9, nullptr, "CopyBetweenCaptureBuffers"},
- {10, nullptr, "AcquireLastApplicationCaptureBuffer"},
- {11, nullptr, "ReleaseLastApplicationCaptureBuffer"},
- {12, nullptr, "AcquireLastForegroundCaptureBuffer"},
- {13, nullptr, "ReleaseLastForegroundCaptureBuffer"},
- {14, nullptr, "AcquireCallerAppletCaptureBuffer"},
- {15, nullptr, "ReleaseCallerAppletCaptureBuffer"},
- {16, nullptr, "AcquireLastApplicationCaptureBufferEx"},
- {17, nullptr, "AcquireLastForegroundCaptureBufferEx"},
- {18, nullptr, "AcquireCallerAppletCaptureBufferEx"},
+ {9, &IDisplayController::StubReplyZeroed, "CopyBetweenCaptureBuffers"},
+ {10, &IDisplayController::StubReplyEvent, "AcquireLastApplicationCaptureBuffer"},
+ {11, &IDisplayController::StubReplyZeroed, "ReleaseLastApplicationCaptureBuffer"},
+ {12, &IDisplayController::StubReplyEvent, "AcquireLastForegroundCaptureBuffer"},
+ {13, &IDisplayController::StubReplyZeroed, "ReleaseLastForegroundCaptureBuffer"},
+ {14, &IDisplayController::StubReplyEvent, "AcquireCallerAppletCaptureBuffer"},
+ {15, &IDisplayController::StubReplyZeroed, "ReleaseCallerAppletCaptureBuffer"},
+ {16, &IDisplayController::StubReplyEvent, "AcquireLastApplicationCaptureBufferEx"},
+ {17, &IDisplayController::StubReplyEvent, "AcquireLastForegroundCaptureBufferEx"},
+ {18, &IDisplayController::StubReplyEvent, "AcquireCallerAppletCaptureBufferEx"},
+ {19, &IDisplayController::StubReplyZeroed, "Unknown19"},
{20, D<&IDisplayController::ClearCaptureBuffer>, "ClearCaptureBuffer"},
- {21, nullptr, "ClearAppletTransitionBuffer"},
+ {21, &IDisplayController::StubReplyZeroed, "ClearAppletTransitionBuffer"},
{22, D<&IDisplayController::AcquireLastApplicationCaptureSharedBuffer>, "AcquireLastApplicationCaptureSharedBuffer"},
{23, D<&IDisplayController::ReleaseLastApplicationCaptureSharedBuffer>, "ReleaseLastApplicationCaptureSharedBuffer"},
{24, D<&IDisplayController::AcquireLastForegroundCaptureSharedBuffer>, "AcquireLastForegroundCaptureSharedBuffer"},
{25, D<&IDisplayController::ReleaseLastForegroundCaptureSharedBuffer>, "ReleaseLastForegroundCaptureSharedBuffer"},
{26, D<&IDisplayController::AcquireCallerAppletCaptureSharedBuffer>, "AcquireCallerAppletCaptureSharedBuffer"},
{27, D<&IDisplayController::ReleaseCallerAppletCaptureSharedBuffer>, "ReleaseCallerAppletCaptureSharedBuffer"},
- {28, nullptr, "TakeScreenShotOfOwnLayerEx"},
+ {28, &IDisplayController::StubReplyZeroed, "TakeScreenShotOfOwnLayerEx"},
};
// clang-format on
diff --git a/src/core/hle/service/am/service/graphics_scale_controller.h b/src/core/hle/service/am/service/graphics_scale_controller.h
index 07e38c6ac8..b31858d3ed 100644
--- a/src/core/hle/service/am/service/graphics_scale_controller.h
+++ b/src/core/hle/service/am/service/graphics_scale_controller.h
@@ -25,9 +25,13 @@ public:
}
private:
- Result GetCurrentGraphicsScale(Out<u32> out_scale) {
+ // The reply is 12 bytes; only the first word (the scale) is known. The other two used to be
+ // left to the message buffer's leftovers.
+ Result GetCurrentGraphicsScale(Out<u32> out_scale, Out<u32> out_unknown1, Out<u32> out_unknown2) {
LOG_WARNING(Service_AM, "(STUBBED) called, returning 10");
*out_scale = 10;
+ *out_unknown1 = 0;
+ *out_unknown2 = 0;
R_SUCCEED();
}
};
diff --git a/src/core/hle/service/am/service/library_applet_accessor.cpp b/src/core/hle/service/am/service/library_applet_accessor.cpp
index 849e5794cf..7119c60596 100644
--- a/src/core/hle/service/am/service/library_applet_accessor.cpp
+++ b/src/core/hle/service/am/service/library_applet_accessor.cpp
@@ -62,21 +62,22 @@ ILibraryAppletAccessor::ILibraryAppletAccessor(Core::System& system_,
{20, D<&ILibraryAppletAccessor::RequestExit>, "RequestExit"},
{25, D<&ILibraryAppletAccessor::Terminate>, "Terminate"},
{30, D<&ILibraryAppletAccessor::GetResult>, "GetResult"},
- {50, nullptr, "SetOutOfFocusApplicationSuspendingEnabled"},
+ {50, &ILibraryAppletAccessor::StubReplyZeroed, "SetOutOfFocusApplicationSuspendingEnabled"},
{60, D<&ILibraryAppletAccessor::PresetLibraryAppletGpuTimeSliceZero>, "PresetLibraryAppletGpuTimeSliceZero"}, //10.0.0+
- {80, nullptr, "RequestForLibraryAppletToGetForeground"}, //19.0.0+
- {81, nullptr, "GetCurrentChildLibraryApplet"}, //19.0.0+
+ {70, &ILibraryAppletAccessor::StubReplyZeroed, "Unknown70"},
+ {80, &ILibraryAppletAccessor::StubReplyZeroed, "RequestForLibraryAppletToGetForeground"}, //19.0.0+
+ {81, &ILibraryAppletAccessor::StubReplyInterface, "GetCurrentChildLibraryApplet"}, //19.0.0+
{90, D<&ILibraryAppletAccessor::Unknown90>, "Unknown90"}, //20.0.0+
{100, D<&ILibraryAppletAccessor::PushInData>, "PushInData"},
{101, D<&ILibraryAppletAccessor::PopOutData>, "PopOutData"},
- {102, nullptr, "PushExtraStorage"},
+ {102, &ILibraryAppletAccessor::StubReplyZeroed, "PushExtraStorage"},
{103, D<&ILibraryAppletAccessor::PushInteractiveInData>, "PushInteractiveInData"},
{104, D<&ILibraryAppletAccessor::PopInteractiveOutData>, "PopInteractiveOutData"},
{105, D<&ILibraryAppletAccessor::GetPopOutDataEvent>, "GetPopOutDataEvent"},
{106, D<&ILibraryAppletAccessor::GetPopInteractiveOutDataEvent>, "GetPopInteractiveOutDataEvent"},
- {110, nullptr, "NeedsToExitProcess"},
+ {110, &ILibraryAppletAccessor::StubReplyZeroed, "NeedsToExitProcess"},
{120, D<&ILibraryAppletAccessor::GetLibraryAppletInfo>, "GetLibraryAppletInfo"},
- {150, nullptr, "RequestForAppletToGetForeground"},
+ {150, &ILibraryAppletAccessor::StubReplyZeroed, "RequestForAppletToGetForeground"},
{160, D<&ILibraryAppletAccessor::GetIndirectLayerConsumerHandle>, "GetIndirectLayerConsumerHandle"}, //2.0.0+
{170, D<&ILibraryAppletAccessor::Unknown170>, "Unknown170"}, //22.0.0+
};
diff --git a/src/core/hle/service/am/service/library_applet_creator.cpp b/src/core/hle/service/am/service/library_applet_creator.cpp
index 4ee92908b5..2993ed7436 100644
--- a/src/core/hle/service/am/service/library_applet_creator.cpp
+++ b/src/core/hle/service/am/service/library_applet_creator.cpp
@@ -4,6 +4,9 @@
// SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <mutex>
+#include <unordered_set>
+
#include "common/settings.h"
#include "core/hle/kernel/k_transfer_memory.h"
#include "core/hle/service/am/applet_data_broker.h"
@@ -99,6 +102,41 @@ AppletProgramId AppletIdToProgramId(AppletId applet_id) {
}
}
+AppletId NormalizeAppletId(AppletId id) {
+ const u32 raw_id = static_cast<u32>(id);
+ static std::mutex mutex;
+ static std::unordered_set<u32> logged_raw_ids;
+ static std::unordered_set<u32> warned_raw_ids;
+
+ bool log_info = false;
+ {
+ std::scoped_lock lk{mutex};
+ log_info = logged_raw_ids.insert(raw_id).second;
+ }
+ if (log_info) {
+ LOG_INFO(Service_AM, "CreateLibraryApplet called with raw applet_id={:#x}", raw_id);
+ }
+
+ if (AppletIdToProgramId(id) == static_cast<AppletProgramId>(0)) {
+ const auto low_byte = static_cast<AppletId>(raw_id & 0xFF);
+ if (AppletIdToProgramId(low_byte) != static_cast<AppletProgramId>(0)) {
+ bool log_warning = false;
+ {
+ std::scoped_lock lk{mutex};
+ log_warning = warned_raw_ids.insert(raw_id).second;
+ }
+ if (log_warning) {
+ LOG_WARNING(Service_AM,
+ "Normalizing unknown raw applet_id={:#x} to low byte {:#x}",
+ raw_id, static_cast<u32>(low_byte));
+ }
+ return low_byte;
+ }
+ }
+
+ return id;
+}
+
std::shared_ptr<ILibraryAppletAccessor> CreateGuestApplet(Core::System& system,
WindowSystem& window_system,
std::shared_ptr<Applet> caller_applet,
@@ -166,8 +204,8 @@ ILibraryAppletCreator::ILibraryAppletCreator(Core::System& system_, std::shared_
m_window_system{window_system}, m_applet{std::move(applet)} {
static const FunctionInfo functions[] = {
{0, D<&ILibraryAppletCreator::CreateLibraryApplet>, "CreateLibraryApplet"},
- {1, nullptr, "TerminateAllLibraryApplets"},
- {2, nullptr, "AreAnyLibraryAppletsLeft"},
+ {1, &ILibraryAppletCreator::StubReplyZeroed, "TerminateAllLibraryApplets"},
+ {2, &ILibraryAppletCreator::StubReplyZeroed, "AreAnyLibraryAppletsLeft"},
{3, D<&ILibraryAppletCreator::CreateLibraryAppletEx>, "CreateLibraryAppletEx"},
{10, D<&ILibraryAppletCreator::CreateStorage>, "CreateStorage"},
{11, D<&ILibraryAppletCreator::CreateTransferMemoryStorage>, "CreateTransferMemoryStorage"},
@@ -184,6 +222,8 @@ Result ILibraryAppletCreator::CreateLibraryApplet(
LOG_DEBUG(Service_AM, "called with applet_id={} applet_mode={}", applet_id,
library_applet_mode);
+ applet_id = NormalizeAppletId(applet_id);
+
std::shared_ptr<ILibraryAppletAccessor> library_applet;
if (ShouldCreateGuestApplet(applet_id)) {
library_applet =
@@ -210,6 +250,8 @@ Result ILibraryAppletCreator::CreateLibraryAppletEx(
LOG_DEBUG(Service_AM, "called with applet_id={} applet_mode={} thread_id={}", applet_id,
library_applet_mode, thread_id);
+ applet_id = NormalizeAppletId(applet_id);
+
std::shared_ptr<ILibraryAppletAccessor> library_applet;
if (ShouldCreateGuestApplet(applet_id)) {
library_applet =
diff --git a/src/core/hle/service/am/service/self_controller.cpp b/src/core/hle/service/am/service/self_controller.cpp
index a8078e6048..d67e1b32cd 100644
--- a/src/core/hle/service/am/service/self_controller.cpp
+++ b/src/core/hle/service/am/service/self_controller.cpp
@@ -37,44 +37,59 @@ ISelfController::ISelfController(Core::System& system_, std::shared_ptr<Applet>
{14, D<&ISelfController::SetRestartMessageEnabled>, "SetRestartMessageEnabled"},
{15, D<&ISelfController::SetScreenShotAppletIdentityInfo>, "SetScreenShotAppletIdentityInfo"},
{16, D<&ISelfController::SetOutOfFocusSuspendingEnabled>, "SetOutOfFocusSuspendingEnabled"},
- {17, nullptr, "SetControllerFirmwareUpdateSection"},
- {18, nullptr, "SetRequiresCaptureButtonShortPressedMessage"},
+ {17, &ISelfController::StubReplyZeroed, "SetControllerFirmwareUpdateSection"},
+ {18, &ISelfController::StubReplyZeroed, "SetRequiresCaptureButtonShortPressedMessage"},
{19, D<&ISelfController::SetAlbumImageOrientation>, "SetAlbumImageOrientation"},
- {20, nullptr, "SetDesirableKeyboardLayout"},
- {21, nullptr, "GetScreenShotProgramId"},
+ {20, &ISelfController::StubReplyZeroed, "SetDesirableKeyboardLayout"},
+ {21, &ISelfController::StubReplyZeroed, "GetScreenShotProgramId"},
+ {22, &ISelfController::StubReplyZeroed, "Unknown22"},
+ {23, &ISelfController::StubReplyZeroed, "Unknown23"},
+ {24, &ISelfController::StubReplyZeroed, "Unknown24"},
{40, D<&ISelfController::CreateManagedDisplayLayer>, "CreateManagedDisplayLayer"},
{41, D<&ISelfController::IsSystemBufferSharingEnabled>, "IsSystemBufferSharingEnabled"},
{42, D<&ISelfController::GetSystemSharedLayerHandle>, "GetSystemSharedLayerHandle"},
{43, D<&ISelfController::GetSystemSharedBufferHandle>, "GetSystemSharedBufferHandle"},
{44, D<&ISelfController::CreateManagedDisplaySeparableLayer>, "CreateManagedDisplaySeparableLayer"},
- {45, nullptr, "SetManagedDisplayLayerSeparationMode"},
- {46, nullptr, "SetRecordingLayerCompositionEnabled"},
+ {45, &ISelfController::StubReplyZeroed, "SetManagedDisplayLayerSeparationMode"},
+ {46, &ISelfController::StubReplyZeroed, "SetRecordingLayerCompositionEnabled"},
{50, D<&ISelfController::SetHandlesRequestToDisplay>, "SetHandlesRequestToDisplay"},
{51, D<&ISelfController::ApproveToDisplay>, "ApproveToDisplay"},
{60, D<&ISelfController::OverrideAutoSleepTimeAndDimmingTime>, "OverrideAutoSleepTimeAndDimmingTime"},
{61, D<&ISelfController::SetMediaPlaybackState>, "SetMediaPlaybackState"},
{62, D<&ISelfController::SetIdleTimeDetectionExtension>, "SetIdleTimeDetectionExtension"},
{63, D<&ISelfController::GetIdleTimeDetectionExtension>, "GetIdleTimeDetectionExtension"},
- {64, nullptr, "SetInputDetectionSourceSet"},
+ {64, &ISelfController::StubReplyZeroed, "SetInputDetectionSourceSet"},
{65, D<&ISelfController::ReportUserIsActive>, "ReportUserIsActive"},
- {66, nullptr, "GetCurrentIlluminance"},
+ {66, &ISelfController::StubReplyZeroed, "GetCurrentIlluminance"},
{67, D<&ISelfController::IsIlluminanceAvailable>, "IsIlluminanceAvailable"},
{68, D<&ISelfController::SetAutoSleepDisabled>, "SetAutoSleepDisabled"},
{69, D<&ISelfController::IsAutoSleepDisabled>, "IsAutoSleepDisabled"},
- {70, nullptr, "ReportMultimediaError"},
- {71, nullptr, "GetCurrentIlluminanceEx"},
+ {70, &ISelfController::StubReplyZeroed, "ReportMultimediaError"},
+ {71, &ISelfController::StubReplyZeroed, "GetCurrentIlluminanceEx"},
{72, D<&ISelfController::SetInputDetectionPolicy>, "SetInputDetectionPolicy"},
- {80, nullptr, "SetWirelessPriorityMode"},
+ {73, &ISelfController::StubReplyZeroed, "Unknown73"},
+ {80, &ISelfController::StubReplyZeroed, "SetWirelessPriorityMode"},
{90, D<&ISelfController::GetAccumulatedSuspendedTickValue>, "GetAccumulatedSuspendedTickValue"},
{91, D<&ISelfController::GetAccumulatedSuspendedTickChangedEvent>, "GetAccumulatedSuspendedTickChangedEvent"},
{100, D<&ISelfController::SetAlbumImageTakenNotificationEnabled>, "SetAlbumImageTakenNotificationEnabled"},
- {110, nullptr, "SetApplicationAlbumUserData"},
+ {110, &ISelfController::StubReplyZeroed, "SetApplicationAlbumUserData"},
{120, D<&ISelfController::SaveCurrentScreenshot>, "SaveCurrentScreenshot"},
{130, D<&ISelfController::SetRecordVolumeMuted>, "SetRecordVolumeMuted"},
+ {200, &ISelfController::StubReplyZeroed, "Unknown200"},
+ {210, &ISelfController::StubReplyZeroed, "Unknown210"},
+ {211, &ISelfController::StubReplyZeroed, "Unknown211"},
+ {220, &ISelfController::StubReplyZeroed, "Unknown220"},
+ {221, &ISelfController::StubReplyZeroed, "Unknown221"},
{230, D<&ISelfController::Unknown230>, "Unknown230"},
{240, D<&ISelfController::Unknown240>, "Unknown240"},
- {1000, nullptr, "GetDebugStorageChannel"},
+ {1000, &ISelfController::StubReplyInterface, "GetDebugStorageChannel"},
{2000, &ISelfController::GetNx2GraphicsSystemSession, "GetGraphicsSystemSession (Switch 2, guessed)"},
+ {2100, &ISelfController::StubReplyZeroed, "Unknown2100"},
+ {2200, &ISelfController::StubReplyZeroed, "Unknown2200"},
+ {2300, &ISelfController::StubReplyZeroed, "Unknown2300"},
+ {2400, &ISelfController::StubReplyZeroed, "Unknown2400"},
+ {2401, &ISelfController::StubReplyZeroed, "Unknown2401"},
+ {2402, &ISelfController::StubReplyZeroed, "Unknown2402"},
};
// clang-format on
diff --git a/src/core/hle/service/am/service/window_controller.cpp b/src/core/hle/service/am/service/window_controller.cpp
index f2b9dccb27..20bf71083e 100644
--- a/src/core/hle/service/am/service/window_controller.cpp
+++ b/src/core/hle/service/am/service/window_controller.cpp
@@ -18,7 +18,7 @@ IWindowController::IWindowController(Core::System& system_, std::shared_ptr<Appl
m_window_system{window_system}, m_applet{std::move(applet)} {
// clang-format off
static const FunctionInfo functions[] = {
- {0, nullptr, "CreateWindow"},
+ {0, &IWindowController::StubReplyInterface, "CreateWindow"},
{1, D<&IWindowController::GetAppletResourceUserId>, "GetAppletResourceUserId"},
{2, D<&IWindowController::GetAppletResourceUserIdOfCallerApplet>, "GetAppletResourceUserIdOfCallerApplet"},
{10, D<&IWindowController::AcquireForegroundRights>, "AcquireForegroundRights"},
diff --git a/src/core/hle/service/aoc/addon_content_manager.cpp b/src/core/hle/service/aoc/addon_content_manager.cpp
index be2aa34baa..c63b08e40d 100644
--- a/src/core/hle/service/aoc/addon_content_manager.cpp
+++ b/src/core/hle/service/aoc/addon_content_manager.cpp
@@ -68,19 +68,19 @@ IAddOnContentManager::IAddOnContentManager(Core::System& system_)
{6, nullptr, "PrepareAddOnContentByApplicationId"},
{7, D<&IAddOnContentManager::PrepareAddOnContent>, "PrepareAddOnContent"},
{8, D<&IAddOnContentManager::GetAddOnContentListChangedEvent>, "GetAddOnContentListChangedEvent"},
- {9, nullptr, "GetAddOnContentLostErrorCode"},
+ {9, &IAddOnContentManager::StubReplyZeroed, "GetAddOnContentLostErrorCode"},
{10, D<&IAddOnContentManager::GetAddOnContentListChangedEventWithProcessId>, "GetAddOnContentListChangedEventWithProcessId"},
{11, D<&IAddOnContentManager::NotifyMountAddOnContent>, "NotifyMountAddOnContent"},
{12, D<&IAddOnContentManager::NotifyUnmountAddOnContent>, "NotifyUnmountAddOnContent"},
- {13, nullptr, "IsAddOnContentMountedForDebug"},
+ {13, &IAddOnContentManager::StubReplyZeroed, "IsAddOnContentMountedForDebug"},
{50, D<&IAddOnContentManager::CheckAddOnContentMountStatus>, "CheckAddOnContentMountStatus"},
{100, D<&IAddOnContentManager::CreateEcPurchasedEventManager>, "CreateEcPurchasedEventManager"},
{101, D<&IAddOnContentManager::CreatePermanentEcPurchasedEventManager>, "CreatePermanentEcPurchasedEventManager"},
- {110, nullptr, "CreateContentsServiceManager"},
- {200, nullptr, "SetRequiredAddOnContentsOnContentsAvailabilityTransition"},
- {300, nullptr, "SetupHostAddOnContent"},
- {301, nullptr, "GetRegisteredAddOnContentPath"},
- {302, nullptr, "UpdateCachedList"},
+ {110, &IAddOnContentManager::StubReplyInterface, "CreateContentsServiceManager"},
+ {200, &IAddOnContentManager::StubReplyZeroed, "SetRequiredAddOnContentsOnContentsAvailabilityTransition"},
+ {300, &IAddOnContentManager::StubReplyZeroed, "SetupHostAddOnContent"},
+ {301, &IAddOnContentManager::StubReplyZeroed, "GetRegisteredAddOnContentPath"},
+ {302, &IAddOnContentManager::StubReplyZeroed, "UpdateCachedList"},
};
// clang-format on
diff --git a/src/core/hle/service/audio/audio.cpp b/src/core/hle/service/audio/audio.cpp
index 211e1cb6bc..eb1511857b 100644
--- a/src/core/hle/service/audio/audio.cpp
+++ b/src/core/hle/service/audio/audio.cpp
@@ -6,6 +6,7 @@
#include "core/core.h"
#include "core/hle/service/audio/audio.h"
+#include "core/hle/service/audio/nx2_audio_device_manager.h"
#include "core/hle/service/audio/nx2_audio_user_client.h"
#include "core/hle/service/audio/audio_controller.h"
#include "core/hle/service/audio/audio_in_manager.h"
@@ -206,6 +207,8 @@ void LoopProcess(Core::System& system) {
server_manager->RegisterNamedService("audren:u", std::make_shared<IAudioRendererManager>(system), 30);
// Switch 2 audio entry point (nn::audio::CreateUserClientSessionByHipc).
server_manager->RegisterNamedService("aud:u", std::make_shared<INx2AudioUserClient>(system), 30);
+ server_manager->RegisterNamedService("auddmg:u", std::make_shared<INx2AudioDeviceManagerService>(system), 30);
+ server_manager->RegisterNamedService("audsmx:u", std::make_shared<INx2AudioSoundMixerService>(system), 30);
server_manager->RegisterNamedService("audren:a", std::make_shared<IAudioRendererManagerForApplet>(system), 30);
server_manager->RegisterNamedService("hwopus", std::make_shared<IHardwareOpusDecoderManager>(system), 25);
ServerManager::RunServer(std::move(server_manager));
diff --git a/src/core/hle/service/audio/audio_device.cpp b/src/core/hle/service/audio/audio_device.cpp
index e9f7e074a1..8f5856b76c 100644
--- a/src/core/hle/service/audio/audio_device.cpp
+++ b/src/core/hle/service/audio/audio_device.cpp
@@ -16,7 +16,9 @@ IAudioDevice::IAudioDevice(Core::System& system_, u64 applet_resource_user_id, u
u32 device_num)
: ServiceFramework{system_, "IAudioDevice"}, service_context{system_, "IAudioDevice"},
impl{std::make_unique<AudioDevice>(system_, applet_resource_user_id, revision)},
- event{service_context.CreateEvent(fmt::format("IAudioDeviceEvent-{}", device_num))} {
+ event{service_context.CreateEvent(fmt::format("IAudioDeviceEvent-{}", device_num))},
+ notification_event{service_context.CreateEvent(
+ fmt::format("IAudioDeviceNotification-{}", device_num))} {
static const FunctionInfo functions[] = {
{0, D<&IAudioDevice::ListAudioDeviceName>, "ListAudioDeviceName"},
{1, D<&IAudioDevice::SetAudioDeviceOutputVolume>, "SetAudioDeviceOutputVolume"},
@@ -27,18 +29,19 @@ IAudioDevice::IAudioDevice(Core::System& system_, u64 applet_resource_user_id, u
{6, D<&IAudioDevice::ListAudioDeviceNameAuto>, "ListAudioDeviceNameAuto"},
{7, D<&IAudioDevice::SetAudioDeviceOutputVolumeAuto>, "SetAudioDeviceOutputVolumeAuto"},
{8, D<&IAudioDevice::GetAudioDeviceOutputVolumeAuto>, "GetAudioDeviceOutputVolumeAuto"},
+ {9, &IAudioDevice::StubReplyZeroed, "Unknown9"},
{10, D<&IAudioDevice::GetActiveAudioDeviceNameAuto>, "GetActiveAudioDeviceNameAuto"},
{11, D<&IAudioDevice::QueryAudioDeviceInputEvent>, "QueryAudioDeviceInputEvent"},
{12, D<&IAudioDevice::QueryAudioDeviceOutputEvent>, "QueryAudioDeviceOutputEvent"},
{13, D<&IAudioDevice::GetActiveAudioDeviceName>, "GetActiveAudioOutputDeviceName"},
{14, D<&IAudioDevice::ListAudioOutputDeviceName>, "ListAudioOutputDeviceName"},
- {15, nullptr, "AcquireAudioInputDeviceNotification"}, //17.0.0+
- {16, nullptr, "ReleaseAudioInputDeviceNotification"}, //17.0.0+
- {17, nullptr, "AcquireAudioOutputDeviceNotification"}, //17.0.0+
- {18, nullptr, "ReleaseAudioOutputDeviceNotification"}, //17.0.0+
+ {15, D<&IAudioDevice::AcquireAudioDeviceNotification>, "AcquireAudioInputDeviceNotification"}, //17.0.0+
+ {16, D<&IAudioDevice::ReleaseAudioDeviceNotification>, "ReleaseAudioInputDeviceNotification"}, //17.0.0+
+ {17, D<&IAudioDevice::AcquireAudioDeviceNotification>, "AcquireAudioOutputDeviceNotification"}, //17.0.0+
+ {18, D<&IAudioDevice::ReleaseAudioDeviceNotification>, "ReleaseAudioOutputDeviceNotification"}, //17.0.0+
{19, D<&IAudioDevice::SetAudioDeviceOutputVolumeAutoTuneEnabled>, "SetAudioDeviceOutputVolumeAutoTuneEnabled"}, //18.0.0+
{20, D<&IAudioDevice::IsAudioDeviceOutputVolumeAutoTuneEnabled>, "IsAudioDeviceOutputVolumeAutoTuneEnabled"}, //18.0.0+
- {21, nullptr, "IsActiveOutputDeviceEstimatedLowLatency"} //21.0.0+
+ {21, &IAudioDevice::StubReplyZeroed, "IsActiveOutputDeviceEstimatedLowLatency"} //21.0.0+
};
RegisterHandlers(functions);
@@ -46,6 +49,7 @@ IAudioDevice::IAudioDevice(Core::System& system_, u64 applet_resource_user_id, u
}
IAudioDevice::~IAudioDevice() {
+ service_context.CloseEvent(notification_event);
service_context.CloseEvent(event);
}
@@ -145,6 +149,21 @@ Result IAudioDevice::QueryAudioDeviceOutputEvent(OutCopyHandle<Kernel::KReadable
R_SUCCEED();
}
+// 15/17(u64): an event that fires when the input/output device set changes, which it never
+// does here. Switch 2 titles (Kirby Air Riders, Switch Sports) ask for it; the auto stub
+// replied without a handle.
+Result IAudioDevice::AcquireAudioDeviceNotification(OutCopyHandle<Kernel::KReadableEvent> out_event,
+ u64 unknown) {
+ LOG_DEBUG(Service_Audio, "called, unknown={:#x}", unknown);
+ *out_event = &notification_event->GetReadableEvent();
+ R_SUCCEED();
+}
+
+Result IAudioDevice::ReleaseAudioDeviceNotification(u64 unknown) {
+ LOG_DEBUG(Service_Audio, "called, unknown={:#x}", unknown);
+ R_SUCCEED();
+}
+
Result IAudioDevice::GetActiveChannelCount(Out<u32> out_active_channel_count) {
*out_active_channel_count = system.AudioCore().GetOutputSink().GetSystemChannels();
LOG_DEBUG(Service_Audio, "(STUBBED) called. Channels={}", *out_active_channel_count);
diff --git a/src/core/hle/service/audio/audio_device.h b/src/core/hle/service/audio/audio_device.h
index f6c12fbfeb..8a7b9a70b0 100644
--- a/src/core/hle/service/audio/audio_device.h
+++ b/src/core/hle/service/audio/audio_device.h
@@ -54,10 +54,14 @@ private:
Out<s32> out_count);
Result SetAudioDeviceOutputVolumeAutoTuneEnabled(bool enabled);
Result IsAudioDeviceOutputVolumeAutoTuneEnabled(Out<bool> out_enabled);
+ Result AcquireAudioDeviceNotification(OutCopyHandle<Kernel::KReadableEvent> out_event,
+ u64 unknown);
+ Result ReleaseAudioDeviceNotification(u64 unknown);
KernelHelpers::ServiceContext service_context;
std::unique_ptr<AudioCore::Renderer::AudioDevice> impl;
Kernel::KEvent* event;
+ Kernel::KEvent* notification_event;
};
} // namespace Service::Audio
diff --git a/src/core/hle/service/audio/audio_renderer_manager.cpp b/src/core/hle/service/audio/audio_renderer_manager.cpp
index 97d9f44ddd..d669d64cec 100644
--- a/src/core/hle/service/audio/audio_renderer_manager.cpp
+++ b/src/core/hle/service/audio/audio_renderer_manager.cpp
@@ -27,7 +27,7 @@ IAudioRendererManager::IAudioRendererManager(Core::System& system_)
{0, D<&IAudioRendererManager::OpenAudioRenderer>, "OpenAudioRenderer"},
{1, D<&IAudioRendererManager::GetWorkBufferSize>, "GetWorkBufferSize"},
{2, D<&IAudioRendererManager::GetAudioDeviceService>, "GetAudioDeviceService"},
- {3, nullptr, "OpenAudioRendererForManualExecution"},
+ {3, &IAudioRendererManager::StubReplyInterface, "OpenAudioRendererForManualExecution"},
{4, D<&IAudioRendererManager::GetAudioDeviceServiceWithRevisionInfo>, "GetAudioDeviceServiceWithRevisionInfo"},
};
// clang-format on
diff --git a/src/core/hle/service/audio/nx2_audio_device_manager.h b/src/core/hle/service/audio/nx2_audio_device_manager.h
new file mode 100644
index 0000000000..746454660e
--- /dev/null
+++ b/src/core/hle/service/audio/nx2_audio_device_manager.h
@@ -0,0 +1,539 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <algorithm>
+#include <array>
+#include <atomic>
+#include <chrono>
+#include <cstring>
+#include <map>
+#include <memory>
+#include <mutex>
+#include <optional>
+#include <string_view>
+#include <vector>
+
+#include <fmt/ranges.h>
+
+#include "common/string_util.h"
+#include "core/core.h"
+#include "core/core_timing.h"
+#include "core/hle/kernel/k_event.h"
+#include "core/memory.h"
+#include "core/hle/service/ipc_helpers.h"
+#include "core/hle/service/kernel_helpers.h"
+#include "core/hle/service/service.h"
+
+namespace Service::Audio {
+
+// Switch 2 sound mixer streams (nn::audio::sysmix, behind audsmx:u). A stream is a ring buffer in
+// the game's own memory: CreateStream hands over its address and size (4 KiB aligned), OpenStream
+// its format and length in frames, and once the streams are engaged the system takes frames out of
+// the ring at the stream's sample rate. Eden doesn't mix these into its output yet; it drains the
+// rings in real time so the game sees its audio being played (Switch Sports' menu movie feeds one).
+// The ring's shared indices, in frames modulo (frame count + 1): the system's read index at
+// buffer + 0x44, the game's write index at buffer + 0x1004.
+struct Nx2SysmixStream {
+ u32 id{};
+ u32 owner{}; ///< the user session whose timer drains it
+ u64 buffer{};
+ u64 size{};
+ u32 sample_type{};
+ u32 channels{};
+ u32 sample_rate{};
+ u32 frames{};
+ bool open{};
+ bool engaged{};
+ double owed_frames{};
+ u32 data_mode{}; ///< command 7's mode; 0 leaves the data event alone
+ s32 data_threshold{}; ///< free frames that signal the data event
+ Kernel::KEvent* state_event{};
+ Kernel::KEvent* data_event{};
+};
+
+struct Nx2SysmixStreams {
+ std::mutex mutex;
+ std::map<u32, std::shared_ptr<Nx2SysmixStream>> streams;
+ u32 next_id{1};
+};
+
+// Streams are created through audsmx:u but started and stopped through auddmg:u (AttachStream and
+// DetachStream), so both services share one list.
+inline std::shared_ptr<Nx2SysmixStreams> Nx2SysmixStreamList() {
+ static const auto list = std::make_shared<Nx2SysmixStreams>();
+ return list;
+}
+
+// Switch 2 virtual output devices (nn::audio::ListVirtualOutputDevices and friends), reached through
+// the object auddmg:u command 0 returns. Eden plays everything through one stereo 48 kHz output, so
+// there is one virtual device. The nx2: name keeps the other commands auto-stubbed and logged.
+// - 0(u32): u32 count, the device names in the out buffer (0x100 bytes each).
+// - 100(u32): the default device's name in the out buffer.
+// - 300(16-byte stream format, name buffer): whether the format is supported (result only).
+// - 401(u32, name buffer): the device's 0x228-byte property block in the out buffer. The sdk hands
+// the game the name, the channel count (0x108) and the u64 at 0x218; with the zeroed placeholder
+// reply it saw 0 channels and Switch Sports failed to set up audio (2153-1604).
+// - 500(16 bytes, name buffer): an event signalled when the device's state changes.
+// - 700(u32 stream id, u32 device functionality, name buffer): attach a sysmix stream to the
+// device, which starts it (nn::audio::devmgr::AttachStream, behind OutputStream Start).
+// - 800(u32 stream id): detach it again (Stop).
+class INx2AudioVirtualDeviceManager final : public ServiceFramework<INx2AudioVirtualDeviceManager> {
+public:
+ explicit INx2AudioVirtualDeviceManager(Core::System& system_)
+ : ServiceFramework{system_, "nx2:auddmg:u:manager"},
+ service_context{system_, "nx2:auddmg:u:manager"},
+ state_event{service_context.CreateEvent("nx2:auddmg:u:StateEvent")} {
+ // clang-format off
+ static const FunctionInfo functions[] = {
+ {0, &INx2AudioVirtualDeviceManager::ListVirtualOutputDevices, "ListVirtualOutputDevices"},
+ {100, &INx2AudioVirtualDeviceManager::GetDefaultVirtualOutputDevice, "GetDefaultVirtualOutputDevice"},
+ {300, &INx2AudioVirtualDeviceManager::IsStreamFormatSupported, "IsVirtualOutputDeviceStreamFormatSupported"},
+ {401, &INx2AudioVirtualDeviceManager::GetVirtualOutputDeviceProperties, "GetVirtualOutputDeviceProperties"},
+ {500, &INx2AudioVirtualDeviceManager::AcquireStateChangeEvent, "AcquireVirtualOutputDeviceStateEvent (guessed)"},
+ {700, &INx2AudioVirtualDeviceManager::AttachStream, "AttachStream"},
+ {800, &INx2AudioVirtualDeviceManager::DetachStream, "DetachStream"},
+ };
+ // clang-format on
+ RegisterHandlers(functions);
+ }
+
+ ~INx2AudioVirtualDeviceManager() override {
+ service_context.CloseEvent(state_event);
+ }
+
+private:
+ static constexpr std::size_t NameSize = 0x100;
+ static constexpr std::string_view DeviceName = "AudioVirtualOutput0";
+ static constexpr std::string_view PhysicalDeviceName = "AudioTvOutput";
+
+ static void CopyName(u8* out, std::string_view name) {
+ std::memcpy(out, name.data(), std::min(name.size(), NameSize - 1));
+ }
+
+ void ListVirtualOutputDevices(HLERequestContext& ctx) {
+ std::array<u8, NameSize> names{};
+ CopyName(names.data(), DeviceName);
+ const u32 count = ctx.GetWriteBufferSize() >= NameSize ? 1 : 0;
+ if (count) {
+ ctx.WriteBuffer(names);
+ }
+ LOG_DEBUG(Service_Audio, "called, {} device", count);
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(ResultSuccess);
+ rb.Push(count);
+ }
+
+ void GetDefaultVirtualOutputDevice(HLERequestContext& ctx) {
+ std::array<u8, NameSize> name{};
+ CopyName(name.data(), DeviceName);
+ ctx.WriteBuffer(name);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void IsStreamFormatSupported(HLERequestContext& ctx) {
+ LOG_DEBUG(Service_Audio, "called");
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ // Property block: name (0x000), u32 type (0x100, 3), u32 (0x104, 2), u32 channel count
+ // (0x108), u32 sample rate (0x10c), physical output name (0x110), its id (0x210), u64 latency
+ // in ns (0x218), u32 device id (0x220). The sdk's output streams pass the device id to
+ // sysmix CreateStream, which refuses 0 (Switch Sports failed with 2153-1604 on a zero there).
+ void GetVirtualOutputDeviceProperties(HLERequestContext& ctx) {
+ std::array<u8, 0x228> block{};
+ const auto name = ctx.ReadBuffer(0);
+ std::memcpy(block.data(), name.data(), std::min(name.size(), NameSize - 1));
+ const auto put32 = [&](std::size_t offset, u32 value) {
+ std::memcpy(block.data() + offset, &value, sizeof(value));
+ };
+ put32(0x100, 3);
+ put32(0x104, 2);
+ put32(0x108, 2); // stereo
+ put32(0x10c, 48000); // Hz
+ CopyName(block.data() + 0x110, PhysicalDeviceName);
+ const u64 latency_ns = 10'000'000;
+ std::memcpy(block.data() + 0x218, &latency_ns, sizeof(latency_ns));
+ put32(0x220, 1);
+ ctx.WriteBuffer(block);
+ LOG_DEBUG(Service_Audio, "called for {}", Common::StringFromBuffer(name));
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void AcquireStateChangeEvent(HLERequestContext& ctx) {
+ IPC::ResponseBuilder rb{ctx, 2, 1};
+ rb.Push(ResultSuccess);
+ rb.PushCopyObjects(ctx, state_event->GetReadableEvent());
+ }
+
+ void AttachStream(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto stream_id = rp.Pop<u32>();
+ const auto functionality = rp.Pop<u32>();
+ LOG_INFO(Service_Audio, "attach stream {} to {} (functionality {})", stream_id,
+ Common::StringFromBuffer(ctx.ReadBuffer(0)), functionality);
+ SetStreamEngaged(stream_id, true);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void DetachStream(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto stream_id = rp.Pop<u32>();
+ LOG_INFO(Service_Audio, "detach stream {}", stream_id);
+ SetStreamEngaged(stream_id, false);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void SetStreamEngaged(u32 stream_id, bool engaged) {
+ const auto list = Nx2SysmixStreamList();
+ std::scoped_lock lk{list->mutex};
+ if (const auto it = list->streams.find(stream_id); it != list->streams.end()) {
+ it->second->engaged = engaged;
+ it->second->owed_frames = 0;
+ if (it->second->state_event) {
+ it->second->state_event->Signal(system.Kernel());
+ }
+ }
+ }
+
+ KernelHelpers::ServiceContext service_context;
+ Kernel::KEvent* state_event;
+};
+
+// Switch 2 "auddmg:u": command 0 (16 bytes and the pid) returns the virtual device manager.
+class INx2AudioDeviceManagerService final : public ServiceFramework<INx2AudioDeviceManagerService> {
+public:
+ explicit INx2AudioDeviceManagerService(Core::System& system_)
+ : ServiceFramework{system_, "nx2:auddmg:u"} {
+ // clang-format off
+ static const FunctionInfo functions[] = {
+ {0, &INx2AudioDeviceManagerService::OpenVirtualDeviceManager, "OpenVirtualDeviceManager (guessed)"},
+ };
+ // clang-format on
+ RegisterHandlers(functions);
+ }
+
+private:
+ void OpenVirtualDeviceManager(HLERequestContext& ctx) {
+ LOG_DEBUG(Service_Audio, "called");
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.PushIpcInterface<INx2AudioVirtualDeviceManager>(ctx, system);
+ }
+};
+
+// The stream object IUserSession command 1 returns. Commands (ids, request -> reply):
+// - 0(u32 sample type, u32 channels, u32 sample rate, u32 frames, u32 prefill, u32 flags): open.
+// - 1: close. 2: u32 state. 3: f32 gain. 4(f32 gain, s64 ramp ns): set the gain.
+// - 5 / 6: acquire (copy handle) / release the state-change event.
+// - 7(u32 mode, s32 threshold) / 8: acquire (copy handle) / release the data event.
+// - 9(s64 ns): clock the stream. 10(s32 frames) -> u32: prime. 11(s32 frames) -> u32: purge.
+// - 12(name buffer): rename.
+class INx2SysmixStreamSession final : public ServiceFramework<INx2SysmixStreamSession> {
+public:
+ explicit INx2SysmixStreamSession(Core::System& system_, std::shared_ptr<Nx2SysmixStreams> streams_,
+ std::shared_ptr<Nx2SysmixStream> stream_)
+ : ServiceFramework{system_, "nx2:audsmx:u:stream"}, service_context{system_, "nx2:audsmx:u:stream"},
+ streams{std::move(streams_)}, stream{std::move(stream_)} {
+ // clang-format off
+ static const FunctionInfo functions[] = {
+ {0, &INx2SysmixStreamSession::OpenStream, "OpenStream"},
+ {1, &INx2SysmixStreamSession::CloseStream, "CloseStream"},
+ {2, &INx2SysmixStreamSession::GetStreamState, "GetStreamState"},
+ {3, &INx2SysmixStreamSession::GetStreamGain, "GetStreamGain"},
+ {4, &INx2SysmixStreamSession::SetStreamGain, "SetStreamGain"},
+ {5, &INx2SysmixStreamSession::AcquireStateChangeEvent, "AcquireStreamStateChangeEvent"},
+ {6, &INx2SysmixStreamSession::Succeed, "ReleaseStreamStateChangeEvent"},
+ {7, &INx2SysmixStreamSession::AcquireDataEvent, "AcquireStreamDataEvent"},
+ {8, &INx2SysmixStreamSession::Succeed, "ReleaseStreamDataEvent"},
+ {9, &INx2SysmixStreamSession::Succeed, "ClockStream"},
+ {10, &INx2SysmixStreamSession::EchoFrames, "PrimeStream"},
+ {11, &INx2SysmixStreamSession::EchoFrames, "PurgeStream"},
+ {12, &INx2SysmixStreamSession::Succeed, "RenameStream"},
+ };
+ // clang-format on
+ RegisterHandlers(functions);
+ std::scoped_lock lk{streams->mutex};
+ stream->state_event = service_context.CreateEvent("nx2:audsmx:u:StreamStateEvent");
+ stream->data_event = service_context.CreateEvent("nx2:audsmx:u:StreamDataEvent");
+ }
+
+ ~INx2SysmixStreamSession() override {
+ // The drain timer signals these events under the lock: unlist the stream first.
+ {
+ std::scoped_lock lk{streams->mutex};
+ streams->streams.erase(stream->id);
+ service_context.CloseEvent(stream->state_event);
+ service_context.CloseEvent(stream->data_event);
+ stream->state_event = nullptr;
+ stream->data_event = nullptr;
+ }
+ }
+
+private:
+ // States the session reports: 0 created, 1 open, 2 open and engaged.
+ void OpenStream(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto sample_type = rp.Pop<u32>();
+ const auto channels = rp.Pop<u32>();
+ const auto sample_rate = rp.Pop<u32>();
+ const auto frames = rp.Pop<u32>();
+ const auto prefill = rp.Pop<u32>();
+ const auto flags = rp.Pop<u32>();
+ LOG_INFO(Service_Audio,
+ "stream {} open: sample type {}, {} channels, {} Hz, {} frames, prefill {}, flags {:#x}",
+ stream->id, sample_type, channels, sample_rate, frames, prefill, flags);
+ {
+ std::scoped_lock lk{streams->mutex};
+ stream->sample_type = sample_type;
+ stream->channels = channels;
+ stream->sample_rate = sample_rate;
+ stream->frames = frames;
+ stream->owed_frames = 0;
+ stream->open = true;
+ stream->state_event->Signal(system.Kernel());
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void CloseStream(HLERequestContext& ctx) {
+ {
+ std::scoped_lock lk{streams->mutex};
+ stream->open = false;
+ stream->state_event->Signal(system.Kernel());
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void GetStreamState(HLERequestContext& ctx) {
+ u32 state{};
+ {
+ std::scoped_lock lk{streams->mutex};
+ state = stream->open ? (stream->engaged ? 2 : 1) : 0;
+ }
+ LOG_DEBUG(Service_Audio, "stream {} state {}", stream->id, state);
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(ResultSuccess);
+ rb.Push(state);
+ }
+
+ void GetStreamGain(HLERequestContext& ctx) {
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(ResultSuccess);
+ rb.Push(gain);
+ }
+
+ void SetStreamGain(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ gain = rp.Pop<f32>();
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void AcquireStateChangeEvent(HLERequestContext& ctx) {
+ IPC::ResponseBuilder rb{ctx, 2, 1};
+ rb.Push(ResultSuccess);
+ rb.PushCopyObjects(ctx, stream->state_event->GetReadableEvent());
+ }
+
+ void AcquireDataEvent(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto mode = rp.Pop<u32>();
+ const auto threshold = rp.Pop<s32>();
+ LOG_INFO(Service_Audio, "stream {} data event: mode {}, threshold {}", stream->id, mode, threshold);
+ {
+ // The game waits on this before its first write, so signal it at once when the ring
+ // already has room (it is empty right after OpenStream); the drain timer keeps it up.
+ std::scoped_lock lk{streams->mutex};
+ stream->data_mode = mode;
+ stream->data_threshold = threshold;
+ if (mode != 0 && stream->open && static_cast<s64>(stream->frames) >= threshold) {
+ stream->data_event->Signal(system.Kernel());
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 2, 1};
+ rb.Push(ResultSuccess);
+ rb.PushCopyObjects(ctx, stream->data_event->GetReadableEvent());
+ }
+
+ void EchoFrames(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto frames = rp.Pop<s32>();
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(ResultSuccess);
+ rb.Push<u32>(static_cast<u32>((std::max)(frames, 0)));
+ }
+
+ void Succeed(HLERequestContext& ctx) {
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ KernelHelpers::ServiceContext service_context;
+ std::shared_ptr<Nx2SysmixStreams> streams;
+ std::shared_ptr<Nx2SysmixStream> stream;
+ f32 gain{1.0f};
+};
+
+// The session audsmx:u command 0 returns. Commands:
+// - 1(u32, u32 creation flags, u64 pid, u64 buffer address, u64 buffer size, name buffer):
+// u32 stream id and the stream object.
+// - 2 / 3(buffer of u32 stream ids): engage / disengage those streams.
+class INx2SysmixUserSession final : public ServiceFramework<INx2SysmixUserSession> {
+public:
+ explicit INx2SysmixUserSession(Core::System& system_) : ServiceFramework{system_, "nx2:audsmx:u:session"} {
+ // clang-format off
+ static const FunctionInfo functions[] = {
+ {1, &INx2SysmixUserSession::CreateStream, "CreateStream"},
+ {2, &INx2SysmixUserSession::EngageStreams, "EngageStreams"},
+ {3, &INx2SysmixUserSession::DisengageStreams, "DisengageStreams"},
+ };
+ // clang-format on
+ RegisterHandlers(functions);
+ drain_timer = Core::Timing::CreateEvent(
+ "nx2:audsmx:u drain",
+ [streams = streams, &system = system, owner = session_id](s64, std::chrono::nanoseconds)
+ -> std::optional<std::chrono::nanoseconds> {
+ Drain(system, *streams, owner);
+ return std::nullopt;
+ });
+ system.CoreTiming().ScheduleLoopingEvent(DrainInterval, DrainInterval, drain_timer);
+ }
+
+ ~INx2SysmixUserSession() override {
+ system.CoreTiming().UnscheduleEvent(drain_timer);
+ }
+
+private:
+ static constexpr std::chrono::nanoseconds DrainInterval{std::chrono::milliseconds{5}};
+ static constexpr u64 ReadIndexOffset = 0x44;
+ static constexpr u64 WriteIndexOffset = 0x1004;
+
+ // Takes up to one interval's worth of frames out of each engaged ring (frames owed while a ring
+ // is empty are dropped, as an underrun plays silence), and signals the data event of every open
+ // stream with at least its threshold of free frames, engaged or not: the game fills the ring
+ // before it engages the stream.
+ static void Drain(Core::System& system, Nx2SysmixStreams& state, u32 owner) {
+ std::scoped_lock lk{state.mutex};
+ auto& memory = system.ApplicationMemory();
+ for (auto& [id, stream] : state.streams) {
+ if (stream->owner != owner || !stream->open || stream->frames == 0 || !stream->buffer) {
+ continue;
+ }
+ const u32 capacity = stream->frames + 1;
+ const u32 write = memory.Read32(stream->buffer + WriteIndexOffset);
+ const u32 read = memory.Read32(stream->buffer + ReadIndexOffset);
+ if (write >= capacity || read >= capacity) {
+ continue;
+ }
+ u32 filled = (write + capacity - read) % capacity;
+ if (stream->engaged) {
+ stream->owed_frames += stream->sample_rate *
+ std::chrono::duration<double>(DrainInterval).count();
+ const auto owed = static_cast<u32>(stream->owed_frames);
+ stream->owed_frames -= owed;
+ const u32 taken = (std::min)(filled, owed);
+ if (taken != 0) {
+ memory.Write32(stream->buffer + ReadIndexOffset, (read + taken) % capacity);
+ filled -= taken;
+ }
+ }
+ const u32 free_frames = capacity - 1 - filled;
+ if (stream->data_event && stream->data_mode != 0 &&
+ static_cast<s64>(free_frames) >= stream->data_threshold) {
+ stream->data_event->Signal(system.Kernel());
+ }
+ }
+ }
+
+ void CreateStream(HLERequestContext& ctx) {
+ IPC::RequestParser rp{ctx};
+ const auto kind = rp.Pop<u32>();
+ const auto flags = rp.Pop<u32>();
+ [[maybe_unused]] const auto pid = rp.Pop<u64>();
+ const auto buffer = rp.Pop<u64>();
+ const auto size = rp.Pop<u64>();
+ const auto name = Common::StringFromBuffer(ctx.ReadBuffer(0));
+ auto stream = std::make_shared<Nx2SysmixStream>();
+ stream->buffer = buffer;
+ stream->size = size;
+ {
+ std::scoped_lock lk{streams->mutex};
+ stream->id = streams->next_id++;
+ stream->owner = session_id;
+ streams->streams.emplace(stream->id, stream);
+ }
+ LOG_INFO(Service_Audio, "stream {} '{}': kind {}, flags {:#x}, buffer {:#x} size {:#x}",
+ stream->id, name, kind, flags, buffer, size);
+ IPC::ResponseBuilder rb{ctx, 3, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.Push(stream->id);
+ rb.PushIpcInterface<INx2SysmixStreamSession>(ctx, system, streams, stream);
+ }
+
+ void SetEngaged(HLERequestContext& ctx, bool engaged) {
+ const auto buffer = ctx.ReadBuffer(0);
+ std::vector<u32> ids(buffer.size() / sizeof(u32));
+ std::memcpy(ids.data(), buffer.data(), ids.size() * sizeof(u32));
+ {
+ std::scoped_lock lk{streams->mutex};
+ for (const u32 id : ids) {
+ if (const auto it = streams->streams.find(id); it != streams->streams.end()) {
+ it->second->engaged = engaged;
+ it->second->owed_frames = 0;
+ if (it->second->state_event) {
+ it->second->state_event->Signal(system.Kernel());
+ }
+ }
+ }
+ }
+ LOG_INFO(Service_Audio, "{} streams {}", engaged ? "engage" : "disengage", fmt::join(ids, ","));
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ void EngageStreams(HLERequestContext& ctx) {
+ SetEngaged(ctx, true);
+ }
+
+ void DisengageStreams(HLERequestContext& ctx) {
+ SetEngaged(ctx, false);
+ }
+
+ static inline std::atomic<u32> next_session_id{1};
+ const u32 session_id = next_session_id++;
+ std::shared_ptr<Nx2SysmixStreams> streams = Nx2SysmixStreamList();
+ std::shared_ptr<Core::Timing::EventType> drain_timer;
+};
+
+// Switch 2 "audsmx:u": command 0 (16 bytes, the pid and a copy handle) returns the user session.
+class INx2AudioSoundMixerService final : public ServiceFramework<INx2AudioSoundMixerService> {
+public:
+ explicit INx2AudioSoundMixerService(Core::System& system_)
+ : ServiceFramework{system_, "nx2:audsmx:u"} {
+ // clang-format off
+ static const FunctionInfo functions[] = {
+ {0, &INx2AudioSoundMixerService::OpenUserSession, "OpenUserSession"},
+ };
+ // clang-format on
+ RegisterHandlers(functions);
+ }
+
+private:
+ void OpenUserSession(HLERequestContext& ctx) {
+ LOG_DEBUG(Service_Audio, "called");
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.PushIpcInterface<INx2SysmixUserSession>(ctx, system);
+ }
+};
+
+} // namespace Service::Audio
diff --git a/src/core/hle/service/caps/caps_su.cpp b/src/core/hle/service/caps/caps_su.cpp
index 4ecfc840e4..b19e6b7a27 100644
--- a/src/core/hle/service/caps/caps_su.cpp
+++ b/src/core/hle/service/caps/caps_su.cpp
@@ -21,10 +21,10 @@ IScreenShotApplicationService::IScreenShotApplicationService(
// clang-format off
static const FunctionInfo functions[] = {
{32, C<&IScreenShotApplicationService::SetShimLibraryVersion>, "SetShimLibraryVersion"},
- {201, nullptr, "SaveScreenShot"},
+ {201, &IScreenShotApplicationService::StubReplyZeroed, "SaveScreenShot"},
{203, C<&IScreenShotApplicationService::SaveScreenShotEx0>, "SaveScreenShotEx0"},
{205, C<&IScreenShotApplicationService::SaveScreenShotEx1>, "SaveScreenShotEx1"},
- {210, nullptr, "SaveScreenShotEx2"},
+ {210, &IScreenShotApplicationService::StubReplyZeroed, "SaveScreenShotEx2"},
};
// clang-format on
diff --git a/src/core/hle/service/caps/caps_u.cpp b/src/core/hle/service/caps/caps_u.cpp
index 6472b2bac6..eb4a10193e 100644
--- a/src/core/hle/service/caps/caps_u.cpp
+++ b/src/core/hle/service/caps/caps_u.cpp
@@ -20,17 +20,21 @@ IAlbumApplicationService::IAlbumApplicationService(Core::System& system_,
static const FunctionInfo functions[] = {
{32, C<&IAlbumApplicationService::SetShimLibraryVersion>, "SetShimLibraryVersion"},
{102, C<&IAlbumApplicationService::GetAlbumFileList0AafeAruidDeprecated>, "GetAlbumFileList0AafeAruidDeprecated"},
- {103, nullptr, "DeleteAlbumFileByAruid"},
- {104, nullptr, "GetAlbumFileSizeByAruid"},
- {105, nullptr, "DeleteAlbumFileByAruidForDebug"},
- {110, nullptr, "LoadAlbumScreenShotImageByAruid"},
- {120, nullptr, "LoadAlbumScreenShotThumbnailImageByAruid"},
- {130, nullptr, "PrecheckToCreateContentsByAruid"},
- {140, nullptr, "GetAlbumFileList1AafeAruidDeprecated"},
- {141, nullptr, "GetAlbumFileList2AafeUidAruidDeprecated"},
+ {103, &IAlbumApplicationService::StubReplyZeroed, "DeleteAlbumFileByAruid"},
+ {104, &IAlbumApplicationService::StubReplyZeroed, "GetAlbumFileSizeByAruid"},
+ {105, &IAlbumApplicationService::StubReplyZeroed, "DeleteAlbumFileByAruidForDebug"},
+ {110, &IAlbumApplicationService::StubReplyZeroed, "LoadAlbumScreenShotImageByAruid"},
+ {120, &IAlbumApplicationService::StubReplyZeroed, "LoadAlbumScreenShotThumbnailImageByAruid"},
+ {130, &IAlbumApplicationService::StubReplyZeroed, "PrecheckToCreateContentsByAruid"},
+ {140, &IAlbumApplicationService::StubReplyZeroed, "GetAlbumFileList1AafeAruidDeprecated"},
+ {141, &IAlbumApplicationService::StubReplyZeroed, "GetAlbumFileList2AafeUidAruidDeprecated"},
{142, C<&IAlbumApplicationService::GetAlbumFileList3AaeAruid>, "GetAlbumFileList3AaeAruid"},
- {143, nullptr, "GetAlbumFileList4AaeUidAruid"},
- {144, nullptr, "GetAllAlbumFileList3AaeAruid"},
+ {143, &IAlbumApplicationService::StubReplyZeroed, "GetAlbumFileList4AaeUidAruid"},
+ {144, &IAlbumApplicationService::StubReplyZeroed, "GetAllAlbumFileList3AaeAruid"},
+ {145, &IAlbumApplicationService::StubReplyZeroed, "Unknown145"},
+ {146, &IAlbumApplicationService::StubReplyZeroed, "Unknown146"},
+ {147, &IAlbumApplicationService::StubReplyZeroed, "Unknown147"},
+ {148, &IAlbumApplicationService::StubReplyZeroed, "Unknown148"},
{60002, nullptr, "OpenAccessorSessionForApplication"},
};
// clang-format on
diff --git a/src/core/hle/service/hid/hid_server.cpp b/src/core/hle/service/hid/hid_server.cpp
index 9d5f5f91d2..eb6a29c158 100644
--- a/src/core/hle/service/hid/hid_server.cpp
+++ b/src/core/hle/service/hid/hid_server.cpp
@@ -74,12 +74,12 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{70, C<&IHidServer::SetSixAxisSensorFusionParameters>, "SetSixAxisSensorFusionParameters"},
{71, C<&IHidServer::GetSixAxisSensorFusionParameters>, "GetSixAxisSensorFusionParameters"},
{72, C<&IHidServer::ResetSixAxisSensorFusionParameters>, "ResetSixAxisSensorFusionParameters"},
- {73, nullptr, "SetAccelerometerParameters"},
- {74, nullptr, "GetAccelerometerParameters"},
- {75, nullptr, "ResetAccelerometerParameters"},
- {76, nullptr, "SetAccelerometerPlayMode"},
- {77, nullptr, "GetAccelerometerPlayMode"},
- {78, nullptr, "ResetAccelerometerPlayMode"},
+ {73, &IHidServer::StubReplyZeroed, "SetAccelerometerParameters"},
+ {74, &IHidServer::StubReplyZeroed, "GetAccelerometerParameters"},
+ {75, &IHidServer::StubReplyZeroed, "ResetAccelerometerParameters"},
+ {76, &IHidServer::StubReplyZeroed, "SetAccelerometerPlayMode"},
+ {77, &IHidServer::StubReplyZeroed, "GetAccelerometerPlayMode"},
+ {78, &IHidServer::StubReplyZeroed, "ResetAccelerometerPlayMode"},
{79, C<&IHidServer::SetGyroscopeZeroDriftMode>, "SetGyroscopeZeroDriftMode"},
{80, C<&IHidServer::GetGyroscopeZeroDriftMode>, "GetGyroscopeZeroDriftMode"},
{81, C<&IHidServer::ResetGyroscopeZeroDriftMode>, "ResetGyroscopeZeroDriftMode"},
@@ -87,7 +87,7 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{83, C<&IHidServer::IsFirmwareUpdateAvailableForSixAxisSensor>, "IsFirmwareUpdateAvailableForSixAxisSensor"},
{84, C<&IHidServer::EnableSixAxisSensorUnalteredPassthrough>, "EnableSixAxisSensorUnalteredPassthrough"},
{85, C<&IHidServer::IsSixAxisSensorUnalteredPassthroughEnabled>, "IsSixAxisSensorUnalteredPassthroughEnabled"},
- {86, nullptr, "StoreSixAxisSensorCalibrationParameter"},
+ {86, &IHidServer::StubReplyZeroed, "StoreSixAxisSensorCalibrationParameter"},
{87, C<&IHidServer::LoadSixAxisSensorCalibrationParameter>, "LoadSixAxisSensorCalibrationParameter"},
{88, C<&IHidServer::GetSixAxisSensorIcInformation>, "GetSixAxisSensorIcInformation"},
{89, C<&IHidServer::ResetIsSixAxisSensorDeviceNewlyAssigned>, "ResetIsSixAxisSensorDeviceNewlyAssigned"},
@@ -119,6 +119,13 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{134, C<&IHidServer::SetNpadAnalogStickUseCenterClamp>, "SetNpadAnalogStickUseCenterClamp"},
{135, C<&IHidServer::SetNpadCaptureButtonAssignment>, "SetNpadCaptureButtonAssignment"},
{136, C<&IHidServer::ClearNpadCaptureButtonAssignment>, "ClearNpadCaptureButtonAssignment"},
+ {137, &IHidServer::StubReplyZeroed, "Unknown137"},
+ {138, &IHidServer::StubReplyZeroed, "Unknown138"},
+ {139, &IHidServer::StubReplyZeroed, "Unknown139"},
+ {140, &IHidServer::StubReplyZeroed, "Unknown140"},
+ {150, &IHidServer::StubReplyZeroed, "Unknown150"},
+ {151, &IHidServer::StubReplyZeroed, "Unknown151"},
+ {160, &IHidServer::StubReplyZeroed, "Unknown160"},
{200, C<&IHidServer::GetVibrationDeviceInfo>, "GetVibrationDeviceInfo"},
{201, C<&IHidServer::SendVibrationValue>, "SendVibrationValue"},
{202, C<&IHidServer::GetActualVibrationValue>, "GetActualVibrationValue"},
@@ -132,7 +139,9 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{210, C<&IHidServer::EndPermitVibrationSession>, "EndPermitVibrationSession"},
{211, C<&IHidServer::IsVibrationDeviceMounted>, "IsVibrationDeviceMounted"},
{212, C<&IHidServer::SendVibrationValueInBool>, "SendVibrationValueInBool"},
+ {213, C<&IHidServer::Unknown213>, "Unknown213"},
{214, &IHidServer::SendNx2VibrationValues, "SendNx2VibrationValues (guessed)"},
+ {220, C<&IHidServer::Unknown220>, "Unknown220"},
{300, C<&IHidServer::ActivateConsoleSixAxisSensor>, "ActivateConsoleSixAxisSensor"},
{301, C<&IHidServer::StartConsoleSixAxisSensor>, "StartConsoleSixAxisSensor"},
{302, C<&IHidServer::StopConsoleSixAxisSensor>, "StopConsoleSixAxisSensor"},
@@ -141,19 +150,24 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{305, C<&IHidServer::StopSevenSixAxisSensor>, "StopSevenSixAxisSensor"},
{306, C<&IHidServer::InitializeSevenSixAxisSensor>, "InitializeSevenSixAxisSensor"},
{307, C<&IHidServer::FinalizeSevenSixAxisSensor>, "FinalizeSevenSixAxisSensor"},
- {308, nullptr, "SetSevenSixAxisSensorFusionStrength"},
- {309, nullptr, "GetSevenSixAxisSensorFusionStrength"},
+ {308, &IHidServer::StubReplyZeroed, "SetSevenSixAxisSensorFusionStrength"},
+ {309, &IHidServer::StubReplyZeroed, "GetSevenSixAxisSensorFusionStrength"},
{310, C<&IHidServer::ResetSevenSixAxisSensorTimestamp>, "ResetSevenSixAxisSensorTimestamp"},
- {320, nullptr, "EnableNpadImu"}, //21.0.0+
- {321, nullptr, "DisableNpadImu"}, //21.0.0+
+ {311, &IHidServer::StubReplyZeroed, "Unknown311"},
+ {312, &IHidServer::StubReplyZeroed, "Unknown312"},
+ {320, &IHidServer::StubReplyZeroed, "EnableNpadImu"}, //21.0.0+
+ {321, &IHidServer::StubReplyZeroed, "DisableNpadImu"}, //21.0.0+
+ {322, &IHidServer::StubReplyZeroed, "Unknown322"},
+ {323, &IHidServer::StubReplyZeroed, "Unknown323"},
+ {324, &IHidServer::StubReplyZeroed, "Unknown324"},
{400, C<&IHidServer::IsUsbFullKeyControllerEnabled>, "IsUsbFullKeyControllerEnabled"},
- {401, nullptr, "EnableUsbFullKeyController"},
- {402, nullptr, "IsUsbFullKeyControllerConnected"},
- {403, nullptr, "HasBattery"},
- {404, nullptr, "HasLeftRightBattery"},
- {405, nullptr, "GetNpadInterfaceType"},
- {406, nullptr, "GetNpadLeftRightInterfaceType"},
- {407, nullptr, "GetNpadOfHighestBatteryLevel"},
+ {401, &IHidServer::StubReplyZeroed, "EnableUsbFullKeyController"},
+ {402, &IHidServer::StubReplyZeroed, "IsUsbFullKeyControllerConnected"},
+ {403, &IHidServer::StubReplyZeroed, "HasBattery"},
+ {404, &IHidServer::StubReplyZeroed, "HasLeftRightBattery"},
+ {405, &IHidServer::StubReplyZeroed, "GetNpadInterfaceType"},
+ {406, &IHidServer::StubReplyZeroed, "GetNpadLeftRightInterfaceType"},
+ {407, &IHidServer::StubReplyZeroed, "GetNpadOfHighestBatteryLevel"},
{408, nullptr, "GetNpadOfHighestBatteryLevelForJoyRight"},
{500, C<&IHidServer::GetPalmaConnectionHandle>, "GetPalmaConnectionHandle"},
{501, C<&IHidServer::InitializePalma>, "InitializePalma"},
@@ -185,11 +199,13 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{527, C<&IHidServer::EnablePalmaBoostMode>, "EnablePalmaBoostMode"},
{528, C<&IHidServer::GetPalmaBluetoothAddress>, "GetPalmaBluetoothAddress"},
{529, C<&IHidServer::SetDisallowedPalmaConnection>, "SetDisallowedPalmaConnection"},
+ {550, &IHidServer::StubReplyZeroed, "Unknown550"},
{1000, C<&IHidServer::SetNpadCommunicationMode>, "SetNpadCommunicationMode"},
{1001, C<&IHidServer::GetNpadCommunicationMode>, "GetNpadCommunicationMode"},
{1002, C<&IHidServer::SetTouchScreenConfiguration>, "SetTouchScreenConfiguration"},
{1003, C<&IHidServer::IsFirmwareUpdateNeededForNotification>, "IsFirmwareUpdateNeededForNotification"},
{1004, C<&IHidServer::SetTouchScreenResolution>, "SetTouchScreenResolution"},
+ {1005, &IHidServer::StubReplyZeroed, "Unknown1005"},
{1270, nullptr, "DeleteButtonConfigStorageRight"},
{1271, nullptr, "IsUsingCustomButtonConfig"},
{1272, nullptr, "IsAnyCustomButtonConfigEnabled"},
@@ -222,23 +238,49 @@ IHidServer::IHidServer(Core::System& system_, std::shared_ptr<ResourceManager> r
{1325, nullptr, "IsTouchScreenAutoTuneRequiredForRepairProviderReplacement"},
{1420, nullptr, "GetAppletResourceProperty"},
{2000, nullptr, "ActivateDigitizer"},
- {3000, nullptr, "GetDebugPadGenericPadMap"},
- {3001, nullptr, "SetDebugPadGenericPadMap"},
- {3002, nullptr, "ResetDebugPadGenericPadMap"},
- {3003, nullptr, "GetDebugPadKeyboardMap"},
- {3004, nullptr, "SetDebugPadKeyboardMap"},
- {3005, nullptr, "ResetDebugPadKeyboardMap"},
- {3006, nullptr, "GetFullKeyGenericPadMap"},
- {3007, nullptr, "SetFullKeyGenericPadMap"},
- {3008, nullptr, "ResetFullKeyGenericPadMap"},
- {3009, nullptr, "GetFullKeyKeyboardMap"},
- {3010, nullptr, "SetFullKeyKeyboardMap"},
- {3011, nullptr, "ResetFullKeyKeyboardMap"},
- {3012, nullptr, "GetDebugPadGenericPadMap"}, //21.0.0+
- {3013, nullptr, "SetDebugPadGenericPadMap"}, //21.0.0+
- {3014, nullptr, "GetDebugPadKeyboardMap"}, //21.0.0+
- {3015, nullptr, "SetDebugPadKeyboardMap"}, //21.0.0+
+ {3000, &IHidServer::StubReplyZeroed, "GetDebugPadGenericPadMap"},
+ {3001, &IHidServer::StubReplyZeroed, "SetDebugPadGenericPadMap"},
+ {3002, &IHidServer::StubReplyZeroed, "ResetDebugPadGenericPadMap"},
+ {3003, &IHidServer::StubReplyZeroed, "GetDebugPadKeyboardMap"},
+ {3004, &IHidServer::StubReplyZeroed, "SetDebugPadKeyboardMap"},
+ {3005, &IHidServer::StubReplyZeroed, "ResetDebugPadKeyboardMap"},
+ {3006, &IHidServer::StubReplyZeroed, "GetFullKeyGenericPadMap"},
+ {3007, &IHidServer::StubReplyZeroed, "SetFullKeyGenericPadMap"},
+ {3008, &IHidServer::StubReplyZeroed, "ResetFullKeyGenericPadMap"},
+ {3009, &IHidServer::StubReplyZeroed, "GetFullKeyKeyboardMap"},
+ {3010, &IHidServer::StubReplyZeroed, "SetFullKeyKeyboardMap"},
+ {3011, &IHidServer::StubReplyZeroed, "ResetFullKeyKeyboardMap"},
+ {3012, &IHidServer::StubReplyZeroed, "GetDebugPadGenericPadMap"}, //21.0.0+
+ {3013, &IHidServer::StubReplyZeroed, "SetDebugPadGenericPadMap"}, //21.0.0+
+ {3014, &IHidServer::StubReplyZeroed, "GetDebugPadKeyboardMap"}, //21.0.0+
+ {3015, &IHidServer::StubReplyZeroed, "SetDebugPadKeyboardMap"}, //21.0.0+
+ {3110, &IHidServer::StubReplyZeroed, "Unknown3110"},
+ {3111, &IHidServer::StubReplyZeroed, "Unknown3111"},
+ {3112, &IHidServer::StubReplyZeroed, "Unknown3112"},
+ {3113, &IHidServer::StubReplyZeroed, "Unknown3113"},
+ {3114, &IHidServer::StubReplyZeroed, "Unknown3114"},
+ {3130, &IHidServer::StubReplyZeroed, "Unknown3130"},
+ {3131, &IHidServer::StubReplyZeroed, "Unknown3131"},
+ {3132, &IHidServer::StubReplyZeroed, "Unknown3132"},
+ {3133, &IHidServer::StubReplyZeroed, "Unknown3133"},
+ {3134, &IHidServer::StubReplyZeroed, "Unknown3134"},
+ {3135, &IHidServer::StubReplyZeroed, "Unknown3135"},
+ {3136, &IHidServer::StubReplyZeroed, "Unknown3136"},
+ {3137, &IHidServer::StubReplyZeroed, "Unknown3137"},
+ {3138, &IHidServer::StubReplyZeroed, "Unknown3138"},
+ {3139, &IHidServer::StubReplyZeroed, "Unknown3139"},
+ {3140, &IHidServer::StubReplyZeroed, "Unknown3140"},
+ {3141, &IHidServer::StubReplyZeroed, "Unknown3141"},
+ {3142, &IHidServer::StubReplyZeroed, "Unknown3142"},
+ {3143, &IHidServer::StubReplyZeroed, "Unknown3143"},
+ {3144, &IHidServer::StubReplyZeroed, "Unknown3144"},
+ {3145, &IHidServer::StubReplyZeroed, "Unknown3145"},
+ {3146, &IHidServer::StubReplyZeroed, "Unknown3146"},
+ {3147, &IHidServer::StubReplyZeroed, "Unknown3147"},
+ {3148, &IHidServer::StubReplyZeroed, "Unknown3148"},
+ {3149, &IHidServer::StubReplyZeroed, "Unknown3149"},
{3150, C<&IHidServer::SetMouseLibraryVersion>, "SetMouseLibraryVersion"}, //21.0.0+
+ {5000, &IHidServer::StubReplyZeroed, "Unknown5000"},
// What? -- {12010, nullptr, "SetButtonConfigLeft"},
};
// clang-format on
@@ -1106,6 +1148,17 @@ void IHidServer::SendNx2VibrationValues(HLERequestContext& ctx) {
rb.Push(ResultSuccess);
}
+Result IHidServer::Unknown213() {
+ LOG_DEBUG(Service_HID, "(STUBBED) called");
+ R_SUCCEED();
+}
+
+Result IHidServer::Unknown220(Out<u8> out) {
+ LOG_DEBUG(Service_HID, "(STUBBED) called");
+ *out = 0;
+ R_SUCCEED();
+}
+
Result IHidServer::SendVibrationValueInBool(
bool is_vibrating, Core::HID::VibrationDeviceHandle vibration_device_handle,
ClientAppletResourceUserId aruid) {
diff --git a/src/core/hle/service/hid/hid_server.h b/src/core/hle/service/hid/hid_server.h
index beeb5ba17f..5738e1798f 100644
--- a/src/core/hle/service/hid/hid_server.h
+++ b/src/core/hle/service/hid/hid_server.h
@@ -192,6 +192,12 @@ private:
// Switch 2 sends vibration every frame with 214 (pointer buffers of device handles and
// values, like 206); Eden doesn't emulate vibration, so accept and drop it.
void SendNx2VibrationValues(HLERequestContext& ctx);
+ // 213 (32 bytes, aruid): sent per controller every frame by Kirby Air Riders (tens of
+ // thousands of auto stub log lines a run); no reply data. Accepted and dropped like 214.
+ Result Unknown213();
+ // 220 (16 bytes, aruid): replies one byte (meaning unknown); answer 0 instead of leaving it to
+ // the message buffer's leftovers.
+ Result Unknown220(Out<u8> out);
Result ActivateConsoleSixAxisSensor(ClientAppletResourceUserId aruid);
Result StartConsoleSixAxisSensor(Core::HID::ConsoleSixAxisSensorHandle console_sixaxis_handle,
ClientAppletResourceUserId aruid);
diff --git a/src/core/hle/service/kernel_helpers.cpp b/src/core/hle/service/kernel_helpers.cpp
index e4d6925f19..db55c6f81a 100644
--- a/src/core/hle/service/kernel_helpers.cpp
+++ b/src/core/hle/service/kernel_helpers.cpp
@@ -48,6 +48,17 @@ ServiceContext::~ServiceContext() {
}
}
+namespace {
+std::mutex debug_names_mutex;
+std::unordered_map<const Kernel::KReadableEvent*, std::string> debug_names;
+} // Anonymous namespace
+
+std::string DebugEventName(const Kernel::KReadableEvent* event) {
+ std::scoped_lock lk{debug_names_mutex};
+ const auto it = debug_names.find(event);
+ return it != debug_names.end() ? it->second : std::string{"?"};
+}
+
Kernel::KEvent* ServiceContext::CreateEvent(std::string&& name) {
// Reserve a new event from the process resource limit
Kernel::KScopedResourceReservation event_reservation(kernel, process,
@@ -74,6 +85,10 @@ Kernel::KEvent* ServiceContext::CreateEvent(std::string&& name) {
Kernel::KEvent::Register(kernel, event);
{
+ std::scoped_lock lk{debug_names_mutex};
+ debug_names[&event->GetReadableEvent()] = name;
+ }
+ {
std::scoped_lock lk{event_names_mutex};
event_names[event] = std::move(name);
}
@@ -86,6 +101,10 @@ void ServiceContext::CloseEvent(Kernel::KEvent* event) {
std::scoped_lock lk{event_names_mutex};
event_names.erase(event);
}
+ {
+ std::scoped_lock lk{debug_names_mutex};
+ debug_names.erase(&event->GetReadableEvent());
+ }
event->GetReadableEvent().Close(kernel);
event->Close(kernel);
}
diff --git a/src/core/hle/service/kernel_helpers.h b/src/core/hle/service/kernel_helpers.h
index a62dd838aa..b03e49300f 100644
--- a/src/core/hle/service/kernel_helpers.h
+++ b/src/core/hle/service/kernel_helpers.h
@@ -16,6 +16,7 @@ namespace Kernel {
class KernelCore;
class KEvent;
class KProcess;
+class KReadableEvent;
} // namespace Kernel
namespace Service::KernelHelpers {
@@ -37,4 +38,7 @@ public:
bool process_created{false};
};
+/// The name a service gave an event (EDEN_NX2_SVC_TRACE shows the handles a thread waits on).
+std::string DebugEventName(const Kernel::KReadableEvent* event);
+
} // namespace Service::KernelHelpers
diff --git a/src/core/hle/service/lm/lm.cpp b/src/core/hle/service/lm/lm.cpp
index d631a6dd80..a7d6d6e707 100644
--- a/src/core/hle/service/lm/lm.cpp
+++ b/src/core/hle/service/lm/lm.cpp
@@ -91,8 +91,9 @@ public:
static const FunctionInfo functions[] = {
{0, &ILogger::Log, "Log"},
{1, &ILogger::SetDestination, "SetDestination"},
- {2, nullptr, "TransmitHashedLog"}, //20.0.0+
- {3, nullptr, "DevNotify"}, //20.0.0+
+ {2, &ILogger::StubReplyZeroed, "TransmitHashedLog"}, //20.0.0+
+ {3, &ILogger::StubReplyZeroed, "DevNotify"}, //20.0.0+
+ {11, &ILogger::StubReplyInterface, "Unknown11"},
};
RegisterHandlers(functions);
}
diff --git a/src/core/hle/service/mii/mii.cpp b/src/core/hle/service/mii/mii.cpp
index 513bb671f8..ffdb17fd14 100644
--- a/src/core/hle/service/mii/mii.cpp
+++ b/src/core/hle/service/mii/mii.cpp
@@ -48,8 +48,8 @@ public:
{15, D<&IDatabaseService::DestroyFile>, "DestroyFile"},
{16, D<&IDatabaseService::DeleteFile>, "DeleteFile"},
{17, D<&IDatabaseService::Format>, "Format"},
- {18, nullptr, "Import"},
- {19, nullptr, "Export"},
+ {18, &IDatabaseService::StubReplyZeroed, "Import"},
+ {19, &IDatabaseService::StubReplyZeroed, "Export"},
{20, D<&IDatabaseService::IsBrokenDatabaseWithClearFlag>, "IsBrokenDatabaseWithClearFlag"},
{21, D<&IDatabaseService::GetIndex>, "GetIndex"},
{22, D<&IDatabaseService::SetInterfaceVersion>, "SetInterfaceVersion"},
@@ -57,6 +57,8 @@ public:
{24, D<&IDatabaseService::ConvertCoreDataToCharInfo>, "ConvertCoreDataToCharInfo"},
{25, D<&IDatabaseService::ConvertCharInfoToCoreData>, "ConvertCharInfoToCoreData"},
{26, D<&IDatabaseService::Append>, "Append"},
+ {27, &IDatabaseService::StubReplyZeroed, "Unknown27"},
+ {28, &IDatabaseService::StubReplyZeroed, "Unknown28"},
};
// clang-format on
diff --git a/src/core/hle/service/ns/platform_service_manager.cpp b/src/core/hle/service/ns/platform_service_manager.cpp
index 88aac56b1c..7bd8980a08 100644
--- a/src/core/hle/service/ns/platform_service_manager.cpp
+++ b/src/core/hle/service/ns/platform_service_manager.cpp
@@ -85,8 +85,12 @@ struct IPlatformServiceManager::Impl {
// Automatically populated based on shared_fonts dump or system archives.
// 6 builtin fonts + extra 2 for whatever may come after
boost::container::static_vector<FontRegion, 8> shared_font_regions;
- /// Plain TTF copies of the same fonts (Switch 2 GetBuiltInFontData), after the BFTTF data.
+ /// Where Switch 2 GetBuiltInFontData finds the same fonts as plain TTF data: page-aligned
+ /// copies in builtin_font_memory (the Switch 1 font memory is full with the system fonts).
boost::container::static_vector<FontRegion, 8> builtin_font_regions;
+ u32 builtin_font_memory_size{};
+ /// Made by the first GetBuiltInFontSharedMemory (Switch 2 titles only).
+ Kernel::KSharedMemory* builtin_font_memory{};
/// Backing memory for the shared font data
std::array<u8, SHARED_FONT_MEM_SIZE> shared_font;
};
@@ -122,7 +126,6 @@ IPlatformServiceManager::IPlatformServiceManager(Core::System& system_, const ch
// Attempt to load shared font data from disk
const auto* nand = fsc.GetSystemNANDContents();
std::size_t offset = 0;
- std::vector<std::vector<u8>> plain_fonts;
// Rebuild shared fonts from data ncas or synthesize
for (auto& font : SHARED_FONTS) {
FileSys::VirtualFile romfs;
@@ -158,34 +161,52 @@ IPlatformServiceManager::IPlatformServiceManager(Core::System& system_, const ch
Common::swap32);
// Font offset and size do not account for the header
const FontRegion region{u32(offset + 8), u32((font_data_u32.size() * sizeof(u32)) - 8)};
- std::vector<u8> plain(region.size);
- DecryptSharedFontToTTF(font_data_u32, plain);
- plain_fonts.push_back(std::move(plain));
DecryptSharedFont(font_data_u32, impl->shared_font, offset);
impl->shared_font_regions.push_back(region);
}
- for (const auto& plain : plain_fonts) {
- offset = Common::AlignUp(offset, 0x1000);
- if (offset + plain.size() > SHARED_FONT_MEM_SIZE) {
- LOG_WARNING(Service_NS, "No shared memory left for plain built-in font copies");
- break;
- }
- std::memcpy(impl->shared_font.data() + offset, plain.data(), plain.size());
- impl->builtin_font_regions.push_back({u32(offset), u32(plain.size())});
- offset += plain.size();
+ // The data after each BFTTF header is the plain font: GetBuiltInFontSharedMemory copies it.
+ u32 builtin_offset = 0;
+ for (const auto& region : impl->shared_font_regions) {
+ impl->builtin_font_regions.push_back({builtin_offset, region.size});
+ builtin_offset = Common::AlignUp(builtin_offset + region.size, 0x1000);
}
+ impl->builtin_font_memory_size = std::max<u32>(builtin_offset, 0x1000);
}
// Switch 2: the shared memory GetBuiltInFontData offsets point into (copy handle, u32 size); the
-// SDK maps it read-only. Eden serves its font shared memory, which holds plain copies too.
+// SDK maps it read-only. It holds plain copies of the fonts, apart from the Switch 1 font memory.
void IPlatformServiceManager::GetBuiltInFontSharedMemory(HLERequestContext& ctx) {
- LOG_WARNING(Service_NS, "(STUBBED) Switch 2 GetBuiltInFontSharedMemory");
- std::memcpy(kernel.GetFontSharedMem().GetPointer(), impl->shared_font.data(), impl->shared_font.size());
+ LOG_WARNING(Service_NS, "(STUBBED) Switch 2 GetBuiltInFontSharedMemory, {:#x} bytes",
+ impl->builtin_font_memory_size);
+ if (impl->builtin_font_memory == nullptr) {
+ auto* const memory = Kernel::KSharedMemory::Create(kernel);
+ if (const Result result = memory->Initialize(kernel, system.DeviceMemory(), nullptr,
+ Kernel::Svc::MemoryPermission::None,
+ Kernel::Svc::MemoryPermission::Read,
+ impl->builtin_font_memory_size);
+ result.IsError()) {
+ LOG_ERROR(Service_NS, "Failed to make {:#x} bytes of built-in font memory: {:#x}",
+ impl->builtin_font_memory_size, result.raw);
+ memory->Close(kernel);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(result);
+ return;
+ }
+ Kernel::KSharedMemory::Register(kernel, memory);
+ u8* const data = memory->GetPointer();
+ std::memset(data, 0, impl->builtin_font_memory_size);
+ for (size_t i = 0; i < impl->builtin_font_regions.size(); ++i) {
+ std::memcpy(data + impl->builtin_font_regions[i].offset,
+ impl->shared_font.data() + impl->shared_font_regions[i].offset,
+ impl->builtin_font_regions[i].size);
+ }
+ impl->builtin_font_memory = memory;
+ }
IPC::ResponseBuilder rb{ctx, 3, 1};
rb.Push(ResultSuccess);
- rb.Push<u32>(static_cast<u32>(SHARED_FONT_MEM_SIZE));
- rb.PushCopyObjects(ctx, kernel.GetFontSharedMem());
+ rb.Push<u32>(impl->builtin_font_memory_size);
+ rb.PushCopyObjects(ctx, *impl->builtin_font_memory);
}
// nn::pl::GetBuiltInFontData(BuiltInFontDataOunce*, BuiltInFontTypeOunce, bool): returns a 16-byte
@@ -212,7 +233,11 @@ void IPlatformServiceManager::GetBuiltInFontData(HLERequestContext& ctx) {
rb.Push<u32>(0);
}
-IPlatformServiceManager::~IPlatformServiceManager() = default;
+IPlatformServiceManager::~IPlatformServiceManager() {
+ if (impl->builtin_font_memory != nullptr) {
+ impl->builtin_font_memory->Close(kernel);
+ }
+}
Result IPlatformServiceManager::RequestLoad(SharedFontType type) {
// Games don't call this so all fonts should be loaded
diff --git a/src/core/hle/service/nvdrv/core/container.cpp b/src/core/hle/service/nvdrv/core/container.cpp
index 9edce03f64..22db243e3f 100644
--- a/src/core/hle/service/nvdrv/core/container.cpp
+++ b/src/core/hle/service/nvdrv/core/container.cpp
@@ -11,6 +11,7 @@
#include "core/hle/service/nvdrv/core/heap_mapper.h"
#include "core/hle/service/nvdrv/core/nvmap.h"
#include "core/hle/service/nvdrv/core/syncpoint_manager.h"
+#include "core/hle/service/nvdrv/devices/nx2_stub_device.h"
#include "core/memory.h"
#include "video_core/host1x/host1x.h"
@@ -130,6 +131,7 @@ void Container::CloseSession(SessionId session_id) {
session.has_preallocated_area = false;
}
session.is_active = false;
+ Devices::Nx2GpuMemoryRegistry::Get().ForgetDeviceRanges(session.asid.id);
smmu.UnregisterProcess(impl->sessions[session_id.id].asid);
impl->id_pool.emplace_front(session_id.id);
}
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
index c4119b2ab1..c5b23bef3e 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.cpp
@@ -5,6 +5,7 @@
// SPDX-FileCopyrightText: 2021 Skyline Team and Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
+#include <cstdlib>
#include <cstring>
#include <utility>
@@ -35,6 +36,13 @@ nvhost_as_gpu::nvhost_as_gpu(Core::System& system_, Module& module_, NvCore::Con
nvhost_as_gpu::~nvhost_as_gpu() {
Nx2GpuMemoryRegistry::Get().RemoveListener(this);
+ // Leave shared ranges to the process's other address spaces; like this address space's own
+ // ranges, one nobody else maps stays allocated.
+ for (const auto& [key, range] : nx2_device_ranges) {
+ if (range.shared) {
+ Nx2GpuMemoryRegistry::Get().ReleaseDeviceRange(range.asid, key.first, key.second);
+ }
+ }
}
NvResult nvhost_as_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> input,
@@ -69,6 +77,9 @@ NvResult nvhost_as_gpu::Ioctl1(DeviceFD fd, Ioctl command, std::span<const u8> i
},
[this](u64 gpu_address, u64 cpu_address, u64 size) {
Nx2BackSparse(gpu_address, cpu_address, size);
+ },
+ [this](u64 gpu_address, u64 cpu_address, u64 size) {
+ Nx2SparseTile(gpu_address, cpu_address, size);
});
}
return result;
@@ -131,27 +142,43 @@ void nvhost_as_gpu::Nx2Map(u64 gpu_address, u64 cpu_address, u64 size) {
}
// The same CPU range is often mapped twice (at its own address and in a sparse region), so
// reuse its SMMU mapping.
- DAddr device_address{};
- bool heap = false;
+ Nx2DeviceRange range{};
if (const auto it = nx2_device_ranges.find({cpu_address, size}); it != nx2_device_ranges.end()) {
- device_address = it->second.address;
- heap = it->second.heap;
+ range = it->second;
} else if (session->has_preallocated_area && session->mapper->IsInBounds(cpu_address, size)) {
- device_address = session->mapper->Map(cpu_address, size);
- heap = true;
+ range = {session->mapper->Map(cpu_address, size), true};
} else {
- auto& smmu = system.Host1x().MemoryManager();
- device_address = smmu.Allocate(Common::AlignUp(size, 0x20000));
- if (device_address == 0) {
+ const auto allocate = [&]() -> u64 {
+ auto& smmu = system.Host1x().MemoryManager();
+ const DAddr address = smmu.Allocate(Common::AlignUp(size, 0x20000));
+ if (address != 0) {
+ smmu.Map(address, cpu_address, size, session->asid, true);
+ }
+ return address;
+ };
+ // EDEN_NX2_PRIVATE_DEVICE_RANGES=1 (A/B aid): a range per address space.
+ static const bool private_ranges = std::getenv("EDEN_NX2_PRIVATE_DEVICE_RANGES") != nullptr;
+ if (private_ranges) {
+ range = {allocate(), false};
+ } else {
+ range = {Nx2GpuMemoryRegistry::Get().AcquireDeviceRange(session->asid.id, cpu_address,
+ size, allocate),
+ false, true, session->asid.id};
+ }
+ if (range.address == 0) {
LOG_CRITICAL(Service_NVDRV, "Switch 2 GPU memory {:#x}+{:#x}: out of SMMU space",
cpu_address, size);
return;
}
- smmu.Map(device_address, cpu_address, size, session->asid, true);
}
- nx2_device_ranges[{cpu_address, size}] = {device_address, heap};
+ const DAddr device_address = range.address;
+ nx2_device_ranges[{cpu_address, size}] = range;
Nx2GpuMemoryRegistry::Get().RecordDeviceRange(cpu_address, size, device_address);
- gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false);
+ // On the GPU thread, like unmaps: a GMMU map takes the texture cache lock, and the callers hold
+ // nx2:gpu locks that the GPU thread takes under that lock (sparse blocks backed on write).
+ system.GPU().MapInOrder([gmmu = gmmu.get(), gpu_address, device_address, size] {
+ gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false);
+ });
LOG_DEBUG(Service_NVDRV, "Switch 2 GPU mapping {:#x}+{:#x} -> CPU {:#x}", gpu_address, size,
cpu_address);
}
@@ -174,6 +201,10 @@ void nvhost_as_gpu::Nx2Release(u64 cpu_address, u64 size) {
}
const Nx2DeviceRange range = it->second;
nx2_device_ranges.erase(it);
+ if (range.shared &&
+ !Nx2GpuMemoryRegistry::Get().ReleaseDeviceRange(range.asid, cpu_address, size)) {
+ return; // another address space of the process still maps it
+ }
auto* session = container.GetSession(nx2_session);
auto* const mapper = range.heap && session != nullptr ? session->mapper.get() : nullptr;
auto& smmu = system.Host1x().MemoryManager();
@@ -221,7 +252,10 @@ void nvhost_as_gpu::Nx2Sparse(u64 gpu_address, u64 size, bool create) {
return;
}
nx2_sparse_ranges[gpu_address] = {size, device_address};
- gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false);
+ // On the GPU thread, as in Nx2Map.
+ system.GPU().MapInOrder([gmmu = gmmu.get(), gpu_address, device_address, size] {
+ gmmu->Map(gpu_address, device_address, size, Tegra::PTEKind::PITCH, false);
+ });
LOG_DEBUG(Service_NVDRV, "Switch 2 sparse region {:#x}+{:#x} -> device {:#x}", gpu_address,
size, device_address);
}
@@ -236,13 +270,39 @@ void nvhost_as_gpu::Nx2BackSparse(u64 gpu_address, u64 cpu_address, u64 size) {
--it;
const auto& [start, range] = *it;
const auto& [region_size, device_address] = range;
- if (gpu_address + size > start + region_size) {
+ if (size > region_size || gpu_address - start > region_size - size) {
return;
}
system.Host1x().MemoryManager().Map(device_address + (gpu_address - start), cpu_address, size,
session->asid, true);
}
+void nvhost_as_gpu::Nx2SparseTile(u64 gpu_address, u64 cpu_address, u64 size) {
+ std::scoped_lock lock(mutex);
+ auto it = nx2_sparse_ranges.upper_bound(gpu_address);
+ auto* session = container.GetSession(nx2_session);
+ if (it == nx2_sparse_ranges.begin() || session == nullptr) {
+ return;
+ }
+ --it;
+ const auto& [start, range] = *it;
+ const auto& [region_size, device_address] = range;
+ if (size > region_size || gpu_address - start > region_size - size) {
+ return;
+ }
+ // In GPU-thread order: command lists submitted before the request still see the old tile.
+ // Unmapping first keeps the SMMU's reverse map right and invalidates the caches over the tile,
+ // whose contents are now what the game's memory holds there.
+ const DAddr device = device_address + (gpu_address - start);
+ auto& smmu = system.Host1x().MemoryManager();
+ system.GPU().MapInOrder([&smmu, device, cpu_address, size, asid = session->asid] {
+ smmu.Unmap(device, size);
+ if (cpu_address != 0) {
+ smmu.Map(device, cpu_address, size, asid, true);
+ }
+ });
+}
+
void nvhost_as_gpu::OnClose(DeviceFD fd) {}
// Switch 2 NvRm (with characteristics flag 26) asks for the read-only syncpoint shim: a GPU page
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
index 69050bb677..37b6d0d4c4 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
+++ b/src/core/hle/service/nvdrv/devices/nvhost_as_gpu.h
@@ -238,6 +238,8 @@ private:
struct Nx2DeviceRange {
DAddr address;
bool heap; ///< Mapped through the session's heap mapper (else an SMMU allocation)
+ bool shared{}; ///< A reference to the process's range in Nx2GpuMemoryRegistry
+ size_t asid{};
};
std::map<std::pair<u64, u64>, Nx2DeviceRange> nx2_device_ranges;
void Nx2Release(u64 cpu_address, u64 size);
@@ -246,6 +248,8 @@ private:
// blocks get memory behind their device pages as the GPU writes them.
void Nx2Sparse(u64 gpu_address, u64 size, bool create);
void Nx2BackSparse(u64 gpu_address, u64 cpu_address, u64 size);
+ // A tile of a sparse region (nx2:gpu 216): its device pages show process memory, or nothing.
+ void Nx2SparseTile(u64 gpu_address, u64 cpu_address, u64 size);
std::map<u64, std::pair<u64, DAddr>> nx2_sparse_ranges; ///< GPU address -> (size, device)
};
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
index 5db984b53d..93d51cd538 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_ctrl_gpu.cpp
@@ -6,6 +6,7 @@
#include <array>
#include <algorithm>
+#include <cstdlib>
#include <cstring>
#include "common/assert.h"
#include "common/logging.h"
@@ -112,12 +113,22 @@ NvResult nvhost_ctrl_gpu::Ioctl3(DeviceFD fd, Ioctl command, std::span<const u8>
case 0x6:
return WrapFixedInlOut(this, &nvhost_ctrl_gpu::GetTPCMasks3, input, output,
inline_output);
- case 0x13:
- LOG_DEBUG(Service_NVDRV, "(STUBBED) called.");
-
- // TODO (jarrodnorwell)
-
- return NvResult::NotImplemented;
+ case 0x13: {
+ // NVGPU_GPU_IOCTL_VSMS_MAPPING: an 8-byte argument struct plus a buffer of
+ // {gpc_index, tpc_index} byte pairs, one per virtual SM. The SDK treats a pair left at
+ // its 0xc0c0 sentinel as failure, so fill every pair: one GPC, two SMs per TPC.
+ if (command.raw != 0xc0084713 || input.size() != 8 || output.size() != 8 ||
+ inline_output.size() < 2) {
+ return NvResult::BadParameter;
+ }
+ std::memcpy(output.data(), input.data(), output.size());
+ for (size_t i = 0; i + 1 < inline_output.size(); i += 2) {
+ inline_output[i] = 0; // gpc_index
+ inline_output[i + 1] = static_cast<u8>(i / 4); // tpc_index
+ }
+ LOG_DEBUG(Service_NVDRV, "VSMS mapping for {} SMs", inline_output.size() / 2);
+ return NvResult::Success;
+ }
default:
break;
}
@@ -207,6 +218,21 @@ void ApplyNx2Characteristics(GC& gc) {
// Without the flag NvRm fails that call and the texture stays at GPU address 0 (title 1's
// movie and the passes rendered into compressible pools never showed).
gc.flags |= 1ULL << 34;
+ // NVGPU_GPU_FLAGS_SUPPORT_REMAP: without it NvRm fails every remap (nvnMemoryPoolMapVirtual)
+ // before asking the server, so title 1 never maps or unmaps its sparse texture tiles (nx2:gpu
+ // 216) and streams its terrain into unmapped sparse memory. EDEN_NX2_SPARSE_REMAP=0 leaves it out.
+ static const bool remap = [] {
+ const char* value = std::getenv("EDEN_NX2_SPARSE_REMAP");
+ return value == nullptr || value[0] != '0';
+ }();
+ if (remap) {
+ gc.flags |= 1ULL << 35;
+ }
+ // Engine support bits 40-44 (2D, 3D, compute, inline-to-memory, ZBC). The SDK filters its class
+ // enumeration by them, and NvRm's newer device path lists a class family in the device info
+ // only when its bit is set; without them Fire Emblem: Fortune's Weave aborts at boot and a
+ // device opened that way has no compute class (Fire Emblem's CUDA driver reports no device).
+ gc.flags |= 0x1FULL << 40;
// The Switch 2 layout keeps nine s16 interface versions at 0x70..0x80 (like nvgpu's
// *_ioctl_nr_last); its NvRm requires e.g. [0x78] >= 6 and enables features above 0xa, 0x1f
diff --git a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
index 0cd72faed3..79586446d5 100644
--- a/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
+++ b/src/core/hle/service/nvdrv/devices/nvhost_gpu.cpp
@@ -24,6 +24,7 @@
#include "video_core/engines/puller.h"
#include "video_core/gpu.h"
#include "video_core/host1x/host1x.h"
+#include "video_core/memory_manager.h"
namespace Service::Nvidia::Devices {
namespace {
@@ -435,6 +436,33 @@ NvResult nvhost_gpu::SubmitGPFIFOImpl(IoctlSubmitGpfifo& params, Tegra::CommandL
entries.submit_hashes.push_back(Tegra::HashPushbufferWords(words));
}
}
+ // The present returns before the GPU thread reaches this frame's work (nx2_gsv.h,
+ // frames ahead), and Mario Kart World then refills command memory it already
+ // submitted: the end of a frame ran with the next frame's present image as its render
+ // target, leaving an image three frames old to be shown. Keep the words as submitted.
+ // EDEN_NX2_PB_SNAPSHOT=0: execute from memory.
+ static const bool snapshot = [] {
+ const char* value = std::getenv("EDEN_NX2_PB_SNAPSHOT");
+ return value == nullptr || value[0] != '0';
+ }();
+ if (snapshot && channel_state->memory_manager && channel_state->payload) {
+ // Before the copy: a suspension during it must count as one after it.
+ entries.snapshot_suspensions =
+ channel_state->payload->dma_pusher.suspensions.load(std::memory_order_acquire);
+ size_t words = 0;
+ for (const auto& entry : entries.command_lists) {
+ words += entry.size;
+ }
+ entries.submitted_words.resize(words);
+ entries.submitted_offsets.reserve(entries.command_lists.size());
+ size_t offset = 0;
+ for (const auto& entry : entries.command_lists) {
+ entries.submitted_offsets.push_back(static_cast<u32>(offset));
+ channel_state->memory_manager->ReadBlockUnsafe(
+ entry.addr, entries.submitted_words.data() + offset, entry.size * sizeof(u32));
+ offset += entry.size;
+ }
+ }
}
auto& flags = params.flags;
diff --git a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
index 989bf61682..8dee9ccfb7 100644
--- a/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
+++ b/src/core/hle/service/nvdrv/devices/nx2_stub_device.h
@@ -5,6 +5,7 @@
#include <algorithm>
#include <atomic>
+#include <compare>
#include <functional>
#include <map>
#include <mutex>
@@ -44,6 +45,9 @@ public:
using SparseListener = std::function<void(u64 gpu_address, u64 size, bool create)>;
/// A block of a sparse region gets memory [cpu_address, +size).
using BackListener = std::function<void(u64 gpu_address, u64 cpu_address, u64 size)>;
+ /// A tile of a sparse region shows process memory [cpu_address, +size), or reads zero again
+ /// (cpu_address 0).
+ using TileListener = std::function<void(u64 gpu_address, u64 cpu_address, u64 size)>;
static Nx2GpuMemoryRegistry& Get() {
static Nx2GpuMemoryRegistry registry;
@@ -120,9 +124,22 @@ public:
}
}
+ // nx2:gpu 216 (sparse map requests): the tile [gpu_address, +size) of a sparse region now
+ // shows the process memory at cpu_address, the game's own memory (sub 101), or with
+ // cpu_address 0 it is unmapped and reads zero. The GPU mapping stays as it is; only the
+ // region's device pages change, in GPU-thread order.
+ void MapSparseTile(u64 gpu_address, u64 cpu_address, u64 size) {
+ std::scoped_lock lk{mutex};
+ for (auto& [owner, listener] : listeners) {
+ if (listener.tile) {
+ listener.tile(gpu_address, cpu_address, size);
+ }
+ }
+ }
+
void AddListener(const void* owner, Listener listener, UnmapListener unmap_listener,
ReleaseListener release_listener = {}, SparseListener sparse_listener = {},
- BackListener back_listener = {}) {
+ BackListener back_listener = {}, TileListener tile_listener = {}) {
std::scoped_lock lk{mutex};
for (const auto& m : mappings) {
listener(m.gpu_address, m.cpu_address, m.size);
@@ -135,7 +152,8 @@ public:
listeners.insert_or_assign(owner, Listeners{std::move(listener), std::move(unmap_listener),
std::move(release_listener),
std::move(sparse_listener),
- std::move(back_listener)});
+ std::move(back_listener),
+ std::move(tile_listener)});
}
void RemoveListener(const void* owner) {
@@ -149,12 +167,50 @@ public:
{
std::scoped_lock lk{device_mutex};
device_ranges.clear();
+ shared_ranges.clear();
}
std::scoped_lock lk{mutex};
mappings.clear();
sparse_regions.clear();
}
+ // Device (SMMU) range of a mapping outside the heap, shared by every address space of one
+ // process: the caches key memory by device address, so a range per address space made separate
+ // copies of the same memory, and presentation could read one the game never drew into. The
+ // first address space to map the range allocates it with `allocate` (0: out of space); every
+ // call takes a reference.
+ u64 AcquireDeviceRange(size_t asid, u64 cpu_address, u64 size,
+ const std::function<u64()>& allocate) {
+ std::scoped_lock lk{device_mutex};
+ const SharedRangeKey key{asid, cpu_address, size};
+ if (const auto it = shared_ranges.find(key); it != shared_ranges.end()) {
+ ++it->second.references;
+ return it->second.device_address;
+ }
+ const u64 device_address = allocate();
+ if (device_address != 0) {
+ shared_ranges.emplace(key, SharedRange{device_address, 1});
+ }
+ return device_address;
+ }
+
+ // Drops a reference. True for the last one: the caller frees the range.
+ bool ReleaseDeviceRange(size_t asid, u64 cpu_address, u64 size) {
+ std::scoped_lock lk{device_mutex};
+ const auto it = shared_ranges.find(SharedRangeKey{asid, cpu_address, size});
+ if (it == shared_ranges.end() || --it->second.references != 0) {
+ return false;
+ }
+ shared_ranges.erase(it);
+ return true;
+ }
+
+ // The process's session closed and its ASID can be reused: stop sharing its ranges.
+ void ForgetDeviceRanges(size_t asid) {
+ std::scoped_lock lk{device_mutex};
+ std::erase_if(shared_ranges, [asid](const auto& entry) { return entry.first.asid == asid; });
+ }
+
// Device (SMMU) address of a CPU range an address space mapped; used to present images.
void RecordDeviceRange(u64 cpu_address, u64 size, u64 device_address) {
std::scoped_lock lk{device_mutex};
@@ -180,6 +236,18 @@ private:
std::mutex device_mutex;
std::map<u64, std::pair<u64, u64>> device_ranges; // cpu start -> (size, device address)
+ struct SharedRangeKey {
+ size_t asid;
+ u64 cpu_address;
+ u64 size;
+ auto operator<=>(const SharedRangeKey&) const = default;
+ };
+ struct SharedRange {
+ u64 device_address;
+ u32 references;
+ };
+ std::map<SharedRangeKey, SharedRange> shared_ranges;
+
struct Mapping {
u64 gpu_address;
u64 cpu_address;
@@ -192,6 +260,7 @@ private:
ReleaseListener release;
SparseListener sparse;
BackListener back;
+ TileListener tile;
};
// Drops [gpu_address, +size) from the recorded mappings, keeping the parts outside it.
diff --git a/src/core/hle/service/nvdrv/nvdrv.cpp b/src/core/hle/service/nvdrv/nvdrv.cpp
index 12923496fb..7853d17235 100644
--- a/src/core/hle/service/nvdrv/nvdrv.cpp
+++ b/src/core/hle/service/nvdrv/nvdrv.cpp
@@ -133,6 +133,7 @@ Module::Module(Core::System& system)
Devices::Nx2GpuMemoryRegistry::Get().Reset();
Tegra::Nx2ClearSemaphoreRegions();
Tegra::Nx2ClearSparseRegions();
+ Tegra::Nx2ClearGpuWrittenPages();
Devices::g_nx2_driver = false;
Tegra::g_nx2_ampere = false;
Core::g_nx2_present_count = 0;
diff --git a/src/core/hle/service/nvdrv/nvdrv_interface.cpp b/src/core/hle/service/nvdrv/nvdrv_interface.cpp
index 020036d74b..2ee0d726a2 100644
--- a/src/core/hle/service/nvdrv/nvdrv_interface.cpp
+++ b/src/core/hle/service/nvdrv/nvdrv_interface.cpp
@@ -13,6 +13,7 @@
#include "core/hle/kernel/k_process.h"
#include "core/hle/kernel/k_readable_event.h"
#include "core/hle/service/ipc_helpers.h"
+#include "core/hle/service/nvdrv/devices/nx2_stub_device.h"
#include "core/hle/service/nvdrv/nvdata.h"
#include "core/hle/service/nvdrv/nvdrv.h"
#include "core/hle/service/nvdrv/nvdrv_interface.h"
@@ -36,7 +37,10 @@ void NVDRV::Open(HLERequestContext& ctx) {
const std::string device_name(Common::StringFromBuffer(buffer));
LOG_DEBUG(Service_NVDRV, "device_name={}", device_name);
- if (device_name == "/dev/nvhost-prof-gpu") {
+ // Switch 2 lists nvhost-prof-gpu with the other GPU nodes and the CUDA driver (Fire
+ // Emblem's subsdk1) opens it, so only refuse it to Switch 1 callers. Switch 2 opens get
+ // the logged stub device.
+ if (device_name == "/dev/nvhost-prof-gpu" && !Devices::g_nx2_driver) {
rb.Push<DeviceFD>(0);
rb.PushEnum(NvResult::NotSupported);
diff --git a/src/core/hle/service/nvnflinger/pixel_format.h b/src/core/hle/service/nvnflinger/pixel_format.h
index b6e8b5f8a0..a38c8be777 100644
--- a/src/core/hle/service/nvnflinger/pixel_format.h
+++ b/src/core/hle/service/nvnflinger/pixel_format.h
@@ -19,6 +19,7 @@ enum class PixelFormat : u32 {
Bgra8888 = 5,
Rgba5551 = 6,
Rgba4444 = 7,
+ Rgba1010102 = 43,
};
} // namespace Service::android
diff --git a/src/core/hle/service/nx2_gsv.h b/src/core/hle/service/nx2_gsv.h
index 6ccdb7ef85..259681a2a2 100644
--- a/src/core/hle/service/nx2_gsv.h
+++ b/src/core/hle/service/nx2_gsv.h
@@ -15,12 +15,14 @@
#include <cstdlib>
#include <cstdio>
#include <initializer_list>
+#include <limits>
#include <atomic>
#include <deque>
#include <map>
#include <optional>
#include <memory>
#include <mutex>
+#include <utility>
#include <vector>
#include "common/alignment.h"
@@ -45,13 +47,17 @@
#include "core/hle/kernel/k_resource_limit.h"
#include "core/hle/kernel/k_server_session.h"
#include "core/hle/kernel/k_event.h"
+#include "core/hle/kernel/k_hardware_timer.h"
#include "core/hle/kernel/k_readable_event.h"
#include "core/hle/kernel/k_session.h"
+#include "core/hle/kernel/k_thread.h"
#include "core/hle/kernel/k_transfer_memory.h"
+#include "core/hle/kernel/svc_results.h"
#include "core/memory.h"
#include "video_core/host1x/host1x.h"
#include "video_core/host1x/nx2_video_decoder.h"
#include "video_core/nx2.h"
+#include "video_core/textures/decoders.h"
#include "core/hle/service/hle_ipc.h"
#include "core/hle/service/ipc_helpers.h"
#include "core/hle/service/kernel_helpers.h"
@@ -102,6 +108,14 @@ inline void TipcReplyWords(HLERequestContext& ctx, std::initializer_list<u32> wo
TipcReplyWords(ctx, std::span<const u32>{words.begin(), words.size()});
}
+// TIPC 0(u64 token) on the Switch 2 sessions: a handshake newer SDKs send first; the reply echoes
+// the token. (The auto stub's reply only echoed it by leaving the request's words in place.)
+inline void TipcHandshake(HLERequestContext& ctx, std::string_view session) {
+ const auto* w = ctx.CommandBuffer();
+ LOG_INFO(Service, "{} TIPC 0 handshake {:#x}", session, u64{w[2]} | (u64{w[3]} << 32));
+ TipcReplyWords(ctx, {w[2], w[3]});
+}
+
// Replies with success and a copied handle to a new session served by `iface`.
template <class T>
inline void ReturnRawSession(Core::System& system, HLERequestContext& ctx, std::shared_ptr<T> iface) {
@@ -139,6 +153,8 @@ public:
{103, &INx2SyncptSession::QueryCounter, "QueryCounter"},
{201, &INx2SyncptSession::RequestSlotEvent, "RequestSlotEvent (guessed)"},
{202, &INx2SyncptSession::CancelSlotEvent, "CancelSlotEvent (guessed)"},
+ // Firmware commands without a known meaning: zeroed replies.
+ {21, &INx2SyncptSession::StubReplyZeroed, "Unknown21"},
});
}
@@ -160,9 +176,7 @@ public:
private:
// Newer SDKs (title 1) open with TIPC 0(u64 token) and abort unless the reply echoes it.
void Handshake(HLERequestContext& ctx) {
- const auto* w = ctx.CommandBuffer();
- LOG_INFO(Service, "nx2:syncpt TIPC 0 handshake {:#x}", u64{w[2]} | (u64{w[3]} << 32));
- TipcReplyWords(ctx, {w[2], w[3]});
+ TipcHandshake(ctx, "nx2:syncpt");
}
void GetCount(HLERequestContext& ctx) {
@@ -299,11 +313,14 @@ public:
explicit INx2GpuSession(Core::System& system_)
: ServiceFramework{system_, "nx2:gpu"}, m_context{system_, "nx2:gpu"} {
RegisterHandlersTipc({
+ {0, &INx2GpuSession::Handshake, "Handshake"},
{11, &INx2GpuSession::GetIdRange, "GetIdRange (guessed)"},
{12, &INx2GpuSession::GetIdEvent, "GetIdEvent (guessed)"},
{21, &INx2GpuSession::Start, "Start (guessed)"},
{22, &INx2GpuSession::OpenSubSession, "OpenSubSession (guessed)"},
{33, &INx2GpuSession::GetCount33, "Unknown33 (u32 count)"},
+ {41, &INx2GpuSession::CreateGraphicsDevice, "CreateGraphicsDevice"},
+ {42, &INx2GpuSession::DestroyGraphicsDevice, "DestroyGraphicsDevice"},
{201, &INx2GpuSession::GetUserPartition, "GetUserPartition (newer SDKs)"},
{202, &INx2GpuSession::GetGsvPartition, "GetGsvPartition (newer SDKs)"},
{203, &INx2GpuSession::GetVaRange, "GetVaRange (guessed)"},
@@ -315,10 +332,23 @@ public:
{213, &INx2GpuSession::SubUnmapJob, "NormalUnmapGpuMemory4K"},
{214, &INx2GpuSession::SubCreateGpuOnlyRegion, "CreateSparseRegion"},
{215, &INx2GpuSession::SubDestroySparseRegion, "DestroySparseRegion"},
+ {216, &INx2GpuSession::SubSparseMap, "ExecuteSparseMapRequests"},
+ // Firmware commands without a known meaning: zeroed replies.
+ {23, &INx2GpuSession::StubReplyZeroed, "Unknown23"},
+ {24, &INx2GpuSession::StubReplyZeroed, "Unknown24"},
+ {25, &INx2GpuSession::StubReplyZeroed, "Unknown25"},
+ {34, &INx2GpuSession::StubReplyZeroed, "Unknown34"},
+ {51, &INx2GpuSession::StubReplyZeroed, "Unknown51"},
+ {8001, &INx2GpuSession::StubReplyZeroed, "Unknown8001"},
+ {8002, &INx2GpuSession::StubReplyZeroed, "Unknown8002"},
+ {8003, &INx2GpuSession::StubReplyZeroed, "Unknown8003"},
+ {8004, &INx2GpuSession::StubReplyZeroed, "Unknown8004"},
});
if (LazySparseRegions()) {
Tegra::Nx2SetSparseBacking(
- [this](u64 gpu_address, u64 size) { BackSparseWrite(gpu_address, size); });
+ [this](u64 gpu_address, u64 size, const u8* data) {
+ BackSparseWrite(gpu_address, size, data);
+ });
}
}
@@ -333,6 +363,10 @@ public:
}
private:
+ void Handshake(HLERequestContext& ctx) {
+ TipcHandshake(ctx, "nx2:gpu");
+ }
+
static constexpr u32 NumIds = 32;
Kernel::KEvent* EventFor(u32 id) {
@@ -394,9 +428,39 @@ private:
static_cast<u32>(second), static_cast<u32>(second >> 32)});
}
+ // An empty partition is min = ~0, max = 0 (min > max). Fire Emblem: Fortune's Weave's SDK
+ // reads (0, 0) as a one-byte partition at address 0 and aborts in nn::gpu::sdk.
void GetGsvPartition(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) nx2:gpu TIPC 202 [2]={:#x}, empty", ctx.CommandBuffer()[2]);
- TipcReplyWords(ctx, {0, 0, 0, 0});
+ TipcReplyWords(ctx, {~0U, ~0U, 0, 0});
+ }
+
+ // Fire Emblem: Fortune's Weave: 41(name u64, flags u64, debug label u64) returns a device id
+ // that later NvRm calls name; 0 is not a valid id. Only flags 0 has been seen.
+ void CreateGraphicsDevice(HLERequestContext& ctx) {
+ const auto* b = ctx.CommandBuffer();
+ const u64 name = u64{b[2]} | (u64{b[3]} << 32);
+ const u64 flags = u64{b[4]} | (u64{b[5]} << 32);
+ u32 id = 0;
+ for (u32 candidate = 1; candidate < m_devices.size(); ++candidate) {
+ if (!m_devices[candidate]) {
+ m_devices[candidate] = true;
+ id = candidate;
+ break;
+ }
+ }
+ LOG_INFO(Service, "nx2:gpu 41 create graphics device name={:#x} flags={:#x} -> id {}", name,
+ flags, id);
+ IPC::ResponseBuilder rb{ctx, 3};
+ rb.Push(id != 0 ? ResultSuccess : Kernel::ResultOutOfResource);
+ rb.Push(id);
+ }
+
+ void DestroyGraphicsDevice(HLERequestContext& ctx) {
+ const u32 id = ctx.CommandBuffer()[2] & 0xff;
+ const bool existed = id < m_devices.size() && std::exchange(m_devices[id], false);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(existed ? ResultSuccess : Kernel::ResultNotFound);
}
void OpenSubSession(HLERequestContext& ctx) {
@@ -542,17 +606,37 @@ private:
const u64 size = last + 1 - start;
std::scoped_lock lk{m_region_mutex};
FreeReleasedRegions();
+ if ((w[7] & SparseRegionFlag) == 0) {
+ m_gpu_only_ranges[start] = size; // kept out of 216, however it is backed
+ }
if ((w[7] & SparseRegionFlag) != 0 && LazySparseRegions()) {
// Title 1's terrain texture arrays and layer pools (170 MB-5.8 GB, mostly empty):
// backed whole, they ran the pool dry and the later ones got no memory at all.
// The region gets a device range of its own, unbacked pages read zero, and
// BackSparseWrite gives a block memory before the GPU writes into it.
- m_sparse_regions[start] = {size, std::vector<u8>(Common::DivCeil(size, SparseBlockSize))};
+ m_sparse_regions[start] = {
+ .size = size,
+ .blocks = std::vector<u8>(Common::DivCeil(size, SparseBlockSize)),
+ };
Nvidia::Devices::Nx2GpuMemoryRegistry::Get().CreateSparse(start, size);
Tegra::Nx2SparseRegionCreated(start, size, SparseBlockSize);
Tegra::Nx2AddSparseSpan(start, size);
LOG_INFO(Service, "nx2:gpu 214 sparse region [{:#x}, {:#x}] backed as the GPU writes it",
start, last);
+ } else if (LazyGpuOnlyRegions()) {
+ // The rest is written by the host GPU, not through guest memory (title 1's and
+ // title 4's render targets, 285-300 MB regions): like a sparse region it gets a
+ // device range of its own and memory per block only when something writes guest
+ // memory there (downloads, copies). The texture cache sees ordinary memory.
+ m_sparse_regions[start] = {
+ .gpu_only = true,
+ .size = size,
+ .blocks = std::vector<u8>(Common::DivCeil(size, SparseBlockSize)),
+ };
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().CreateSparse(start, size);
+ Tegra::Nx2AddSparseSpan(start, size);
+ LOG_INFO(Service, "nx2:gpu 214 GPU-only region [{:#x}, {:#x}] backed as it is written",
+ start, last);
} else if (const auto address = BackGpuOnlyRegion(start, size)) {
Nvidia::Devices::Nx2GpuMemoryRegistry::Get().MapAt(start, *address, size);
LOG_INFO(Service, "nx2:gpu 214 GPU-only region [{:#x}, {:#x}] backed at {:#x} "
@@ -579,8 +663,17 @@ private:
first, last, w[1], w[2], w[7], w[8]);
std::scoped_lock lk{m_region_mutex};
FreeReleasedRegions();
+ if (m_gpu_only_ranges.erase(first) != 0 && last > first) {
+ DropGpuOnlyTiles(first, last - first + 1, false);
+ m_shared_regions.erase(first);
+ std::erase_if(m_shared_memory, [first](const auto& entry) { return entry.second == first; });
+ }
if (last > first) {
- if (m_sparse_regions.erase(first) != 0) {
+ if (const auto region = m_sparse_regions.find(first); region != m_sparse_regions.end()) {
+ m_tile_bytes -= Core::Memory::YUZU_PAGESIZE *
+ static_cast<u64>(std::count(region->second.tile_pages.begin(),
+ region->second.tile_pages.end(), true));
+ m_sparse_regions.erase(region);
Tegra::Nx2SparseRegionDestroyed(first);
Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DestroySparse(first, last - first + 1);
} else {
@@ -609,6 +702,66 @@ private:
rb.Push(ResultSuccess);
}
+ // Sub 216 (nn::gpu::sdk::ExecuteSparseMapRequests, behind nvnMemoryPoolMapVirtual and NvRm's
+ // remap): an A buffer of SparseMapRequests and a B buffer that gets the kind applied to each;
+ // word 8-9 is 1, 10 the address space, 11-12 the request size (40), 13 a sync flag, 14 the job
+ // slot. The reply is the number of requests done. This is how the game commits and releases
+ // its sparse texture tiles: a map points a tile at its own memory (sub 101), an unmap makes it
+ // read zero again. A region the game maps tiles into needs no memory of its own.
+ void SubSparseMap(HLERequestContext& ctx) {
+ const auto* w = ctx.CommandBuffer();
+ const u32 slot = w[14];
+ const auto data = ctx.BufferDescriptorA().empty() ? std::span<const u8>{} : ctx.ReadBufferA(0);
+ const size_t count = data.size() / sizeof(SparseMapRequest);
+ std::vector<u8> kinds(count);
+ {
+ std::scoped_lock lk{m_region_mutex};
+ FreeReleasedRegions();
+ ++m_tile_calls;
+ for (size_t i = 0; i < count; ++i) {
+ SparseMapRequest request;
+ std::memcpy(&request, data.data() + i * sizeof(SparseMapRequest), sizeof(request));
+ kinds[i] = request.kind;
+ ApplySparseMapRequest(request);
+ }
+ // The first request, then at most every 30 s.
+ const auto now = std::chrono::steady_clock::now();
+ if (count != 0 && (m_tile_requests == count ||
+ now - m_tile_log_time >= std::chrono::seconds{30})) {
+ m_tile_log_time = now;
+ LOG_INFO(Service, "nx2:gpu 216: {} calls, {} maps, {} unmaps; {} MiB of sparse tiles "
+ "and {} MiB in GPU-only regions mapped, {} maps into regions that "
+ "keep their own memory left out; {} MiB of sparse blocks backed, {} "
+ "MiB released",
+ m_tile_calls, m_tile_maps, m_tile_unmaps, m_tile_bytes >> 20,
+ m_gpu_only_tile_bytes >> 20, m_tile_kept, m_sparse_backed_bytes >> 20,
+ m_sparse_released_bytes >> 20);
+ }
+ }
+ if (!ctx.BufferDescriptorB().empty() && count != 0) {
+ ctx.WriteBufferB(kinds.data(), kinds.size(), 0);
+ }
+ SignalAfterGpu(slot);
+ TipcReplyWords(ctx, {static_cast<u32>(count), static_cast<u32>(u64{count} >> 32)});
+ }
+
+ static bool IsAllZero(const u8* data, size_t size) {
+ size_t i = 0;
+ for (; i + sizeof(u64) <= size; i += sizeof(u64)) {
+ u64 word;
+ std::memcpy(&word, data + i, sizeof(word));
+ if (word != 0) {
+ return false;
+ }
+ }
+ for (; i < size; ++i) {
+ if (data[i] != 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
// Returns the pages of destroyed regions the GPU thread is done with to the kernel.
void FreeReleasedRegions() {
auto* const process = system.ApplicationProcess();
@@ -716,7 +869,7 @@ private:
// usually on the GPU thread): the blocks the write touches get memory, cleared, behind the
// region's device pages. The GPU mapping stays as it is, so the images over the region keep
// their storage and only see the write.
- void BackSparseWrite(u64 gpu_address, u64 size) {
+ void BackSparseWrite(u64 gpu_address, u64 size, const u8* data) {
struct Pending {
u64 region;
u64 block;
@@ -739,11 +892,29 @@ private:
}
};
bool waits_for_other = false;
+ u64 zero_bytes = 0;
{
std::unique_lock lk{m_region_mutex};
for_regions([&](u64 start, SparseRegion& region, u64 first, u64 last) {
for (u64 block = first; block <= last; ++block) {
- if (region.blocks[block] == BlockEmpty) {
+ if (region.blocks[block] == BlockEmpty && region.tiles) {
+ // The write lands in the game's own memory (a tile it mapped) or nowhere;
+ // the texture cache only learns that the block has contents now.
+ region.blocks[block] = BlockWritten;
+ Tegra::Nx2SparseBlockWritten(start, block);
+ } else if (region.blocks[block] == BlockEmpty) {
+ // Zeros over a block nobody wrote change nothing: it reads zero without
+ // memory. Title 1 clears ~1 GB of them at boot.
+ const u64 block_begin = start + block * SparseBlockSize;
+ const u64 write_begin = (std::max)(gpu_address, block_begin);
+ const u64 write_end = (std::min)(
+ {end, block_begin + SparseBlockSize, start + region.size});
+ if (data != nullptr &&
+ IsAllZero(data + (write_begin - gpu_address), write_end - write_begin) &&
+ !Tegra::Nx2SparseBlockIsWritten(start, block)) {
+ zero_bytes += write_end - write_begin;
+ continue;
+ }
region.blocks[block] = BlockPending;
todo.push_back({start, block});
} else if (region.blocks[block] == BlockPending) {
@@ -752,6 +923,16 @@ private:
}
});
}
+ if (zero_bytes != 0) {
+ std::scoped_lock lk{m_region_mutex};
+ const u64 before = m_sparse_zero_bytes;
+ m_sparse_zero_bytes += zero_bytes;
+ if (before == 0 || (before >> 28) != (m_sparse_zero_bytes >> 28)) {
+ LOG_INFO(Service, "nx2:gpu {} MiB of zeros over unwritten sparse blocks left out "
+ "(they read zero without memory)",
+ m_sparse_zero_bytes >> 20);
+ }
+ }
// Allocating and mapping happens outside the lock (see AllocateGpuOnlyBacking).
for (const auto& [start, block] : todo) {
u64 block_size = SparseBlockSize;
@@ -769,6 +950,11 @@ private:
const auto it = m_sparse_regions.find(start);
if (it == m_sparse_regions.end() || block >= it->second.blocks.size()) {
stale = true; // the region was destroyed while its block was being backed
+ } else if (it->second.tiles) {
+ // The game started mapping its tiles there meanwhile (216): no block.
+ stale = true;
+ it->second.blocks[block] = BlockWritten;
+ Tegra::Nx2SparseBlockWritten(start, block);
} else if (!backing) {
auto& region = it->second;
region.blocks[block] = BlockFailed; // out of memory: not again for every write
@@ -830,20 +1016,329 @@ private:
return lazy;
}
+ // EDEN_NX2_GPU_ONLY_LAZY=0 backs GPU-only regions whole, as before.
+ static bool LazyGpuOnlyRegions() {
+ static const bool lazy = [] {
+ const char* value = std::getenv("EDEN_NX2_GPU_ONLY_LAZY");
+ return value == nullptr || value[0] != '0';
+ }();
+ return lazy;
+ }
+
static constexpr u32 SparseRegionFlag = 0x200; ///< Word 7 of 214 (0x60c arrays, 0xa0c pools)
static constexpr u64 SparseBlockSize = 4ULL << 20;
static constexpr u8 BlockEmpty = 0;
static constexpr u8 BlockBacked = 1;
static constexpr u8 BlockFailed = 2;
static constexpr u8 BlockPending = 3; ///< Being allocated and mapped by BackSparseWrite
+ static constexpr u8 BlockWritten = 4; ///< Written in a region the game maps tiles into
struct SparseRegion {
+ bool gpu_only{}; ///< A GPU-only region (no sparse flag), backed lazily as well
u64 size;
- std::vector<u8> blocks; ///< BlockEmpty / Backed / Failed / Pending per SparseBlockSize
+ std::vector<u8> blocks; ///< BlockEmpty / Backed / Failed / Pending / Written per block
bool warned{}; ///< Logged that the pool ran out for its blocks
+ bool tiles{}; ///< The game maps its own memory into it (216): no blocks
+ std::vector<bool> tile_pages; ///< Pages a tile maps, once `tiles`
};
std::map<u64, SparseRegion> m_sparse_regions; ///< By GPU address
u64 m_sparse_blocks{}; ///< Sparse blocks backed so far
u64 m_sparse_backed_bytes{}; ///< Sparse region memory given out so far
+ u64 m_sparse_zero_bytes{}; ///< Zeros written over unwritten sparse blocks, left out
+ u64 m_sparse_released_bytes{}; ///< Blocks given back once the game mapped tiles there
+
+ // nn::gpu::sdk::SparseMapRequest (216), filled from NvRm's nvgpu_as_remap_op.
+ struct SparseMapRequest {
+ u64 gpu_address;
+ u64 size;
+ u8 no_write;
+ u8 cacheable;
+ u8 unknown_12;
+ u8 kind; ///< Wanted: the compressible kind if there is one
+ u8 fallback_kind; ///< The incompressible kind
+ u8 unknown_15; ///< Remap op flag bit 15
+ u16 padding_16;
+ u32 memory_handle; ///< Sub 101's handle; 0 unmaps the range
+ u32 padding_1c;
+ u64 offset; ///< Into that memory
+ };
+ static_assert(sizeof(SparseMapRequest) == 40);
+ u64 m_tile_calls{};
+ u64 m_tile_requests{};
+ std::chrono::steady_clock::time_point m_tile_log_time{};
+ u64 m_tile_maps{};
+ u64 m_tile_unmaps{};
+ u64 m_tile_kept{}; ///< Maps into GPU-only regions that keep their own memory, left out
+ u64 m_tile_bytes{}; ///< Sparse region pages a tile maps right now
+
+ void ApplySparseMapRequest(const SparseMapRequest& request) {
+ ++m_tile_requests;
+ if (request.size == 0) {
+ return;
+ }
+ const u64 gpu_address = request.gpu_address;
+ const u64 size = request.size;
+ if (size > std::numeric_limits<u64>::max() - gpu_address) {
+ LOG_WARNING(Service, "nx2:gpu 216 [{:#x}, +{:#x}] wraps the address space", gpu_address,
+ size);
+ return;
+ }
+ auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get();
+ auto it = m_sparse_regions.upper_bound(gpu_address);
+ SparseRegion* region = nullptr;
+ u64 start = 0;
+ if (it != m_sparse_regions.begin()) {
+ --it;
+ if (size <= it->second.size && gpu_address - it->first <= it->second.size - size) {
+ start = it->first;
+ region = &it->second;
+ }
+ }
+ if (const auto gpu_only = GpuOnlyRegionOf(gpu_address, size)) {
+ ApplyGpuOnlyRequest(*gpu_only, request);
+ return;
+ }
+ if (request.memory_handle == 0) {
+ ++m_tile_unmaps;
+ LOG_DEBUG(Service, "nx2:gpu 216 unmap [{:#x}, +{:#x}]", gpu_address, size);
+ if (region != nullptr) {
+ Tegra::Nx2SparseTilesMapped(start, gpu_address - start, size, false);
+ registry.MapSparseTile(gpu_address, 0, size);
+ MarkTiles(*region, gpu_address - start, size, false);
+ } else {
+ registry.Unmap(gpu_address, size);
+ }
+ return;
+ }
+ ++m_tile_maps;
+ const auto memory = m_memory.find(request.memory_handle);
+ if (memory == m_memory.end() || request.offset > memory->second.size || size > memory->second.size - request.offset) {
+ LOG_WARNING(Service, "nx2:gpu 216 map [{:#x}, +{:#x}] of unknown memory {} offset {:#x}",
+ gpu_address, size, request.memory_handle, request.offset);
+ return;
+ }
+ const u64 cpu_address = memory->second.address + request.offset;
+ LOG_DEBUG(Service, "nx2:gpu 216 map [{:#x}, +{:#x}] -> memory {} offset {:#x} kind {:#x}",
+ gpu_address, size, request.memory_handle, request.offset, request.kind);
+ if (region == nullptr) {
+ registry.MapAt(gpu_address, cpu_address, size);
+ return;
+ }
+ if (!region->tiles) {
+ StartTiles(start, *region);
+ }
+ // The game's memory already holds what the tile shows (title 1 fills nine world-sized
+ // masks, R8/RG16/R16 at 8192-16384 texels, in its own memory before mapping them): the
+ // blocks count as written, so the texture cache gives the images over them memory and
+ // loads them. Left unwritten, those images read zero and the static world was unlit.
+ const u64 first_block = (gpu_address - start) / SparseBlockSize;
+ const u64 last_block = (gpu_address + size - 1 - start) / SparseBlockSize;
+ for (u64 block = first_block; block <= last_block; ++block) {
+ if (region->blocks[block] == BlockEmpty) {
+ region->blocks[block] = BlockWritten;
+ }
+ Tegra::Nx2SparseBlockWritten(start, block);
+ }
+ Tegra::Nx2SparseTilesMapped(start, gpu_address - start, size, true);
+ registry.MapSparseTile(gpu_address, cpu_address, size);
+ MarkTiles(*region, gpu_address - start, size, true);
+ }
+
+ // Start of the region 214 made without the sparse flag that holds the range (by its flags,
+ // not by how it is backed).
+ std::optional<u64> GpuOnlyRegionOf(u64 gpu_address, u64 size) const {
+ auto it = m_gpu_only_ranges.upper_bound(gpu_address);
+ if (it == m_gpu_only_ranges.begin()) {
+ return std::nullopt;
+ }
+ --it;
+ if (size > it->second || gpu_address - it->first > it->second - size) {
+ return std::nullopt;
+ }
+ return it->first;
+ }
+
+ // 216 into a GPU-only region: render targets, and with remap support also some texture arrays
+ // (title 1: 5.8 GB and 1.4 GB). The game backs those with its own memory too, which it fills
+ // through its own address, so a map points the range at that memory the way 211 does: both
+ // addresses reach the same device pages and the texture cache sees the writes. (Re-pointing the
+ // region's own device pages instead, as for sparse tiles, left the arrays stale: black terrain.)
+ // Left alone:
+ // - Regions whose memory the game shares with another GPU-only region (render targets): the
+ // texture cache would flush and re-upload such overlapping render targets every frame. All of
+ // a group keep the memory 214 gave them.
+ // - Regions 214 backs as they are written: the texture cache reads an image's memory in full
+ // when it uploads it, which makes the game's pages behind it resident on the host, while
+ // unwritten blocks read as zero for free (title 1: ~1 GB less resident at 150 s).
+ void ApplyGpuOnlyRequest(u64 region, const SparseMapRequest& request) {
+ auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get();
+ const u64 gpu_address = request.gpu_address;
+ const u64 size = request.size;
+ if (request.memory_handle == 0) {
+ ++m_tile_unmaps;
+ DropGpuOnlyTiles(gpu_address, size, true);
+ return;
+ }
+ ++m_tile_maps;
+ if (m_sparse_regions.contains(region)) {
+ ++m_tile_kept;
+ return;
+ }
+ if (m_shared_regions.contains(region)) {
+ m_shared_memory[{request.memory_handle, request.offset}] = region;
+ ++m_tile_kept;
+ return;
+ }
+ const auto memory = m_memory.find(request.memory_handle);
+ if (memory == m_memory.end() || request.offset > memory->second.size || size > memory->second.size - request.offset) {
+ LOG_WARNING(Service, "nx2:gpu 216 map [{:#x}, +{:#x}] of unknown memory {} offset {:#x}",
+ gpu_address, size, request.memory_handle, request.offset);
+ return;
+ }
+ if (const auto other = RegionSharing(request.memory_handle, request.offset, size, region)) {
+ ++m_tile_kept;
+ for (const u64 shared : {region, *other}) {
+ if (m_shared_regions.insert(shared).second) {
+ const auto range = m_gpu_only_ranges.find(shared);
+ if (range != m_gpu_only_ranges.end()) {
+ DropGpuOnlyTiles(shared, range->second, true);
+ }
+ LOG_INFO(Service, "nx2:gpu GPU-only region at {:#x} shares memory with another: "
+ "it keeps its own",
+ shared);
+ }
+ }
+ return;
+ }
+ DropGpuOnlyTiles(gpu_address, size, false);
+ registry.MapAt(gpu_address, memory->second.address + request.offset, size);
+ m_gpu_only_tiles[gpu_address] = {size, region, request.memory_handle, request.offset};
+ m_gpu_only_memory[{request.memory_handle, request.offset}] = {request.offset + size, region};
+ m_gpu_only_tile_max = (std::max)(m_gpu_only_tile_max, size);
+ m_gpu_only_tile_bytes += size;
+ }
+
+ // Another GPU-only region whose tiles hold any of [offset, +size) of the memory, if one does.
+ std::optional<u64> RegionSharing(u32 handle, u64 offset, u64 size, u64 region) const {
+ auto it = m_gpu_only_memory.lower_bound({handle, offset + size});
+ while (it != m_gpu_only_memory.begin()) {
+ --it;
+ const auto& [key, value] = *it;
+ const auto& [end, owner] = value;
+ if (key.first != handle || key.second + m_gpu_only_tile_max <= offset) {
+ break;
+ }
+ if (end > offset && owner != region) {
+ return owner;
+ }
+ }
+ // Regions that already keep their own memory record no tiles: compare with their requests.
+ if (const auto kept = m_shared_memory.find({handle, offset}); kept != m_shared_memory.end() &&
+ kept->second != region) {
+ return kept->second;
+ }
+ return std::nullopt;
+ }
+
+ // The memory 214 gave a GPU-only region at [gpu_address, +size), when it was backed whole.
+ std::optional<u64> OwnBackingOf(u64 gpu_address, u64 size) const {
+ auto it = m_gpu_only_regions.upper_bound(gpu_address);
+ if (it == m_gpu_only_regions.begin()) {
+ return std::nullopt;
+ }
+ --it;
+ if (size > it->second.size || gpu_address - it->first > it->second.size - size) {
+ return std::nullopt;
+ }
+ return it->second.cpu_address + (gpu_address - it->first);
+ }
+
+ // Forgets the game's tiles in [gpu_address, +size) of GPU-only regions; with `restore` (an
+ // unmap) those ranges show the region's own memory again, or nothing where it has none.
+ void DropGpuOnlyTiles(u64 gpu_address, u64 size, bool restore) {
+ auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get();
+ auto it = m_gpu_only_tiles.upper_bound(gpu_address);
+ if (it != m_gpu_only_tiles.begin()) {
+ const auto previous = std::prev(it);
+ if (previous->first + previous->second.size > gpu_address) {
+ it = previous;
+ }
+ }
+ while (it != m_gpu_only_tiles.end() && it->first < gpu_address + size) {
+ const auto& [tile_address, tile] = *it;
+ if (restore) {
+ if (const auto backing = OwnBackingOf(tile_address, tile.size)) {
+ registry.MapAt(tile_address, *backing, tile.size);
+ } else {
+ registry.Unmap(tile_address, tile.size);
+ }
+ }
+ if (const auto owner = m_gpu_only_memory.find({tile.memory_handle, tile.offset});
+ owner != m_gpu_only_memory.end() && owner->second.second == tile.region) {
+ if (m_shared_regions.contains(tile.region)) {
+ m_shared_memory[owner->first] = tile.region;
+ }
+ m_gpu_only_memory.erase(owner);
+ }
+ m_gpu_only_tile_bytes -= tile.size;
+ it = m_gpu_only_tiles.erase(it);
+ }
+ }
+
+ // The game maps tiles into this region itself (216): writes outside its tiles are dropped, as
+ // on the console, so it gets no more blocks, and the blocks it got before (written while no
+ // tile was mapped, which the console would have dropped) go back.
+ void StartTiles(u64 start, SparseRegion& region) {
+ using Core::Memory::YUZU_PAGESIZE;
+ region.tiles = true;
+ region.tile_pages.assign(Common::DivCeil(region.size, YUZU_PAGESIZE), false);
+ std::shared_ptr<std::atomic<bool>> done;
+ u64 released = 0;
+ for (u64 block = 0; block < region.blocks.size(); ++block) {
+ if (region.blocks[block] != BlockBacked) {
+ continue;
+ }
+ region.blocks[block] = BlockWritten;
+ const u64 block_address = start + block * SparseBlockSize;
+ const auto backing = m_gpu_only_regions.find(block_address);
+ if (backing == m_gpu_only_regions.end()) {
+ continue;
+ }
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().MapSparseTile(block_address, 0,
+ backing->second.size);
+ if (!done) {
+ done = std::make_shared<std::atomic<bool>>(false);
+ system.GPU().RunInOrder([done] { done->store(true, std::memory_order_release); });
+ }
+ released += backing->second.size;
+ m_released_regions.push_back({std::move(backing->second), done});
+ m_gpu_only_regions.erase(backing);
+ }
+ m_sparse_released_bytes += released;
+ LOG_INFO(Service, "nx2:gpu sparse region at {:#x}: the game maps its tiles, {} MiB of blocks "
+ "released",
+ start, released >> 20);
+ }
+
+ // Counts the bytes of tiles mapped, page by page.
+ void MarkTiles(SparseRegion& region, u64 offset, u64 size, bool mapped) {
+ using Core::Memory::YUZU_PAGESIZE;
+ if (!region.tiles) {
+ return;
+ }
+ const u64 last = (std::min)(Common::DivCeil(offset + size, YUZU_PAGESIZE),
+ static_cast<u64>(region.tile_pages.size()));
+ for (u64 page = offset / YUZU_PAGESIZE; page < last; ++page) {
+ if (region.tile_pages[page] != mapped) {
+ region.tile_pages[page] = mapped;
+ if (mapped) {
+ m_tile_bytes += YUZU_PAGESIZE;
+ } else {
+ m_tile_bytes -= YUZU_PAGESIZE;
+ }
+ }
+ }
+ }
+
u64 m_gpu_only_bytes{}; ///< Backing of GPU-only regions and sparse blocks right now
u64 m_gpu_only_peak{};
std::atomic<u64> m_alias_hint{}; ///< 2 MiB slot after the last backing in the alias code region
@@ -861,6 +1356,21 @@ private:
std::shared_ptr<std::atomic<bool>> done; ///< Set by the GPU thread past the region's last use
};
std::map<u64, GpuOnlyRegion> m_gpu_only_regions; ///< By GPU address
+ std::map<u64, u64> m_gpu_only_ranges; ///< 214 regions without the sparse flag: GPU address -> size
+ struct GpuOnlyTile {
+ u64 size;
+ u64 region; ///< Start of its GPU-only region
+ u32 memory_handle;
+ u64 offset;
+ };
+ std::map<u64, GpuOnlyTile> m_gpu_only_tiles; ///< The game's memory 216 put in GPU-only regions
+ /// (memory handle, offset) -> (end offset, region) of those tiles, to find shared memory
+ std::map<std::pair<u32, u64>, std::pair<u64, u64>> m_gpu_only_memory;
+ /// Memory mapped into regions that keep their own (handle, offset) -> region
+ std::map<std::pair<u32, u64>, u64> m_shared_memory;
+ std::set<u64> m_shared_regions; ///< GPU-only regions sharing memory: they keep their own
+ u64 m_gpu_only_tile_max{}; ///< Largest such tile, bounds the search
+ u64 m_gpu_only_tile_bytes{}; ///< GPU-only region memory the game maps right now
std::vector<ReleasedRegion> m_released_regions;
struct GpuMemory {
@@ -872,6 +1382,47 @@ private:
std::array<Kernel::KEvent*, NumIds> m_events{};
std::map<u32, GpuMemory> m_memory;
u32 m_next_memory_handle{1};
+ std::array<bool, 64> m_devices{}; // gpu 41 graphics device ids (0 unused)
+
+ // Every 30 s: the backing of sparse blocks and GPU-only regions is marked cold. The game
+ // writes it once (texture uploads by 2D blits: title 1 fills 2-4 GB of texture pools) and it
+ // is read again only when the texture cache builds an image over it, so the OS may compress or
+ // page it out before anything else. Last member: stops before the regions are destroyed.
+ void HintColdBacking() {
+ struct Range {
+ u64 physical_offset;
+ u64 virtual_address;
+ u64 size;
+ };
+ std::vector<Range> ranges;
+ {
+ std::scoped_lock lk{m_region_mutex};
+ for (const auto& [gpu_address, region] : m_gpu_only_regions) {
+ u64 virtual_address = region.cpu_address;
+ for (const auto& block : *region.pages) {
+ ranges.push_back({GetInteger(block.GetAddress()) - Core::DramMemoryMap::Base,
+ virtual_address, block.GetSize()});
+ virtual_address += block.GetSize();
+ }
+ }
+ }
+ auto& buffer = system.DeviceMemory().buffer;
+ for (const Range& range : ranges) {
+ buffer.HintCold(range.physical_offset, range.size, range.virtual_address);
+ }
+ }
+ std::jthread m_cold_hint_thread{[this](std::stop_token stop) {
+ Common::SetCurrentThreadName("nx2:ColdHint");
+ Common::SetCurrentThreadPriority(Common::ThreadPriority::Low);
+ while (!stop.stop_requested()) {
+ for (int i = 0; i < 30 && !stop.stop_requested(); ++i) {
+ std::this_thread::sleep_for(std::chrono::seconds{1});
+ }
+ if (!stop.stop_requested()) {
+ HintColdBacking();
+ }
+ }
+ }};
};
// Native memory and images handed out by the Switch 2 graphics server (gsv 111/114/423/429/315),
@@ -890,6 +1441,10 @@ struct Nx2NativeObjects {
u64 offset{}; // into its memory (images of a decoder pool share one)
bool window_pool{};
bool decoder{}; // a video decoder output image (gsv 911)
+ // A decoder image the client reads on the GPU (gsv 428/426): written block-linear.
+ bool gpu_read{};
+ u32 gpu_maps{}; // outstanding 426 maps
+ bool mapped_here{}; // 426 added its GPU mapping (no other mapping covered it)
};
std::mutex mutex;
std::map<u32, Memory> memories;
@@ -973,6 +1528,34 @@ private:
std::map<u32, Kernel::KEvent*> m_events;
};
+// nn::camera::sdk camera context (gsv 602). GameChat builds its local camera video storage from it
+// and polls it every frame. Nothing about the commands is known beyond the arguments seen (3 and
+// 21 take the camera name "Default", 29 a u32 0x180 = 384, the width of the 384x216 camera images),
+// and the game runs on an empty success, so they answer that without a log line per call.
+class INx2CameraSession final : public ServiceFramework<INx2CameraSession> {
+public:
+ explicit INx2CameraSession(Core::System& system_) : ServiceFramework{system_, "nx2:camera"} {
+ RegisterHandlersTipc({
+ {3, &INx2CameraSession::Stub, "Unknown3 (camera name \"Default\")"},
+ {21, &INx2CameraSession::Stub, "Unknown21 (camera name \"Default\")"},
+ {29, &INx2CameraSession::Stub, "Unknown29 (u32 0x180, polled every frame)"},
+ });
+ }
+
+private:
+ void Stub(HLERequestContext& ctx) {
+ const u32 command = ctx.GetCommand();
+ if (m_logged.insert(command).second) {
+ LOG_WARNING(Service, "(STUBBED) nx2:camera TIPC {} [2]={:#x} [3]={:#x}, returning success",
+ command, ctx.CommandBuffer()[2], ctx.CommandBuffer()[3]);
+ }
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultSuccess);
+ }
+
+ std::set<u32> m_logged;
+};
+
// Video decoder output images (nn::codec image pools, gsv 991/911/961). The preference
// (MakeDecodeImagePreference) holds an output layout code at word 1 and (width - 1) |
// (height - 1) << 16 at word 2. Codes come in pairs, one per CodecMode: 1/4 NV12 (format 3, what
@@ -1027,19 +1610,73 @@ public:
: ServiceFramework{system_, "nx2:vi2"}, m_context{system_, "nx2:vi2"},
m_objects{std::move(objects_)} {
RegisterHandlersTipc({
+ {0, &INx2Vi2Session::Handshake, "Handshake"},
{1, &INx2Vi2Session::ReturnOne, "Unknown1"},
{3, &INx2Vi2Session::ReturnOne, "GetDisplayCount (guessed)"},
{5, &INx2Vi2Session::GetTimestampRecorderCount, "GetTimestampRecorderCount (guessed)"},
{11, &INx2Vi2Session::ReturnEvent, "Unknown11"},
- {31, &INx2Vi2Session::GetVsyncEvent, "GetVsyncEvent (guessed)"},
- {32, &INx2Vi2Session::GetVsyncEvent, "GetDisplayVsyncEvent (guessed)"},
- // Title 1 calls these ~75 times a second with the display name "default"; the
- // auto stub's empty success reply works, but logged two lines per call (a 50 MB log
- // in an 18 minute session).
- {104, &INx2Vi2Session::SucceedQuietly, "Unknown104 (display \"default\")"},
- {105, &INx2Vi2Session::SucceedQuietly, "Unknown105 (display \"default\")"},
+ {31, &INx2Vi2Session::AcquireNativeEvent, "AcquireNativeEvent"},
+ {32, &INx2Vi2Session::DuplicateNativeEvent, "DuplicateNativeEvent"},
+ {261, &INx2Vi2Session::BindVsync, "BindDisplayVsyncEvent"},
+ {262, &INx2Vi2Session::UnbindNativeEvent, "UnbindDisplayVsyncEvent"},
+ {263, &INx2Vi2Session::BindChange, "BindDisplayRefreshRateEvent"},
+ {264, &INx2Vi2Session::UnbindNativeEvent, "UnbindDisplayRefreshRateEvent"},
+ {265, &INx2Vi2Session::BindChange, "BindDisplayHdrStatusEvent"},
+ {266, &INx2Vi2Session::UnbindNativeEvent, "UnbindDisplayHdrStatusEvent"},
+ {267, &INx2Vi2Session::BindChange, "BindDisplayHdrPaperWhiteEvent"},
+ {268, &INx2Vi2Session::UnbindNativeEvent, "UnbindDisplayHdrPaperWhiteEvent"},
+ {269, &INx2Vi2Session::BindChange, "BindDisplayBufferMaxResolutionEvent"},
+ {270, &INx2Vi2Session::UnbindNativeEvent, "UnbindDisplayBufferMaxResolutionEvent"},
+ {271, &INx2Vi2Session::BindChange, "BindDisplayModeCapabilityEvent"},
+ {272, &INx2Vi2Session::UnbindNativeEvent, "UnbindDisplayModeCapabilityEvent"},
+ // Title 1 calls these ~75 times a second with the display name "default"; quiet
+ // handlers (the auto stub logged two lines per call: a 50 MB log in an 18 minute
+ // session).
+ {103, &INx2Vi2Session::QueryDisplayRefreshRate, "QueryDisplayRefreshRate"},
+ {104, &INx2Vi2Session::QueryDisplayHdrStatus, "QueryDisplayHdrStatus"},
+ {105, &INx2Vi2Session::QueryDisplayHdrPaperWhite, "QueryDisplayHdrPaperWhite"},
+ {212, &INx2Vi2Session::GetLayerBufferBits, "GetLayerBufferBits"},
+ {106, &INx2Vi2Session::GetBufferMaxResolution, "GetBufferMaxResolution"},
{228, &INx2Vi2Session::CancelClientBuffer, "CancelClientBuffer"},
{229, &INx2Vi2Session::PresentClientBuffer, "PresentClientBuffer"},
+ // Firmware commands without a known meaning: zeroed replies.
+ {13, &INx2Vi2Session::StubReplyZeroed, "Unknown13"},
+ {51, &INx2Vi2Session::StubReplyZeroed, "Unknown51"},
+ {101, &INx2Vi2Session::StubReplyZeroed, "Unknown101"},
+ {102, &INx2Vi2Session::StubReplyZeroed, "Unknown102"},
+ {107, &INx2Vi2Session::StubReplyZeroed, "Unknown107"},
+ {108, &INx2Vi2Session::StubReplyZeroed, "Unknown108"},
+ {111, &INx2Vi2Session::StubReplyZeroed, "Unknown111"},
+ {112, &INx2Vi2Session::StubReplyZeroed, "Unknown112"},
+ {113, &INx2Vi2Session::StubReplyZeroed, "Unknown113"},
+ {202, &INx2Vi2Session::StubReplyZeroed, "Unknown202"},
+ {230, &INx2Vi2Session::StubReplyZeroed, "Unknown230"},
+ {231, &INx2Vi2Session::StubReplyZeroed, "Unknown231"},
+ {232, &INx2Vi2Session::StubReplyZeroed, "Unknown232"},
+ {251, &INx2Vi2Session::StubReplyZeroed, "Unknown251"},
+ {252, &INx2Vi2Session::StubReplyZeroed, "Unknown252"},
+ {261, &INx2Vi2Session::StubReplyZeroed, "Unknown261"},
+ {262, &INx2Vi2Session::StubReplyZeroed, "Unknown262"},
+ {263, &INx2Vi2Session::StubReplyZeroed, "Unknown263"},
+ {264, &INx2Vi2Session::StubReplyZeroed, "Unknown264"},
+ {265, &INx2Vi2Session::StubReplyZeroed, "Unknown265"},
+ {266, &INx2Vi2Session::StubReplyZeroed, "Unknown266"},
+ {267, &INx2Vi2Session::StubReplyZeroed, "Unknown267"},
+ {268, &INx2Vi2Session::StubReplyZeroed, "Unknown268"},
+ {269, &INx2Vi2Session::StubReplyZeroed, "Unknown269"},
+ {270, &INx2Vi2Session::StubReplyZeroed, "Unknown270"},
+ {271, &INx2Vi2Session::StubReplyZeroed, "Unknown271"},
+ {272, &INx2Vi2Session::StubReplyZeroed, "Unknown272"},
+ {301, &INx2Vi2Session::StubReplyZeroed, "Unknown301"},
+ {302, &INx2Vi2Session::StubReplyZeroed, "Unknown302"},
+ {351, &INx2Vi2Session::StubReplyZeroed, "Unknown351"},
+ {352, &INx2Vi2Session::StubReplyZeroed, "Unknown352"},
+ {353, &INx2Vi2Session::StubReplyZeroed, "Unknown353"},
+ {354, &INx2Vi2Session::StubReplyZeroed, "Unknown354"},
+ {355, &INx2Vi2Session::StubReplyZeroed, "Unknown355"},
+ {356, &INx2Vi2Session::StubReplyZeroed, "Unknown356"},
+ {358, &INx2Vi2Session::StubReplyZeroed, "Unknown358"},
+ {359, &INx2Vi2Session::StubReplyZeroed, "Unknown359"},
});
}
@@ -1059,6 +1696,10 @@ public:
}
private:
+ void Handshake(HLERequestContext& ctx) {
+ TipcHandshake(ctx, "nx2:vi2");
+ }
+
// 229(u64 client token, u32 slot, u32 image, ...fences): result, u64 mask of buffer slots the
// client may acquire next, four zero words. Nothing is shown yet; every slot is handed back
// at once (changes.patch's default mask is 0x7fff).
@@ -1070,29 +1711,45 @@ private:
LOG_INFO(Service, "nx2:vi2 229 present #{} token={:#x} slot={} image={}", m_presents,
u64{w[2]} | (u64{w[3]} << 32), w[4], w[5]);
}
- // Wait for the previous frame's fence instead of this one, so the game prepares frame N+1
- // while the GPU thread runs frame N (the console keeps several frames in flight). Waiting
- // for this frame serialized the two: 8-56 ms of every 34-105 ms frame on title 1.
- // EDEN_NX2_FRAMES_AHEAD=0 restores the wait for the current frame.
- static const bool frames_ahead = [] {
+ // Wait for this frame's fence, so the game doesn't write the next frame's data (constant
+ // buffers holding texture handles, vertex data, Pokopia's glyph atlas) while the GPU
+ // thread still reads this frame's. But title 1's GPU waits on semaphores its CPU releases
+ // only after this present returns: while a channel stays suspended on one, wait for the
+ // previous frame's fence instead and let the game prepare frame N+1 meanwhile (waiting
+ // for this frame there cost 8-56 ms of every 34-105 ms frame, or never finished).
+ // EDEN_NX2_FRAMES_AHEAD=1 always waits for the previous frame, =0 always for this one.
+ static const int frames_ahead = [] {
const char* value = std::getenv("EDEN_NX2_FRAMES_AHEAD");
- return value == nullptr || value[0] != '0';
+ return value == nullptr ? -1 : value[0] == '1' ? 1 : 0;
}();
const auto entered = std::chrono::steady_clock::now();
- if (frames_ahead) {
+ if (frames_ahead == 1 || (frames_ahead == -1 && !WaitForFrame(~w[22], w[24], true))) {
LOG_DEBUG(Service, "nx2:vi2 present #{} fence ({}, {:#x}), waiting for ({}, {:#x}); now {:#x}",
m_presents, ~w[22], w[24], m_previous_fence_id, m_previous_fence_value,
~w[22] < 192 ? system.Host1x().GetSyncpointManager().GetGuestSyncpointValue(~w[22]) : 0U);
+ // With EDEN_NX2_FRAMES_AHEAD=1, only run a frame ahead while the GPU keeps up. With
+ // the previous frame still on the GPU here, the GPU is the limit and running ahead
+ // gains nothing; and Splatoon Raiders, which reuses its command memory as if the GPU
+ // were at most a frame behind, rewrote command lists Eden had not read yet (misread
+ // commands, then crashes, with a slow GPU). Wait for this frame too then. Not when a
+ // suspended channel brought us here: this frame can't finish before the present
+ // returns.
+ const bool gpu_behind = frames_ahead == 1 && m_previous_fence_id < 192 &&
+ !system.Host1x().GetSyncpointManager().IsReadyGuest(
+ m_previous_fence_id, m_previous_fence_value);
WaitForFrame(m_previous_fence_id, m_previous_fence_value);
- m_previous_fence_id = ~w[22];
- m_previous_fence_value = w[24];
- } else {
+ if (gpu_behind) {
+ WaitForFrame(~w[22], w[24]);
+ }
+ } else if (frames_ahead == 0) {
WaitForFrame(~w[22], w[24]);
}
+ m_previous_fence_id = ~w[22];
+ m_previous_fence_value = w[24];
const auto waited = std::chrono::steady_clock::now();
- Composite(w[5], ~w[22], w[24]);
+ Composite(w[5], ~w[22], w[24], w[7]);
const auto composited = std::chrono::steady_clock::now();
- PaceToVblank();
+ PaceToVblank(w[12]);
const auto paced = std::chrono::steady_clock::now();
// Where each frame's time goes on the present thread, averaged per 600 presents: the
// game is blocked here while Eden's GPU thread catches up (fence) and composites.
@@ -1148,25 +1805,49 @@ private:
// present faster than that. Every slot is reported free here: hold each present until the
// next vblank instead (title 4 ran at up to 160 fps, its timing and audio with it).
std::chrono::steady_clock::time_point m_next_vblank{};
+ u32 m_swap_word{};
- void PaceToVblank() {
+ // timing: present word 12, (swap interval << 16) | refresh rate. Fire Emblem: Fortune's Weave
+ // sends 0x1003c during its boot logos and 0x2003c (two vblanks per frame, 30 fps) after; paced
+ // at every vblank it ran at double speed. EDEN_NX2_SWAP_INTERVAL=<n> forces the interval.
+ void PaceToVblank(u32 timing) {
if (!Settings::values.use_speed_limit.GetValue() || Settings::SpeedLimit() == 0) {
return;
}
- const auto interval = std::chrono::nanoseconds{100'000'000'000LL / 60 / Settings::SpeedLimit()};
+ static const u32 forced_interval = [] {
+ const char* value = std::getenv("EDEN_NX2_SWAP_INTERVAL");
+ return value ? static_cast<u32>(std::strtoul(value, nullptr, 10)) : 0U;
+ }();
+ const u32 swap_interval =
+ forced_interval != 0 ? forced_interval : std::clamp<u32>(timing >> 16, 1, 4);
+ const u32 refresh_rate = (timing & 0xffff) >= 30 && (timing & 0xffff) <= 240 ? timing & 0xffff : 60;
+ if (timing != std::exchange(m_swap_word, timing)) {
+ LOG_INFO(Service, "nx2:vi2 present #{} swap interval {} at {} Hz (word 12 = {:#x})",
+ m_presents, swap_interval, refresh_rate, timing);
+ }
+ const auto interval = std::chrono::nanoseconds{
+ 100'000'000'000LL * swap_interval / refresh_rate / Settings::SpeedLimit()};
const auto now = std::chrono::steady_clock::now();
if (now > m_next_vblank + interval) {
- // Running slower than 60 fps: show at once and don't bank the lost time.
+ // Running slower than the swap interval: show at once and don't bank the lost time.
m_next_vblank = now;
} else if (now < m_next_vblank) {
- std::this_thread::sleep_until(m_next_vblank);
+ // Sleep this service thread in the guest kernel. It runs on guest core 3, and a host
+ // sleep held that core for the whole wait: game threads scheduled there ran only
+ // between presents (Fortune's Weave's loader read one RomFS block per frame).
+ const auto wait = std::chrono::duration_cast<std::chrono::nanoseconds>(m_next_vblank - now);
+ static_cast<void>(Kernel::GetCurrentThread(kernel).Sleep(
+ kernel, kernel.HardwareTimer().GetTick() + wait.count() + 2));
}
m_next_vblank += interval;
}
- void WaitForFrame(u32 fence_id, u32 fence_value) {
+ /// Waits until the fence is reached. With `unless_gpu_waits`, gives up after a few ms once a
+ /// GPU channel was seen suspended on a semaphore (it may be one this thread releases later).
+ /// Returns whether the fence was reached.
+ bool WaitForFrame(u32 fence_id, u32 fence_value, bool unless_gpu_waits = false) {
if (fence_id >= 192) {
- return;
+ return true;
}
auto& syncpoints = system.Host1x().GetSyncpointManager();
const auto start = std::chrono::steady_clock::now();
@@ -1191,11 +1872,24 @@ private:
wake->cv.notify_all();
});
std::unique_lock lk{wake->mutex};
+ // Title 1's channels resume and suspend again while they wait on this thread, so any
+ // suspension counts, not only an unbroken one.
+ bool gpu_waited = false;
while (!wake->done && system.IsPoweredOn() &&
std::chrono::steady_clock::now() < deadline) {
- wake->cv.wait_for(lk, std::chrono::milliseconds{50});
+ if (unless_gpu_waits) {
+ gpu_waited |= system.GPU().HasSuspendedChannels();
+ if (gpu_waited &&
+ std::chrono::steady_clock::now() - start > std::chrono::milliseconds{3}) {
+ return false;
+ }
+ }
+ wake->cv.wait_for(lk, std::chrono::milliseconds{unless_gpu_waits ? 1 : 50});
}
}
+ if (unless_gpu_waits && !syncpoints.IsReadyGuest(fence_id, fence_value)) {
+ return false;
+ }
if (!syncpoints.IsReadyGuest(fence_id, fence_value) && m_wait_timeouts++ < 64) {
LOG_WARNING(Service, "nx2:vi2 present #{} fence ({}, {}) not reached in 4 s (now {})",
m_presents, fence_id, fence_value, syncpoints.GetGuestSyncpointValue(fence_id));
@@ -1207,14 +1901,18 @@ private:
std::chrono::steady_clock::now() - start)
.count());
}
+ return syncpoints.IsReadyGuest(fence_id, fence_value);
}
// Shows a presented native image: word 5 is the image id, ~word 22 / word 24 the GPU fence
// (syncpoint, value) to wait for (layout from changes.patch). The image is a block-linear
// RGBA8 surface in the console's native GOB layout at GPU address == CPU address.
- void Composite(u32 image_id, u32 fence_id, u32 fence_value) {
+ // shown_size: the present's word 7, ((height - 1) << 16) | (width - 1) of the area the game drew
+ // into from the image's top-left corner (0 when it gives none).
+ void Composite(u32 image_id, u32 fence_id, u32 fence_value, u32 shown_size) {
u64 address{};
Nx2ImageLayout layout{};
+ u32 image_format{};
{
std::scoped_lock lk{m_objects->mutex};
const auto image = m_objects->images.find(image_id);
@@ -1227,6 +1925,7 @@ private:
}
address = memory->second.address;
layout = Nx2CalculateImageLayout(image->second.preference.data());
+ image_format = image->second.preference[6];
}
const auto device = Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DeviceAddressOf(address);
if (!device) {
@@ -1236,6 +1935,16 @@ private:
}
u32 crop_width = layout.width;
u32 crop_height = layout.height;
+ // The present says how much of the image the game drew: Kirby Air Riders draws 1920x1080
+ // into 2560x1440 images, Mario Kart World 2560x1440 into 3840x2160 ones. Showing the whole
+ // image put the frame in the top-left corner.
+ const u32 shown_width = (shown_size & 0xFFFF) + 1, shown_height = (shown_size >> 16) + 1;
+ const bool has_shown_size = shown_size != 0 && shown_width <= layout.width &&
+ shown_height <= layout.height;
+ if (has_shown_size) {
+ crop_width = shown_width;
+ crop_height = shown_height;
+ }
// Handheld titles keep their docked present images (up to 3840x2160) and draw the frame
// into the top-left 1920x1080, the handheld panel's resolution; the console's display
// scans out only that region. Showing whole images put the frame in a corner inside black
@@ -1243,7 +1952,8 @@ private:
if (!Settings::IsDockedMode()) {
crop_width = (std::min)(crop_width, 1920U);
crop_height = (std::min)(crop_height, 1080U);
- } else if (system.GetApplicationProcessProgramID() == 0x0400C3F00006E000ULL &&
+ } else if (!has_shown_size &&
+ system.GetApplicationProcessProgramID() == 0x0400C3F00006E000ULL &&
layout.width == 3840 && layout.height == 2160) {
// Mario Kart World docked draws 2560x1440 into the top-left of its 4K images.
crop_width = 2560;
@@ -1265,8 +1975,9 @@ private:
(u64{crop_width} << 16) | crop_height;
if (shown != m_shown_layout) {
m_shown_layout = shown;
- LOG_INFO(Service, "nx2:vi2 composite image {} layout {}x{}, crop {}x{}",
- image_id, layout.width, layout.height, crop_width, crop_height);
+ LOG_INFO(Service,
+ "nx2:vi2 composite image {} layout {}x{}, drawn area {:#x}, crop {}x{}",
+ image_id, layout.width, layout.height, shown_size, crop_width, crop_height);
}
std::vector<Tegra::FramebufferConfig> layers{Tegra::FramebufferConfig{
.address = *device,
@@ -1274,7 +1985,9 @@ private:
.width = layout.width,
.height = layout.height,
.stride = static_cast<u32>(layout.pitch / 4),
- .pixel_format = android::PixelFormat::Rgba8888,
+ // Preference word 6: 1 = RGBA8, 4 = RGB10A2.
+ .pixel_format = image_format == 4 ? android::PixelFormat::Rgba1010102
+ : android::PixelFormat::Rgba8888,
.transform_flags = android::BufferTransformFlags::Unset,
.crop_rect = {0, 0, static_cast<int>(crop_width), static_cast<int>(crop_height)},
.blending = Tegra::BlendMode::Opaque,
@@ -1373,15 +2086,66 @@ private:
static_cast<u32>(AvailableSlots >> 32), 0, 0, 0, 0});
}
+ // nn::vi2::QueryDisplayRefreshRate(display name): u32 refresh rate in Hz, then a bool (a u64
+ // in all). nn::vi::detail::DefaultDisplay::QueryRefreshRate takes only -1, 60 or 120 and fails
+ // on anything else; Minecraft polls it and never draws while it fails. A one-word reply left
+ // the bool to whatever the message buffer held.
+ void QueryDisplayRefreshRate(HLERequestContext& ctx) {
+ TipcReplyWords(ctx, {60, 0});
+ }
+
+ // nn::vi2::QueryDisplayHdrPaperWhite(display name): u16 paper white in nits. The empty reply
+ // left it to the message buffer's leftovers; answer the BT.2408 reference white.
+ void QueryDisplayHdrPaperWhite(HLERequestContext& ctx) {
+ TipcReplyWords(ctx, {203});
+ }
+
+ // nn::vi2::QueryDisplayHdrStatus(display name): u64 HdrOutputFormat, u16. The sdk's
+ // DefaultDisplay::QueryHdrStatus takes format 1 as HDR output and fails with 0x272 on
+ // anything above 1. With the empty auto stub reply it read the request's leftover words as
+ // the format; Kirby Air Riders re-queries whenever its HDR status event fires and rebuilt its
+ // window images every few frames, which showed black. Report SDR output.
+ // nn::vi2::QueryDisplayHdrStatus(display name): u64 output format, u16 peak luminance in
+ // nits. The sdk takes format 1 as SDR and 2 as HDR and fails the query on any other value.
+ // Report SDR output with a typical SDR panel's peak.
+ void QueryDisplayHdrStatus(HLERequestContext& ctx) {
+ TipcReplyWords(ctx, {1, 0, 400});
+ }
+
+ // 212(u64 layer): u64 bits of the layer's buffer slots that are ready. The client polls it once
+ // a second until some are set; it only got going because the auto stub's reply happened to
+ // hold non-zero leftovers. Report the same slots 228/229 hand back.
+ void GetLayerBufferBits(HLERequestContext& ctx) {
+ constexpr u64 AvailableSlots = 0x7fff;
+ TipcReplyWords(ctx, {static_cast<u32>(AvailableSlots), static_cast<u32>(AvailableSlots >> 32)});
+ }
+
void ReturnOne(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) nx2:vi2 TIPC {}, returning 1", ctx.GetCommand());
TipcReplyWords(ctx, {1});
}
- // The same reply as the auto stub (success, no data), without its log lines.
- void SucceedQuietly(HLERequestContext& ctx) {
- IPC::ResponseBuilder rb{ctx, 2};
- rb.Push(ResultSuccess);
+ // The highest buffer resolution mode of the "default" display. Fire Emblem: Fortune's Weave picks
+ // its output from it: 1 -> 1280x720, 2 -> 1920x1080, 4 and 8 -> 2560x1440; 0 (the auto stub's
+ // reply), 3 and 5 fall back to 720p. Docked reports 4, handheld 2 (the screen is 1080p);
+ // EDEN_VI2_MAX_RESOLUTION_MODE=<n> overrides.
+ void GetBufferMaxResolution(HLERequestContext& ctx) {
+ const auto* packet = ctx.CommandBuffer();
+ const u64 name = u64{packet[2]} | (u64{packet[3]} << 32);
+ constexpr u64 DefaultDisplay = 0x00746c7561666564ULL; // "default"
+ Result result = Kernel::ResultInvalidCombination;
+ u64 mode{};
+ if (packet[0] == 0x7a && packet[1] == 2) {
+ result = Result{ErrorModule::VI, 7};
+ if (name == DefaultDisplay) {
+ static const char* forced = std::getenv("EDEN_VI2_MAX_RESOLUTION_MODE");
+ mode = forced ? std::strtoull(forced, nullptr, 0) : Settings::IsDockedMode() ? 4 : 2;
+ result = ResultSuccess;
+ }
+ }
+ IPC::ResponseBuilder rb{ctx, 4};
+ rb.Push(result.raw);
+ rb.Push(mode);
}
// Must be < 8; nn::vi::detail::LayerHistory creates 7 timestamp recorders per layer.
@@ -1400,17 +2164,105 @@ private:
rb.PushCopyObjects(ctx, event->GetReadableEvent());
}
- // 31(u32 kind) and 32(u32 display): events signalled on every vblank. Title 1's main thread
- // waits on the 32 event until it has fired twice before it submits any GPU work, and hung
- // there while it never fired.
- void GetVsyncEvent(HLERequestContext& ctx) {
- LOG_INFO(Service, "nx2:vi2 {} vsync event, [2]={:#x}", ctx.GetCommand(), ctx.CommandBuffer()[2]);
- auto* event = m_context.CreateEvent(fmt::format("nx2:vi2:Vsync{}", m_events.size()));
- m_events.push_back(event);
- {
- std::scoped_lock lk{m_vsync->mutex};
- m_vsync->events.push_back(event);
+ // 31(u32 index < 8): the session's event for that slot (the sdk picks a free index). The
+ // 261..272 commands bind a slot to a display event source and unbind it again. Until a slot
+ // is bound it fires on every vblank, as all of them used to.
+ void AcquireNativeEvent(HLERequestContext& ctx) {
+ const u32 index = ctx.CommandBuffer()[2];
+ if (index >= m_native_events.size()) {
+ LOG_ERROR(Service, "nx2:vi2 31 index {} out of range", index);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultUnknown);
+ return;
+ }
+ LOG_INFO(Service, "nx2:vi2 31 native event {}", index);
+ ReplyNativeEvent(ctx, index);
+ }
+
+ // 32(u32 index, copy handle): the client hands back its handle for an acquired slot and gets
+ // the slot's event again; the handle descriptor and handle come before the index. Games call
+ // it for slot 0 right after binding it to vsync. Title 1's main thread waits on this event
+ // until it has fired twice before it submits any GPU work, and hung there while it never
+ // fired.
+ void DuplicateNativeEvent(HLERequestContext& ctx) {
+ const u32* words = ctx.CommandBuffer();
+ u32 data = 2;
+ if (words[1] >> 31) {
+ const u32 descriptor = words[2];
+ data = 3 + ((descriptor >> 1) & 0xf) + ((descriptor >> 5) & 0xf) + (descriptor & 1 ? 2 : 0);
+ }
+ const u32 index = words[data];
+ if (index >= m_native_events.size()) {
+ LOG_ERROR(Service, "nx2:vi2 32 index {} out of range", index);
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultUnknown);
+ return;
+ }
+ LOG_INFO(Service, "nx2:vi2 32 native event {}", index);
+ ReplyNativeEvent(ctx, index);
+ }
+
+ // 261(u64 display name, u32 index): fire the slot on every vblank.
+ void BindVsync(HLERequestContext& ctx) {
+ const u32 index = ctx.CommandBuffer()[4];
+ LOG_INFO(Service, "nx2:vi2 261 native event {} -> vsync", index);
+ if (index < m_native_events.size() && m_native_events[index]) {
+ SetVsyncSlot(index, true);
+ }
+ TipcReplyWords(ctx, {});
+ }
+
+ // 263/265/267/269/271(u64 display name, u32 index): fire the slot when the display's refresh
+ // rate, HDR status, HDR paper white, buffer max resolution or mode capability changes, so the
+ // game re-queries it. None of them change here: fire once so the game reads the current
+ // state, then leave the slot quiet. Kirby Air Riders re-queried its HDR status on every
+ // present while this slot fired with vsync.
+ void BindChange(HLERequestContext& ctx) {
+ const u32 index = ctx.CommandBuffer()[4];
+ LOG_INFO(Service, "nx2:vi2 {} native event {} -> change notification", ctx.GetCommand(), index);
+ if (index < m_native_events.size() && m_native_events[index]) {
+ SetVsyncSlot(index, false);
+ m_native_events[index]->Signal(system.Kernel());
+ }
+ TipcReplyWords(ctx, {});
+ }
+
+ // 262..272 even (u64 display name, u32 index): detach the slot from its source.
+ void UnbindNativeEvent(HLERequestContext& ctx) {
+ const u32 index = ctx.CommandBuffer()[4];
+ LOG_INFO(Service, "nx2:vi2 {} native event {} unbound", ctx.GetCommand(), index);
+ if (index < m_native_events.size() && m_native_events[index]) {
+ SetVsyncSlot(index, false);
+ }
+ TipcReplyWords(ctx, {});
+ }
+
+ void ReplyNativeEvent(HLERequestContext& ctx, u32 index) {
+ auto*& event = m_native_events[index];
+ if (!event) {
+ event = m_context.CreateEvent(fmt::format("nx2:vi2:Native{}", index));
+ m_events.push_back(event);
+ SetVsyncSlot(index, true);
+ StartVsyncTimer();
+ }
+ IPC::ResponseBuilder rb{ctx, 2, 1, 0};
+ rb.Push(ResultSuccess);
+ rb.PushCopyObjects(ctx, event->GetReadableEvent());
+ }
+
+ void SetVsyncSlot(u32 index, bool on) {
+ auto* event = m_native_events[index];
+ std::scoped_lock lk{m_vsync->mutex};
+ auto& list = m_vsync->events;
+ const auto it = std::find(list.begin(), list.end(), event);
+ if (on && it == list.end()) {
+ list.push_back(event);
+ } else if (!on && it != list.end()) {
+ list.erase(it);
}
+ }
+
+ void StartVsyncTimer() {
if (!m_vsync_timer) {
m_vsync_timer = Core::Timing::CreateEvent(
"nx2:vi2 vsync",
@@ -1431,14 +2283,12 @@ private:
}()};
system.CoreTiming().ScheduleLoopingEvent(VblankInterval, VblankInterval, m_vsync_timer);
}
- IPC::ResponseBuilder rb{ctx, 2, 1, 0};
- rb.Push(ResultSuccess);
- rb.PushCopyObjects(ctx, event->GetReadableEvent());
}
KernelHelpers::ServiceContext m_context;
std::shared_ptr<Nx2NativeObjects> m_objects;
std::vector<Kernel::KEvent*> m_events;
+ std::array<Kernel::KEvent*, 8> m_native_events{}; ///< 31's slots, also listed in m_events
// Shared with the vsync timer callback, which can outlive the session by one call.
struct VsyncState {
std::mutex mutex;
@@ -1463,7 +2313,9 @@ inline bool Nx2FirstVideoCommand(std::string_view service, u32 command) {
// The Switch 2 video decoder (nn::codec::video::detail::ipc). gsv 902/906/1002 hand out a session
// whose TIPC 1 (CreateVideoDecoderService: handle at word 3, result at word 4) creates a decoder
// service, another session of this class. Layouts read from the SDK's nn::codec VP9 decoder:
-// - 200(copy handle: work buffer transfer memory, u64 size, u32, u32, u32): initialize.
+// - 200(copy handle: work buffer transfer memory, u64 size, u32, u32, u32): initialize. 201 adds a
+// fourth u32 (a flag that 200 leaves at 1) and 202 that and a u64: the same initialization.
+// 210(copy handle, u32 x4) replies only a result.
// - 101(A: codec name, "video/vp9\0"; u32 width, u32 height, u32 rate, u32): configure.
// - 3(): the submission-ready event (copy handle at word 3, result at word 4).
// - 6 / 207 / 8: start / stop / flush; 115: finalize.
@@ -1474,6 +2326,8 @@ inline bool Nx2FirstVideoCommand(std::string_view service, u32 command) {
// rewritten once that handle (the client's reads) has passed. Reply the image's next handle.
// - 212(u32 image id): unregister it.
// - 109(A: one compressed frame, u64 pts in us, u8 end of stream): submit input; reply a u64.
+// 400 is the same with a second A buffer after the frame, a 0x838-byte block of frame
+// information; the decoder here only needs the frame.
// - 213(): the next finished frame (16 words): u32 image id, u32 width, u32 height, u32 width,
// u32 height, u32 colour space, TimeSpan pts (ns), u8 end of stream.
// A NativeSyncHandle is {u32 SyncCounterId (~syncpoint), u32 mode, u64 value}; clients wait on it
@@ -1495,11 +2349,15 @@ public:
{109, &INx2VideoSession::SubmitInput, "SubmitInputBuffer"},
{115, &INx2VideoSession::Succeed, "Finalize"},
{200, &INx2VideoSession::Succeed, "Initialize"},
+ {201, &INx2VideoSession::Succeed, "InitializeWithFlag"},
+ {202, &INx2VideoSession::Succeed, "InitializeWithFlagAndValue"},
{207, &INx2VideoSession::Flush, "Stop"},
+ {210, &INx2VideoSession::Succeed, "Unknown210"},
{212, &INx2VideoSession::UnregisterBuffer, "UnregisterBuffer"},
{213, &INx2VideoSession::GetOutputInfo, "GetOutputInfo"},
{311, &INx2VideoSession::RegisterBuffer, "RegisterBuffer"},
{314, &INx2VideoSession::ReleaseBuffer, "ReleaseBuffer"},
+ {400, &INx2VideoSession::SubmitInput, "SubmitInputBufferWithInfo"},
});
}
@@ -1687,9 +2545,11 @@ private:
void SubmitInput(HLERequestContext& ctx) {
const auto* w = ctx.CommandBuffer();
- // Words 2-4 are the A descriptor of the compressed frame.
- const u64 pts = u64{w[5]} | (u64{w[6]} << 32);
- const bool end = (w[7] & 0xFF) != 0;
+ // Words 2-4 are the A descriptor of the compressed frame (400: 5-7 that of its information
+ // block), the data words follow.
+ const size_t words = 2 + 3 * ctx.BufferDescriptorA().size();
+ const u64 pts = u64{w[words]} | (u64{w[words + 1]} << 32);
+ const bool end = (w[words + 2] & 0xFF) != 0;
Log(ctx, "submit");
Input input{{}, pts, end};
if (!ctx.BufferDescriptorA().empty()) {
@@ -1847,6 +2707,7 @@ private:
const Decoded& frame, std::vector<u8>& bytes) {
u64 address{};
Nx2DecoderImageLayout layout{};
+ bool block_linear{};
{
std::scoped_lock lk{m_objects->mutex};
const auto image = m_objects->images.find(image_id);
@@ -1854,6 +2715,8 @@ private:
return {m_width, m_height};
}
layout = Nx2CalculateDecoderImageLayout(image->second.preference.data());
+ block_linear = image->second.gpu_read &&
+ layout.format != Tegra::Host1x::Nx2PictureFormat::Nv12;
const auto memory = m_objects->memories.find(image->second.memory);
if (memory != m_objects->memories.end() && memory->second.address != 0) {
address = memory->second.address + image->second.offset;
@@ -1867,10 +2730,25 @@ private:
const u32 width = std::min(frame.picture->width, layout.width);
const u32 height = std::min(frame.picture->height, layout.height);
bytes.resize(layout.size);
+ auto& memory = system.ApplicationMemory();
+ if (block_linear) {
+ // Read on the GPU: RGBA (the blit's format 0xd5) in the native block-linear layout,
+ // 16-GOB blocks, as our Eden tree writes these images.
+ decoder->Render(*frame.picture, Tegra::Host1x::Nx2PictureFormat::Rgba8, width, height,
+ layout.pitch, 0, bytes);
+ m_tiled.resize(layout.size);
+ Tegra::Texture::SwizzleTexture(m_tiled, bytes, 4, static_cast<u32>(layout.pitch / 4),
+ layout.height, 1, 4, 0, 1, true);
+ memory.WriteBlock(address, m_tiled.data(), m_tiled.size());
+ if (const auto device =
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DeviceAddressOf(address)) {
+ system.GPU().InvalidateRegion(*device, m_tiled.size());
+ }
+ return {width, height};
+ }
decoder->Render(*frame.picture, layout.format, width, height, layout.pitch,
layout.chroma_offset, bytes);
DumpFrame(bytes, layout, width, height);
- auto& memory = system.ApplicationMemory();
memory.WriteBlock(address, bytes.data(), layout.pitch * height);
if (layout.format == Tegra::Host1x::Nx2PictureFormat::Nv12) {
memory.WriteBlock(address + layout.chroma_offset, bytes.data() + layout.chroma_offset,
@@ -1916,6 +2794,7 @@ private:
u64 m_submitted{};
u32 m_syncpoint{NoSyncpoint}; ///< Counts finished frames (allocated at the first register)
u32 m_sync_base{}; ///< Its value before the first frame
+ std::vector<u8> m_tiled; ///< Decode thread: block-linear copy of a frame (WriteImage)
// Shared with the decode thread.
std::mutex m_mutex;
@@ -1959,6 +2838,7 @@ public:
{429, &INx2GraphicsSystemSession::CreateNativeImage, "CreateNativeImage"},
{430, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyNativeImage"},
{426, &INx2GraphicsSystemSession::QueryNativeImage, "MapNativeImage"},
+ {427, &INx2GraphicsSystemSession::UnmapNativeImage, "UnmapNativeImage"},
{428, &INx2GraphicsSystemSession::QueryNativeImage, "QueryNativeImage"},
{435, &INx2GraphicsSystemSession::CalculateNativeImageSize, "CalculateNativeImageSize"},
{1402, &INx2GraphicsSystemSession::OpenNvsrvSession, "OpenNvsrvSession (guessed)"},
@@ -1967,6 +2847,7 @@ public:
"CalculateDecoderImagePoolBuffer"},
{901, &INx2GraphicsSystemSession::Stub, "VideoInitialize (guessed)"},
{902, &INx2GraphicsSystemSession::OpenVideoSession, "OpenVideoDecoderSession (guessed)"},
+ {905, &INx2GraphicsSystemSession::OpenVideoSession, "OpenVideoDecoderSessionWithFlag"},
{906, &INx2GraphicsSystemSession::OpenVideoSession, "OpenVideoDecoderSessionWithVersion"},
{1001, &INx2GraphicsSystemSession::Stub, "VideoInitialize2 (guessed)"},
{1002, &INx2GraphicsSystemSession::OpenVideoSession, "OpenVideoDecoderSession2 (guessed)"},
@@ -1979,7 +2860,92 @@ public:
{1291, &INx2GraphicsSystemSession::CalculateChatImagePoolSize,
"CalculateChatImagePoolSize (guessed)"},
{911, &INx2GraphicsSystemSession::CreateDecoderImage, "CreateDecoderImage"},
+ // 912(u32 image id): the video group's x12, freeing one decoder output image (Switch
+ // Sports frees its four when its movie ends). Unlisting it stops the decode thread
+ // writing frames into memory the game reuses.
+ {912, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyDecoderImage"},
{961, &INx2GraphicsSystemSession::GetCpuImageDescriptor, "GetCpuUserImageDescriptor"},
+ // Firmware commands without a known meaning: zeroed replies.
+ {11, &INx2GraphicsSystemSession::RegisterClientHandle, "RegisterClientHandle"},
+ {113, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown113"},
+ {115, &INx2GraphicsSystemSession::QueryGrantedFeatures, "QueryGrantedFeatures"},
+ {120, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown120"},
+ {121, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown121"},
+ {122, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown122"},
+ {123, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown123"},
+ {124, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown124"},
+ {125, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown125"},
+ {126, &INx2GraphicsSystemSession::GetSessionFlag, "GetSessionFlag"},
+ {131, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown131"},
+ {133, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown133"},
+ {187, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown187"},
+ {201, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission201"},
+ {211, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown211"},
+ {212, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown212"},
+ {213, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown213"},
+ {214, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown214"},
+ {215, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown215"},
+ {301, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission301"},
+ {303, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown303"},
+ {311, &INx2GraphicsSystemSession::CreateNativeImage, "CreateFeatureImagePool311"},
+ {312, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyFeatureImagePool312"},
+ {321, &INx2GraphicsSystemSession::CreateNativeImage, "CreateFeatureImagePool321"},
+ {361, &INx2GraphicsSystemSession::GetCpuImageDescriptor, "GetFeatureCpuImageDescriptor361"},
+ {391, &INx2GraphicsSystemSession::CalculateChatImagePoolSize, "CalculateFeatureImagePoolSize391"},
+ {401, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission401"},
+ {402, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission402"},
+ {403, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission403"},
+ {424, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown424"},
+ {425, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown425"},
+ {427, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown427"},
+ {434, &INx2GraphicsSystemSession::AttachGpuAddressSpace, "AttachGpuAddressSpace"},
+ {436, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown436"},
+ {461, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown461"},
+ {462, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown462"},
+ {463, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown463"},
+ {464, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown464"},
+ {465, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown465"},
+ {466, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown466"},
+ {467, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown467"},
+ {468, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown468"},
+ {493, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown493"},
+ {494, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission494"},
+ {603, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown603"},
+ {612, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyFeatureImagePool612"},
+ {613, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown613"},
+ {614, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown614"},
+ {615, &INx2GraphicsSystemSession::CreateNativeImage, "CreateFeatureImagePool615"},
+ {691, &INx2GraphicsSystemSession::CalculateChatImagePoolSize, "CalculateFeatureImagePoolSize691"},
+ {801, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission801"},
+ {802, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown802"},
+ {803, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown803"},
+ {811, &INx2GraphicsSystemSession::CreateNativeImage, "CreateFeatureImagePool811"},
+ {812, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyFeatureImagePool812"},
+ {813, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown813"},
+ {814, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown814"},
+ {861, &INx2GraphicsSystemSession::GetCpuImageDescriptor, "GetFeatureCpuImageDescriptor861"},
+ {891, &INx2GraphicsSystemSession::CalculateChatImagePoolSize, "CalculateFeatureImagePoolSize891"},
+ {1003, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1003"},
+ {1011, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1011"},
+ {1012, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1012"},
+ {1091, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1091"},
+ {1101, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission1101"},
+ {1102, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission1102"},
+ {1111, &INx2GraphicsSystemSession::SetFeatureVersion, "SetFeatureVersion"},
+ {1112, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1112"},
+ {1113, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1113"},
+ {1121, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1121"},
+ {1203, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1203"},
+ {1212, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyFeatureImagePool1212"},
+ {1501, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission1501"},
+ {1502, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission1502"},
+ {1503, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission1503"},
+ {1601, &INx2GraphicsSystemSession::CheckPermission, "CheckPermission1601"},
+ {1602, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1602"},
+ {1603, &INx2GraphicsSystemSession::StubReplyZeroed, "Unknown1603"},
+ {1611, &INx2GraphicsSystemSession::CreateNativeImage, "CreateFeatureImagePool1611"},
+ {1612, &INx2GraphicsSystemSession::DestroyNativeImage, "DestroyFeatureImagePool1612"},
+ {1691, &INx2GraphicsSystemSession::CalculateChatImagePoolSize, "CalculateFeatureImagePoolSize1691"},
});
}
@@ -1995,6 +2961,56 @@ public:
}
private:
+ // Feature permission checks (201, 301, 401-403, 1101, 1102, 1501-1503): each tests one bit of
+ // the permissions the session was opened with and fails when it is clear. Games only ask for
+ // features they are granted.
+ void CheckPermission(HLERequestContext& ctx) {
+ LOG_DEBUG(Service, "nx2:gsv TIPC {} permission check", ctx.GetCommand());
+ TipcReplyWords(ctx, {});
+ }
+
+ // 115(u64 requested feature flags): the subset that is granted, which is all of them here.
+ // A zeroed reply would deny every feature.
+ void QueryGrantedFeatures(HLERequestContext& ctx) {
+ const auto* w = ctx.CommandBuffer();
+ LOG_INFO(Service, "nx2:gsv TIPC 115 features {:#x}", u64{w[2]} | (u64{w[3]} << 32));
+ TipcReplyWords(ctx, {w[2], w[3]});
+ }
+
+ // 126: one bit of the session's state (meaning unknown); clear.
+ void GetSessionFlag(HLERequestContext& ctx) {
+ TipcReplyWords(ctx, {0});
+ }
+
+ // 434(u32 id from nx2:gpu 33): the session takes a reference on that GPU address space.
+ void AttachGpuAddressSpace(HLERequestContext& ctx) {
+ const u32 id = ctx.CommandBuffer()[2];
+ LOG_INFO(Service, "nx2:gsv TIPC 434 address space {:#x}", id);
+ m_address_spaces.insert(id);
+ TipcReplyWords(ctx, {});
+ }
+
+ // 11(copy handle): handed over once per session and kept; a second call fails.
+ void RegisterClientHandle(HLERequestContext& ctx) {
+ if (m_client_handle_registered) {
+ LOG_WARNING(Service, "nx2:gsv TIPC 11 called twice");
+ IPC::ResponseBuilder rb{ctx, 2};
+ rb.Push(ResultUnknown);
+ return;
+ }
+ m_client_handle_registered = true;
+ TipcReplyWords(ctx, {});
+ }
+
+ // 1111(u32): a version or mode number the server checks and records.
+ void SetFeatureVersion(HLERequestContext& ctx) {
+ LOG_INFO(Service, "nx2:gsv TIPC 1111 value {:#x}", ctx.CommandBuffer()[2]);
+ TipcReplyWords(ctx, {});
+ }
+
+ std::set<u32> m_address_spaces; ///< Ids attached with 434
+ bool m_client_handle_registered = false; ///< Set by 11
+
// Index of the first raw data word of a TIPC request: header (2 words), then an optional
// special header with pid and copy/move handle words.
static size_t TipcDataOffset(const u32* cmd_buf) {
@@ -2018,7 +3034,9 @@ private:
// and InitializeVp9Decoder failed with 2001-0114 (title 1 aborts when a movie starts).
// Newer SDKs (nn::codec::video::detail::InitializeServerContext) send 906(u8, u64 decoder
// library version) instead of 902; title 4 sends version 0x3'00030000 and aborted with
- // InvalidHandle at its first movie while 906 was auto-stubbed.
+ // InvalidHandle at its first movie while 906 was auto-stubbed. The SDK of later titles
+ // (ipc::Initialize(bool)) sends 905(u8) instead of 902 when its flag is set; the reply is the
+ // same. 1001/1002 open the encoder's creator session (CreateVideoEncoderService uses it).
void OpenVideoSession(HLERequestContext& ctx) {
const auto* w = ctx.CommandBuffer();
const bool second = ctx.GetCommand() == 1002;
@@ -2031,13 +3049,11 @@ private:
// nn::camera::sdk::CreateCameraContext sends 601() then 602(u64 1), which returns the camera
// context session (handle at word 3, result at word 4). Auto-stubbed, 602 had no handle, the
- // SDK sent 603 (destroy) and title 1 (1.8.0) aborted with 2034-0000. Its commands fall
- // through to the nx2 auto stub.
+ // SDK sent 603 (destroy) and title 1 (1.8.0) aborted with 2034-0000. Its commands are
+ // answered by INx2CameraSession.
void OpenCameraSession(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) nx2:gsv TIPC 602, returning a nx2:camera session");
- ReturnRawSession(system, ctx,
- std::make_shared<INx2StubInterface>(
- system, INx2StubInterface::InternName("nx2:camera")));
+ ReturnRawSession(system, ctx, std::make_shared<INx2CameraSession>(system));
}
// nn::chat::gsv::Initialize sends 1201 then 1202, the same pair as 901/902 and 1001/1002.
@@ -2223,8 +3239,19 @@ private:
void DestroyNativeMemory(HLERequestContext& ctx) {
const u32 id = ctx.CommandBuffer()[2];
- std::scoped_lock lk{m_objects->mutex};
- m_objects->memories.erase(id);
+ std::vector<std::optional<std::pair<u64, u64>>> unmaps;
+ {
+ std::scoped_lock lk{m_objects->mutex};
+ for (auto& [image_id, image] : m_objects->images) {
+ if (image.memory == id && image.mapped_here) {
+ unmaps.push_back(TakeDecoderMapping(image));
+ }
+ }
+ m_objects->memories.erase(id);
+ }
+ for (const auto& range : unmaps) {
+ DropDecoderMapping(range);
+ }
LOG_DEBUG(Service, "nx2:gsv 112 DestroyNativeMemory id {}", id);
TipcReplyWords(ctx, {});
}
@@ -2274,6 +3301,8 @@ private:
TipcReplyWords(ctx, {id});
}
+ // Pool size of a feature's images (1291 here; 391, 691, 891 and 1691 for the other feature
+ // groups, which share the server template).
// nn::chat::gsv::CalculateRequiredBufferMemory: 1291(preference[16], u32 count), the request
// of 1510, replies result, u64 size, u64 alignment. Auto-stubbed, the size became 0x1000 and
// the camera pool (1211) got memory for none of its images.
@@ -2299,6 +3328,9 @@ private:
// the 429 preference words (0x10, 0x14), a guess.
void QueryNativeImage(HLERequestContext& ctx) {
const u32 handle = ctx.CommandBuffer()[2];
+ if (QueryDecoderImage(ctx, handle)) {
+ return;
+ }
std::array<u32, 16> preference{};
u64 image_offset = 0;
u64 memory_address = 0;
@@ -2343,6 +3375,108 @@ private:
}
}
+ // 428/426 for a decoder output image (Fire Emblem: Fortune's Weave's movie player blits each
+ // one, block-linear RGBA, format 0xd5, block height 16 GOBs, into a texture of its own). The
+ // reply follows our Eden tree, where the movies play: words 3-4 the GPU address, 5-6 the last
+ // byte's address, then the descriptor {1, 0, dimensions, .., 0x01000001, 1, offset, size}
+ // (offset 0 once mapped). 426 maps the image at its own address for the GPU: decoder pool
+ // memory is process memory (114) that no GPU mapping covers, and the blit read zeros.
+ bool QueryDecoderImage(HLERequestContext& ctx, u32 handle) {
+ const bool map = ctx.GetCommand() == 426;
+ u64 address{};
+ u64 size{};
+ u64 offset{};
+ u32 dimensions{};
+ bool register_mapping = false;
+ {
+ std::scoped_lock lk{m_objects->mutex};
+ const auto image = m_objects->images.find(handle);
+ if (image == m_objects->images.end() || !image->second.decoder) {
+ return false;
+ }
+ const auto memory = m_objects->memories.find(image->second.memory);
+ if (memory == m_objects->memories.end() || memory->second.address == 0) {
+ return false;
+ }
+ image->second.gpu_read = true;
+ offset = image->second.offset;
+ size = image->second.size;
+ dimensions = image->second.preference[2];
+ if (map) {
+ address = memory->second.address + offset;
+ register_mapping = image->second.gpu_maps++ == 0 &&
+ !Nvidia::Devices::Nx2GpuMemoryRegistry::Get().DeviceAddressOf(address);
+ image->second.mapped_here |= register_mapping;
+ }
+ }
+ if (register_mapping) {
+ Nvidia::Devices::Nx2GpuMemoryRegistry::Get().Register(address, size);
+ }
+ LOG_INFO(Service, "nx2:gsv {} decoder image {} at {:#x}+{:#x}{}", ctx.GetCommand(), handle,
+ address, size, register_mapping ? ", mapped for the GPU" : "");
+ std::array<u32, 37> words{};
+ if (map) {
+ const u64 last = address + size - 1;
+ words[0] = static_cast<u32>(address);
+ words[1] = static_cast<u32>(address >> 32);
+ words[2] = static_cast<u32>(last);
+ words[3] = static_cast<u32>(last >> 32);
+ }
+ u32* const desc = words.data() + 4;
+ desc[0] = 1;
+ desc[2] = dimensions;
+ desc[8] = 0x01000001;
+ desc[9] = 1;
+ desc[10] = map ? 0 : static_cast<u32>(offset);
+ desc[11] = static_cast<u32>(size);
+ IPC::ResponseBuilder rb{ctx, static_cast<u32>(2 + words.size())};
+ rb.PushRaw<u32>(ResultSuccess.raw);
+ for (const u32 w : words) {
+ rb.Push<u32>(w);
+ }
+ return true;
+ }
+
+ // 427(u32 handle, u32 address space, u64 address, u64 last address): undoes a 426. The last
+ // unmap of a decoder image drops the GPU mapping 426 made.
+ void UnmapNativeImage(HLERequestContext& ctx) {
+ const auto* w = ctx.CommandBuffer();
+ const u32 handle = w[2];
+ std::optional<std::pair<u64, u64>> unmap;
+ {
+ std::scoped_lock lk{m_objects->mutex};
+ if (const auto image = m_objects->images.find(handle);
+ image != m_objects->images.end() && image->second.gpu_maps != 0 &&
+ --image->second.gpu_maps == 0) {
+ unmap = TakeDecoderMapping(image->second);
+ }
+ }
+ LOG_DEBUG(Service, "nx2:gsv 427 image {} [{:#x}, {:#x}]", handle, Pair(w, 4), Pair(w, 6));
+ DropDecoderMapping(unmap);
+ TipcReplyWords(ctx, {});
+ }
+
+ // The range 426 mapped for an image, if it did; called with m_objects->mutex held.
+ std::optional<std::pair<u64, u64>> TakeDecoderMapping(Nx2NativeObjects::Image& image) {
+ image.gpu_maps = 0;
+ if (!std::exchange(image.mapped_here, false)) {
+ return std::nullopt;
+ }
+ const auto memory = m_objects->memories.find(image.memory);
+ if (memory == m_objects->memories.end() || memory->second.address == 0) {
+ return std::nullopt;
+ }
+ return std::pair{memory->second.address + image.offset, image.size};
+ }
+
+ static void DropDecoderMapping(const std::optional<std::pair<u64, u64>>& range) {
+ if (range) {
+ auto& registry = Nvidia::Devices::Nx2GpuMemoryRegistry::Get();
+ registry.Unmap(range->first, range->second);
+ registry.ReleaseBacking(range->first, range->second);
+ }
+ }
+
// 911(preference[16], u32 count, u32 memory id) (nn::codec::InitializeDecoderImagePool):
// result, the id of the first of `count` decoder output images; the client names image i of
// the pool id + i. They share the pool's memory, one image size apart.
@@ -2401,8 +3535,15 @@ private:
void DestroyNativeImage(HLERequestContext& ctx) {
const u32 id = ctx.CommandBuffer()[2];
- std::scoped_lock lk{m_objects->mutex};
- m_objects->images.erase(id);
+ std::optional<std::pair<u64, u64>> unmap;
+ {
+ std::scoped_lock lk{m_objects->mutex};
+ if (const auto image = m_objects->images.find(id); image != m_objects->images.end()) {
+ unmap = TakeDecoderMapping(image->second);
+ m_objects->images.erase(image);
+ }
+ }
+ DropDecoderMapping(unmap);
TipcReplyWords(ctx, {});
}
diff --git a/src/core/hle/service/ro/ro.cpp b/src/core/hle/service/ro/ro.cpp
index 335efa3ad3..2af4f1cf58 100644
--- a/src/core/hle/service/ro/ro.cpp
+++ b/src/core/hle/service/ro/ro.cpp
@@ -356,6 +356,22 @@ public:
// Read NRR.
NrrHeader header{};
process->GetMemory().ReadBlock(nrr_address, std::addressof(header), sizeof(header));
+ size_t hashes_offset = header.GetHashesOffset();
+ u32 num_hashes = header.GetNumHashes();
+ if (process->IsSwitch2()) {
+ // Switch 2 NRRs carry 3072-bit keys and signatures: the fields after them (program
+ // id, size, kind, hash list offset and count) sit 0x180 bytes later, the hash list
+ // offset and count at 0x4c0.
+ std::array<u32, 2> fields{};
+ process->GetMemory().ReadBlock(nrr_address + 0x4c0, fields.data(), sizeof(fields));
+ hashes_offset = fields[0];
+ num_hashes = fields[1];
+ }
+ // The hash list lies inside the NRR (read as a Switch 1 header, a Switch 2 one asked for
+ // 51 GB of hashes).
+ R_UNLESS(hashes_offset <= nrr_size &&
+ num_hashes <= (nrr_size - hashes_offset) / sizeof(Sha256Hash),
+ RO::ResultInvalidNrr);
// Set NRR info.
context->SetNrrInfoInUse(nrr_info, true);
@@ -363,10 +379,9 @@ public:
nrr_info->nrr_heap_size = nrr_size;
// Read NRR hash list.
- nrr_info->hashes.resize(header.GetNumHashes());
- process->GetMemory().ReadBlock(nrr_address + header.GetHashesOffset(),
- nrr_info->hashes.data(),
- sizeof(Sha256Hash) * header.GetNumHashes());
+ nrr_info->hashes.resize(num_hashes);
+ process->GetMemory().ReadBlock(nrr_address + hashes_offset, nrr_info->hashes.data(),
+ sizeof(Sha256Hash) * num_hashes);
R_SUCCEED();
}
diff --git a/src/core/hle/service/service.cpp b/src/core/hle/service/service.cpp
index 1490af0328..b47d67468b 100644
--- a/src/core/hle/service/service.cpp
+++ b/src/core/hle/service/service.cpp
@@ -9,6 +9,10 @@
#include <tuple>
#include <mutex>
#include <map>
+#include <memory>
+#include <set>
+#include <string>
+#include <string_view>
#include <chrono>
#include <algorithm>
#include <cstdlib>
@@ -18,8 +22,10 @@
#include "common/settings.h"
#include "core/core.h"
#include "core/hle/ipc.h"
+#include "core/hle/kernel/k_event.h"
#include "core/hle/kernel/kernel.h"
#include "core/hle/service/ipc_helpers.h"
+#include "core/hle/service/kernel_helpers.h"
#include "core/hle/service/service.h"
#include "core/hle/service/nx2_stub_interface.h"
#include "core/hle/service/sm/sm.h"
@@ -49,6 +55,75 @@ ServiceFrameworkBase::ServiceFrameworkBase(Core::System& system_, const char* se
ServiceFrameworkBase::~ServiceFrameworkBase() {
// Wait for other threads to release access before destroying
const auto guard = ServiceFrameworkBase::LockService();
+ if (stub_event) {
+ stub_context->CloseEvent(stub_event);
+ }
+}
+
+namespace {
+
+// Callers read the reply words they expect without checking how many were written, so an empty
+// reply hands them the request's leftover words as data (an HDR output format, a refresh rate, a
+// device id). Stub replies carry this many zeroed words.
+constexpr u32 StubZeroedWords = 32;
+
+void PushStubZeroes(IPC::ResponseBuilder& rb) {
+ for (u32 i = 0; i < StubZeroedWords; ++i) {
+ rb.Push<u32>(0);
+ }
+}
+
+} // namespace
+
+bool ServiceFrameworkBase::StubAllowed(HLERequestContext& ctx) {
+ static const bool env_auto_stub = std::getenv("EDEN_NX2_AUTO_STUB") != nullptr;
+ const auto* const caller = ctx.GetThread().GetOwnerProcess();
+ if (Settings::values.use_auto_stub || env_auto_stub ||
+ std::string_view{service_name}.starts_with(Nx2StubPrefix) ||
+ (caller != nullptr && caller->IsSwitch2())) {
+ // Logged once per command: these are still unimplemented.
+ static std::mutex mutex;
+ static std::set<std::pair<std::string_view, u32>> logged;
+ std::scoped_lock lk{mutex};
+ if (logged.emplace(service_name, ctx.GetCommand()).second) {
+ LOG_WARNING(Service, "(STUBBED) {}::{} (placeholder reply)", service_name, ctx.GetCommand());
+ }
+ return true;
+ }
+ const auto& table = ctx.IsTipc() ? handlers_tipc : handlers;
+ const auto it = table.find(ctx.GetCommand());
+ ReportUnimplementedFunction(ctx, it == table.end() ? nullptr : &it->second);
+ return false;
+}
+
+void ServiceFrameworkBase::StubReplyZeroed(HLERequestContext& ctx) {
+ if (!StubAllowed(ctx)) {
+ return;
+ }
+ IPC::ResponseBuilder rb{ctx, 2 + StubZeroedWords};
+ rb.Push(ResultSuccess);
+ PushStubZeroes(rb);
+}
+
+void ServiceFrameworkBase::StubReplyEvent(HLERequestContext& ctx) {
+ if (!StubAllowed(ctx)) {
+ return;
+ }
+ if (!stub_event) {
+ stub_context = std::make_unique<KernelHelpers::ServiceContext>(system, service_name);
+ stub_event = stub_context->CreateEvent(fmt::format("{}:StubEvent", service_name));
+ }
+ IPC::ResponseBuilder rb{ctx, 2 + StubZeroedWords, 1, 0};
+ rb.Push(ResultSuccess);
+ PushStubZeroes(rb);
+ rb.PushCopyObjects(ctx, stub_event->GetReadableEvent());
+}
+
+void ServiceFrameworkBase::StubReplyInterface(HLERequestContext& ctx) {
+ if (!StubAllowed(ctx)) {
+ return;
+ }
+ ReturnNx2StubInterface(system, ctx, service_name);
}
void ServiceFrameworkBase::RegisterHandlersBase(const FunctionInfoBase* functions, std::size_t n) {
@@ -67,6 +142,27 @@ void ServiceFrameworkBase::RegisterHandlersBaseTipc(const FunctionInfoBase* func
void ServiceFrameworkBase::ReportUnimplementedFunction(HLERequestContext& ctx,
const FunctionInfoBase* info) {
+ // EDEN_NX2_AUTO_STUB=1 auto-stubs every unknown command without touching the config. Switch 2
+ // programs are always auto-stubbed: they still call many commands nobody has implemented.
+ static const bool env_auto_stub = std::getenv("EDEN_NX2_AUTO_STUB") != nullptr;
+ const auto* const caller = ctx.GetThread().GetOwnerProcess();
+ const bool auto_stub = Settings::values.use_auto_stub || env_auto_stub ||
+ std::string_view{service_name}.starts_with(Nx2StubPrefix) ||
+ (caller != nullptr && caller->IsSwitch2());
+ // Auto-stubbed commands are reported once each: title 1 polls nx2:camera 29 every frame
+ // (~47 calls a second), and every report wrote a Critical line, which flushes the log file.
+ if (auto_stub) {
+ static std::mutex reported_mutex;
+ static std::set<std::pair<std::string, u32>> reported;
+ std::scoped_lock lk{reported_mutex};
+ if (!reported.emplace(service_name, ctx.GetCommand()).second) {
+ IPC::ResponseBuilder rb{ctx, 2 + StubZeroedWords};
+ rb.Push(ResultSuccess);
+ PushStubZeroes(rb);
+ return;
+ }
+ }
+
auto cmd_buf = ctx.CommandBuffer();
std::string function_name = info == nullptr ? "<unknown>" : info->name;
@@ -80,16 +176,11 @@ void ServiceFrameworkBase::ReportUnimplementedFunction(HLERequestContext& ctx,
system.GetReporter().SaveUnimplementedFunctionReport(ctx, ctx.GetCommand(), function_name, service_name);
UNIMPLEMENTED_MSG("Unknown / unimplemented {}", fmt::to_string(buf));
- // EDEN_NX2_AUTO_STUB=1 auto-stubs every unknown command without touching the config. Switch 2
- // programs are always auto-stubbed: they still call many commands nobody has implemented.
- static const bool env_auto_stub = std::getenv("EDEN_NX2_AUTO_STUB") != nullptr;
- const auto* const caller = ctx.GetThread().GetOwnerProcess();
- if (Settings::values.use_auto_stub || env_auto_stub ||
- std::string_view{service_name}.starts_with(Nx2StubPrefix) ||
- (caller != nullptr && caller->IsSwitch2())) {
- LOG_WARNING(Service, "Using auto stub fallback!");
- IPC::ResponseBuilder rb{ctx, 2};
+ if (auto_stub) {
+ LOG_WARNING(Service, "Using auto stub fallback (repeats of this command are not logged)");
+ IPC::ResponseBuilder rb{ctx, 2 + StubZeroedWords};
rb.Push(ResultSuccess);
+ PushStubZeroes(rb);
}
}
diff --git a/src/core/hle/service/service.h b/src/core/hle/service/service.h
index 68b0d636d8..d1be62c8c7 100644
--- a/src/core/hle/service/service.h
+++ b/src/core/hle/service/service.h
@@ -7,6 +7,7 @@
#pragma once
#include <cstddef>
+#include <memory>
#include <mutex>
#include "common/container/unordered_map.h"
#include "common/common_types.h"
@@ -20,12 +21,17 @@ class System;
}
namespace Kernel {
+class KEvent;
class KServerSession;
class ServiceThread;
} // namespace Kernel
namespace Service {
+namespace KernelHelpers {
+class ServiceContext;
+}
+
namespace FileSystem {
class FileSystemController;
}
@@ -77,6 +83,24 @@ protected:
[[nodiscard]] virtual std::unique_lock<std::mutex> LockService() noexcept {
return std::unique_lock{lock_service};
}
+
+ /// Placeholder handlers for commands whose reply shape is known but whose meaning is not, for
+ /// Switch 2 callers (others get the unimplemented-function path, as with nullptr):
+ /// StubReplyZeroed replies with zeroed data words, StubReplyEvent adds a copy handle to an
+ /// event that never fires, and StubReplyInterface returns an nx2: stub interface whose
+ /// commands are auto-stubbed.
+ void StubReplyZeroed(HLERequestContext& ctx);
+ void StubReplyEvent(HLERequestContext& ctx);
+ void StubReplyInterface(HLERequestContext& ctx);
+
+private:
+ /// False (after reporting the command as unimplemented) for callers the stubs don't serve.
+ bool StubAllowed(HLERequestContext& ctx);
+
+ /// Event handed out by StubReplyEvent, created on first use.
+ Kernel::KEvent* stub_event{};
+ std::unique_ptr<KernelHelpers::ServiceContext> stub_context;
+
private:
template <typename T>
friend class ServiceFramework;
diff --git a/src/core/hle/service/set/settings_server.cpp b/src/core/hle/service/set/settings_server.cpp
index 942dddc9bc..78847ba082 100644
--- a/src/core/hle/service/set/settings_server.cpp
+++ b/src/core/hle/service/set/settings_server.cpp
@@ -102,9 +102,14 @@ ISettingsServer::ISettingsServer(Core::System& system_) : ServiceFramework{syste
{7, C<&ISettingsServer::GetKeyCodeMap>, "GetKeyCodeMap"},
{8, C<&ISettingsServer::GetQuestFlag>, "GetQuestFlag"},
{9, C<&ISettingsServer::GetKeyCodeMap2>, "GetKeyCodeMap2"},
- {10, nullptr, "GetFirmwareVersionForDebug"},
+ {10, &ISettingsServer::StubReplyZeroed, "GetFirmwareVersionForDebug"},
{11, C<&ISettingsServer::GetDeviceNickName>, "GetDeviceNickName"},
{12, C<&ISettingsServer::GetKeyCodeMapByPort>, "GetKeyCodeMapByPort"},
+ {13, &ISettingsServer::StubReplyZeroed, "Unknown13"},
+ {14, &ISettingsServer::StubReplyZeroed, "Unknown14"},
+ {15, &ISettingsServer::StubReplyZeroed, "Unknown15"},
+ {16, &ISettingsServer::StubReplyZeroed, "Unknown16"},
+ {17, &ISettingsServer::StubReplyZeroed, "Unknown17"},
};
// clang-format on
diff --git a/src/core/hle/service/sockets/bsd.cpp b/src/core/hle/service/sockets/bsd.cpp
index 0137a3be66..02c6bb7762 100644
--- a/src/core/hle/service/sockets/bsd.cpp
+++ b/src/core/hle/service/sockets/bsd.cpp
@@ -1089,10 +1089,10 @@ BSD_USA::BSD_USA(Core::System& system_, const char* name, bool is_user_)
{1, &BSD_USA::StartMonitoring, "StartMonitoring"},
{2, &BSD_USA::Socket, "Socket"},
{3, &BSD_USA::SocketExempt, "SocketExempt"},
- {4, nullptr, "Open"},
+ {4, &BSD_USA::StubReplyZeroed, "Open"},
{5, &BSD_USA::Select, "Select"},
{6, &BSD_USA::Poll, "Poll"},
- {7, nullptr, "Sysctl"},
+ {7, &BSD_USA::StubReplyZeroed, "Sysctl"},
{8, &BSD_USA::Recv, "Recv"},
{9, &BSD_USA::RecvFrom, "RecvFrom"},
{10, &BSD_USA::Send, "Send"},
@@ -1104,33 +1104,37 @@ BSD_USA::BSD_USA(Core::System& system_, const char* name, bool is_user_)
{16, &BSD_USA::GetSockName, "GetSockName"},
{17, &BSD_USA::GetSockOpt, "GetSockOpt"},
{18, &BSD_USA::Listen, "Listen"},
- {19, nullptr, "Ioctl"},
+ {19, &BSD_USA::StubReplyZeroed, "Ioctl"},
{20, &BSD_USA::Fcntl, "Fcntl"},
{21, &BSD_USA::SetSockOpt, "SetSockOpt"},
{22, &BSD_USA::Shutdown, "Shutdown"},
- {23, nullptr, "ShutdownAllSockets"},
+ {23, &BSD_USA::StubReplyZeroed, "ShutdownAllSockets"},
{24, &BSD_USA::Write, "Write"},
{25, &BSD_USA::Read, "Read"},
{26, &BSD_USA::Close, "Close"},
{27, &BSD_USA::DuplicateSocket, "DuplicateSocket"},
- {28, nullptr, "GetResourceStatistics"},
- {29, nullptr, "RecvMMsg"}, //3.0.0+
- {30, nullptr, "SendMMsg"}, //3.0.0+
+ {28, &BSD_USA::StubReplyZeroed, "GetResourceStatistics"},
+ {29, &BSD_USA::StubReplyZeroed, "RecvMMsg"}, //3.0.0+
+ {30, &BSD_USA::StubReplyZeroed, "SendMMsg"}, //3.0.0+
{31, &BSD_USA::EventFd, "EventFd"}, //7.0.0+
- {32, nullptr, "RegisterResourceStatisticsName"}, //7.0.0+
- {33, nullptr, "RegisterClientShared"}, //10.0.0+
- {34, nullptr, "GetSocketStatistics"}, //15.0.0+
- {35, nullptr, "NifIoctl"}, //17.0.0+
- {36, nullptr, "Unknown36"}, //18.0.0+
- {37, nullptr, "Unknown37"}, //18.0.0+
- {38, nullptr, "Unknown38"}, //18.0.0+
- {39, nullptr, "Unknown39"}, //20.0.0+
- {40, nullptr, "Unknown40"}, //20.0.0+
- {41, nullptr, "Unknown41"}, //21.0.0+
- {42, nullptr, "Unknown42"}, //21.0.0+
- {43, nullptr, "Unknown43"}, //21.0.0+
- {200, nullptr, "SetThreadCoreMask"}, //15.0.0+
- {201, nullptr, "GetThreadCoreMask"}, //15.0.0+
+ {32, &BSD_USA::StubReplyZeroed, "RegisterResourceStatisticsName"}, //7.0.0+
+ {33, &BSD_USA::StubReplyZeroed, "RegisterClientShared"}, //10.0.0+
+ {34, &BSD_USA::StubReplyZeroed, "GetSocketStatistics"}, //15.0.0+
+ {35, &BSD_USA::StubReplyZeroed, "NifIoctl"}, //17.0.0+
+ // Switch 2 titles register with 36 (16 bytes and the pid; a bsd errno back) instead of
+ // RegisterClient. The auto stub left the errno to the message buffer's leftovers, and
+ // Minecraft's socket layer failed later in EventFd.
+ {36, &BSD_USA::RegisterClient, "RegisterClientSwitch2"}, //18.0.0+
+ {37, &BSD_USA::StubReplyZeroed, "Unknown37"}, //18.0.0+
+ {38, &BSD_USA::StubReplyZeroed, "Unknown38"}, //18.0.0+
+ {39, &BSD_USA::StubReplyZeroed, "Unknown39"}, //20.0.0+
+ {40, &BSD_USA::StubReplyZeroed, "Unknown40"}, //20.0.0+
+ {41, &BSD_USA::StubReplyEvent, "Unknown41"}, //21.0.0+
+ {42, &BSD_USA::StubReplyZeroed, "Unknown42"}, //21.0.0+
+ {43, &BSD_USA::StubReplyZeroed, "Unknown43"}, //21.0.0+
+ {200, &BSD_USA::StubReplyZeroed, "SetThreadCoreMask"}, //15.0.0+
+ {201, &BSD_USA::StubReplyZeroed, "GetThreadCoreMask"}, //15.0.0+
+ {44, &BSD_USA::StubReplyZeroed, "Unknown44"},
};
// clang-format on
@@ -1158,22 +1162,28 @@ BSDCFG::BSDCFG(Core::System& system_, const char *name)
: ServiceFramework{system_, name} {
// clang-format off
static const FunctionInfo functions[] = {
- {0, nullptr, "SetIfUp"},
- {1, nullptr, "SetIfUpWithEvent"},
- {2, nullptr, "CancelIf"},
- {3, nullptr, "SetIfDown"},
- {4, nullptr, "GetIfState"},
- {5, nullptr, "DhcpRenew"},
- {6, nullptr, "AddStaticArpEntry"},
- {7, nullptr, "RemoveArpEntry"},
- {8, nullptr, "LookupArpEntry"},
- {9, nullptr, "LookupArpEntry2"},
- {10, nullptr, "ClearArpEntries"},
- {11, nullptr, "ClearArpEntries2"},
- {12, nullptr, "PrintArpEntries"},
- {13, nullptr, "Unknown13"},
- {14, nullptr, "Unknown14"},
- {15, nullptr, "Unknown15"},
+ {0, &BSDCFG::StubReplyZeroed, "SetIfUp"},
+ {1, &BSDCFG::StubReplyZeroed, "SetIfUpWithEvent"},
+ {2, &BSDCFG::StubReplyZeroed, "CancelIf"},
+ {3, &BSDCFG::StubReplyZeroed, "SetIfDown"},
+ {4, &BSDCFG::StubReplyZeroed, "GetIfState"},
+ {5, &BSDCFG::StubReplyZeroed, "DhcpRenew"},
+ {6, &BSDCFG::StubReplyZeroed, "AddStaticArpEntry"},
+ {7, &BSDCFG::StubReplyZeroed, "RemoveArpEntry"},
+ {8, &BSDCFG::StubReplyZeroed, "LookupArpEntry"},
+ {9, &BSDCFG::StubReplyZeroed, "LookupArpEntry2"},
+ {10, &BSDCFG::StubReplyZeroed, "ClearArpEntries"},
+ {11, &BSDCFG::StubReplyZeroed, "ClearArpEntries2"},
+ {12, &BSDCFG::StubReplyZeroed, "PrintArpEntries"},
+ {13, &BSDCFG::StubReplyZeroed, "Unknown13"},
+ {14, &BSDCFG::StubReplyZeroed, "Unknown14"},
+ {15, &BSDCFG::StubReplyZeroed, "Unknown15"},
+ {16, &BSDCFG::StubReplyZeroed, "Unknown16"},
+ {100, &BSDCFG::StubReplyZeroed, "Unknown100"},
+ {101, &BSDCFG::StubReplyZeroed, "Unknown101"},
+ {102, &BSDCFG::StubReplyZeroed, "Unknown102"},
+ {103, &BSDCFG::StubReplyZeroed, "Unknown103"},
+ {104, &BSDCFG::StubReplyZeroed, "Unknown104"},
};
// clang-format on
diff --git a/src/core/hle/service/sockets/nsd.cpp b/src/core/hle/service/sockets/nsd.cpp
index 241f2971ca..eb867cf08a 100644
--- a/src/core/hle/service/sockets/nsd.cpp
+++ b/src/core/hle/service/sockets/nsd.cpp
@@ -86,7 +86,7 @@ void NSD::SetChangeEnvironmentIdentifierDisabled(HLERequestContext& ctx) {
LOG_WARNING(Service, "(STUBBED) called, disabled={}", disabled);
- IPC::ResponseBuilder rb{ctx, 1};
+ IPC::ResponseBuilder rb{ctx, 2};
rb.Push(ResultSuccess);
}
@@ -114,7 +114,7 @@ void NSD::ResolveEx(HLERequestContext& ctx) {
}
ctx.WriteBuffer(fqdn_out);
- IPC::ResponseBuilder rb{ctx, 4};
+ IPC::ResponseBuilder rb{ctx, 3};
rb.Push(ResultSuccess);
rb.Push(ResultSuccess);
}
diff --git a/src/core/hle/service/ssl/ssl.cpp b/src/core/hle/service/ssl/ssl.cpp
index 999efd4295..15aa478b75 100644
--- a/src/core/hle/service/ssl/ssl.cpp
+++ b/src/core/hle/service/ssl/ssl.cpp
@@ -87,7 +87,7 @@ public:
{3, D<&ISslConnection::SetIoMode>, "SetIoMode"},
{4, D<&ISslConnection::GetSocketDescriptor>, "GetSocketDescriptor"},
{5, D<&ISslConnection::GetHostName>, "GetHostName"},
- {6, nullptr, "GetVerifyOption"},
+ {6, &ISslConnection::StubReplyZeroed, "GetVerifyOption"},
{7, D<&ISslConnection::GetIoMode>, "GetIoMode"},
{8, D<&ISslConnection::DoHandshake>, "DoHandshake"},
{9, &ISslConnection::DoHandshakeGetServerCert, "DoHandshakeGetServerCert"},
@@ -105,18 +105,20 @@ public:
{21, D<&ISslConnection::GetRenegotiationMode>, "GetRenegotiationMode"},
{22, D<&ISslConnection::SetOption>, "SetOption"},
{23, D<&ISslConnection::GetOption>, "GetOption"},
- {24, nullptr, "GetVerifyCertErrors"},
- {25, nullptr, "GetCipherInfo"},
+ {24, &ISslConnection::StubReplyZeroed, "GetVerifyCertErrors"},
+ {25, &ISslConnection::StubReplyZeroed, "GetCipherInfo"},
{26, D<&ISslConnection::SetNextAlpnProto>, "SetNextAlpnProto"},
{27, D<&ISslConnection::GetNextAlpnProto>, "GetNextAlpnProto"},
- {28, nullptr, "SetDtlsSocketDescriptor"},
- {29, nullptr, "GetDtlsHandshakeTimeout"},
- {30, nullptr, "SetPrivateOption"},
- {31, nullptr, "SetSrtpCiphers"},
- {32, nullptr, "GetSrtpCipher"},
- {33, nullptr, "ExportKeyingMaterial"},
- {34, nullptr, "SetIoTimeout"},
- {35, nullptr, "GetIoTimeout"},
+ {28, &ISslConnection::StubReplyZeroed, "SetDtlsSocketDescriptor"},
+ {29, &ISslConnection::StubReplyZeroed, "GetDtlsHandshakeTimeout"},
+ {30, &ISslConnection::StubReplyZeroed, "SetPrivateOption"},
+ {31, &ISslConnection::StubReplyZeroed, "SetSrtpCiphers"},
+ {32, &ISslConnection::StubReplyZeroed, "GetSrtpCipher"},
+ {33, &ISslConnection::StubReplyZeroed, "ExportKeyingMaterial"},
+ {34, &ISslConnection::StubReplyZeroed, "SetIoTimeout"},
+ {35, &ISslConnection::StubReplyZeroed, "GetIoTimeout"},
+ {36, &ISslConnection::StubReplyZeroed, "Unknown36"},
+ {37, &ISslConnection::StubReplyZeroed, "Unknown37"},
};
// clang-format on
@@ -420,10 +422,14 @@ private:
R_SUCCEED();
}
- Result GetNextAlpnProto(OutBuffer<BufferAttr_HipcMapAlias> data, Out<u32> to_write) {
- *to_write = u32((std::min)(next_alpn_proto.size(), data.size()));
- next_alpn_proto.assign(data.begin(), data.begin() + *to_write);
- LOG_DEBUG(Service_SSL, "GetNextAlpnProto called, size={}", *to_write);
+ // Replies with two words, the ALPN state and the size written to the buffer. It used to reply
+ // with one and copy the caller's (output) buffer over the protocols SetNextAlpnProto stored.
+ // No TLS negotiation happens here, so report that nothing was negotiated.
+ Result GetNextAlpnProto(OutBuffer<BufferAttr_HipcMapAlias> data, Out<u32> out_state,
+ Out<u32> out_size) {
+ LOG_DEBUG(Service_SSL, "GetNextAlpnProto called, buffer size={}", data.size());
+ *out_state = 0;
+ *out_size = 0;
R_SUCCEED();
}
@@ -557,8 +563,10 @@ private:
rb.Push(client_id);
}
- Result RegisterInternalPki() {
+ // Replies with a u64 (the registered certificate's id); the reply used to carry no data.
+ Result RegisterInternalPki(Out<u64> out_id) {
LOG_WARNING(Service_SSL, "(STUBBED) called");
+ *out_id = 0;
R_SUCCEED();
}
};
@@ -636,7 +644,7 @@ class ISslServiceForSystem final : public ServiceFramework<ISslServiceForSystem>
explicit ISslServiceForSystem(Core::System& system_) : ServiceFramework{system_, "ssl:s"} {
// clang-format off
static const FunctionInfo functions[] = {
- {0, D<&ISslServiceForSystem::CreateContext>, "CreateContext"},
+ {0, &ISslServiceForSystem::CreateContext, "CreateContext"},
{1, D<&ISslServiceForSystem::GetContextCount>, "GetContextCount"},
{2, D<&ISslServiceForSystem::GetCertificates>, "GetCertificates"},
{3, D<&ISslServiceForSystem::GetCertificateBufSize>, "GetCertificateBufSize"},
@@ -646,7 +654,7 @@ class ISslServiceForSystem final : public ServiceFramework<ISslServiceForSystem>
{7, D<&ISslServiceForSystem::SetDebugOption>, "SetDebugOption"},
{8, D<&ISslServiceForSystem::GetDebugOption>, "GetDebugOption"},
{9, D<&ISslServiceForSystem::ClearTls12FallbackFlag>, "ClearTls12FallbackFlag"},
- {100, D<&ISslServiceForSystem::CreateContextForSystem>, "CreateContextForSystem"},
+ {100, &ISslServiceForSystem::CreateContext, "CreateContextForSystem"},
{101, D<&ISslServiceForSystem::SetThreadCoreMask>, "SetThreadCoreMask"},
{102, D<&ISslServiceForSystem::GetThreadCoreMask>, "GetThreadCoreMask"},
{103, D<&ISslServiceForSystem::VerifySignature>, "VerifySignature"}
@@ -656,19 +664,30 @@ class ISslServiceForSystem final : public ServiceFramework<ISslServiceForSystem>
RegisterHandlers(functions);
};
- Result CreateContext() {
- LOG_DEBUG(Service_SSL, "(STUBBED) called.");
+ // 0 and 100(SslVersion, padding, u64 pid placeholder; pid): an ISslContext, like ssl's
+ // CreateContext. The empty reply returned no object, so the caller got handle 0
+ // (Minecraft calls 100).
+ void CreateContext(HLERequestContext& ctx) {
+ struct Parameters {
+ SslVersion ssl_version;
+ INSERT_PADDING_BYTES(0x4);
+ u64 pid_placeholder;
+ };
+ static_assert(sizeof(Parameters) == 0x10, "Parameters is an invalid size");
- // TODO (jarrodnorwell)
+ IPC::RequestParser rp{ctx};
+ const auto parameters = rp.PopRaw<Parameters>();
+ LOG_WARNING(Service_SSL, "(STUBBED) called, command={}, api_version={}", ctx.GetCommand(),
+ parameters.ssl_version.api_version);
- return ResultSuccess;
- };
+ IPC::ResponseBuilder rb{ctx, 2, 0, 1};
+ rb.Push(ResultSuccess);
+ rb.PushIpcInterface<ISslContext>(ctx, system, parameters.ssl_version);
+ }
- Result GetContextCount() {
+ Result GetContextCount(Out<u32> out_count) {
LOG_DEBUG(Service_SSL, "(STUBBED) called.");
-
- // TODO (jarrodnorwell)
-
+ *out_count = 0;
return ResultSuccess;
};
@@ -704,11 +723,9 @@ class ISslServiceForSystem final : public ServiceFramework<ISslServiceForSystem>
return ResultSuccess;
};
- Result FlushSessionCache() {
+ Result FlushSessionCache(Out<u32> out_count) {
LOG_DEBUG(Service_SSL, "(STUBBED) called.");
-
- // TODO (jarrodnorwell)
-
+ *out_count = 0;
return ResultSuccess;
};
@@ -736,14 +753,6 @@ class ISslServiceForSystem final : public ServiceFramework<ISslServiceForSystem>
return ResultSuccess;
};
- Result CreateContextForSystem() {
- LOG_DEBUG(Service_SSL, "(STUBBED) called.");
-
- // TODO (jarrodnorwell)
-
- return ResultSuccess;
- };
-
Result SetThreadCoreMask() {
LOG_DEBUG(Service_SSL, "(STUBBED) called.");
@@ -752,11 +761,9 @@ class ISslServiceForSystem final : public ServiceFramework<ISslServiceForSystem>
return ResultSuccess;
};
- Result GetThreadCoreMask() {
+ Result GetThreadCoreMask(Out<u64> out_mask) {
LOG_DEBUG(Service_SSL, "(STUBBED) called.");
-
- // TODO (jarrodnorwell)
-
+ *out_mask = 0;
return ResultSuccess;
};
diff --git a/src/core/launch_params.cpp b/src/core/launch_params.cpp
index 25fdd45fdb..6c436d0346 100644
--- a/src/core/launch_params.cpp
+++ b/src/core/launch_params.cpp
@@ -39,6 +39,7 @@ LaunchParams ParseLaunchParams(Core::System& system, int argc, char *argv[], wch
{"null-render", no_argument, 0, 'n'},
{"singlecore", no_argument, 0, 's'},
{"filter", no_argument, 0, 'x'},
+ {"pack-nup", required_argument, 0, 'N'},
{0, 0, 0, 0},
// clang-format on
};
@@ -62,6 +63,9 @@ LaunchParams ParseLaunchParams(Core::System& system, int argc, char *argv[], wch
case 'g':
p.filepath = std::string(optarg);
break;
+ case 'N':
+ p.pack_nup_source = std::string(optarg);
+ break;
case 'i': {
p.input_profile = std::string(optarg);
break;
@@ -196,7 +200,9 @@ void ApplyLaunchParams(LaunchParams const& lp) noexcept {
"-p, --program Pass following string as arguments to executable\n"
"-u, --user Select a specific user profile from 0 to 7\n"
"-d, --debug Run the GDB stub on a port from 1 to 65535\n"
- "-v, --version Output version information and exit\n",
+ "-v, --version Output version information and exit\n"
+ "--pack-nup <dump> [<out.nup>]"
+ " Pack a loose dump with all its updates and DLC into a NUP file and exit\n",
lp.argv0
);
}
diff --git a/src/core/launch_params.h b/src/core/launch_params.h
index 4da30fc1c2..8d503770d8 100644
--- a/src/core/launch_params.h
+++ b/src/core/launch_params.h
@@ -23,6 +23,8 @@ struct LaunchParams {
std::optional<int> selected_user{};
std::optional<u16> override_gdb_port{};
std::optional<std::string> log_filter{};
+ // --pack-nup <dump> [<out.nup>]: packs a loose dump instead of booting a game (eden-cli).
+ std::optional<std::string> pack_nup_source{};
u16 port = Network::DefaultRoomPort;
bool use_multiplayer = false;
bool fullscreen = false;
diff --git a/src/core/loader/deconstructed_rom_directory.cpp b/src/core/loader/deconstructed_rom_directory.cpp
index cda1645548..49352a5b49 100644
--- a/src/core/loader/deconstructed_rom_directory.cpp
+++ b/src/core/loader/deconstructed_rom_directory.cpp
@@ -6,7 +6,11 @@
#include <algorithm>
#include <cctype>
+#include <chrono>
#include <cstring>
+#include <map>
+#include <memory>
+#include <mutex>
#include <string_view>
#include "common/logging.h"
#include "common/random.h"
@@ -77,6 +81,24 @@ struct PatchCollection {
namespace {
+FileSys::VirtualDir FindLooseRomFSDirectory(const FileSys::VirtualDir& parent) {
+ if (parent == nullptr) {
+ return nullptr;
+ }
+ if (const auto dir = parent->GetSubdirectory("romfs")) {
+ return dir;
+ }
+ for (const auto& dir : parent->GetSubdirectories()) {
+ const auto name = dir->GetName();
+ if (name.size() == 5 && std::equal(name.begin(), name.end(), "romfs", [](char a, char b) {
+ return std::tolower(static_cast<unsigned char>(a)) == b;
+ })) {
+ return dir;
+ }
+ }
+ return nullptr;
+}
+
// RomFS for a loose ExeFS: the first .romfs file next to main, else a romfs/ directory next to
// the exefs/ directory (the usual layout of an extracted title).
FileSys::VirtualFile FindLooseRomFS(const FileSys::VirtualDir& exefs_dir) {
@@ -89,35 +111,43 @@ FileSys::VirtualFile FindLooseRomFS(const FileSys::VirtualDir& exefs_dir) {
}
}
if (const auto parent = exefs_dir->GetParentDirectory()) {
- if (const auto romfs_dir = parent->GetSubdirectory("romfs")) {
+ if (const auto romfs_dir = FindLooseRomFSDirectory(parent)) {
+ // Building it walks every file and keeps an object for each (title 1: 206k files, ~150
+ // MiB), so loaders of the same directory share one build while any of them holds it.
+ static std::mutex cache_mutex;
+ static std::map<std::string, std::weak_ptr<FileSys::VfsFile>> cache;
+ std::scoped_lock lk{cache_mutex};
+ auto& cached = cache[romfs_dir->GetFullPath()];
+ if (auto built = cached.lock()) {
+ return built;
+ }
LOG_INFO(Loader, "Using RomFS directory next to the ExeFS");
- return FileSys::CreateRomFS(romfs_dir);
+ const auto started = std::chrono::steady_clock::now();
+ auto built = FileSys::CreateRomFS(romfs_dir);
+ LOG_INFO(Loader, "Loose RomFS image ready: elapsed_ms={}",
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::steady_clock::now() - started)
+ .count());
+ cached = built;
+ return built;
}
}
return nullptr;
}
-// The display version of a dump without control data, from its RomFS: Nintendo EPD titles name
-// their resource size tables after it (System/Resource/ResourceSizeTable.Product.180.* in Mario
-// Kart World 1.8.0). A hotfix can keep its minor version's tables (Product.110 in a 1.1.1).
-std::string RomFSProductVersion(const FileSys::VirtualDir& romfs_dir) {
- if (romfs_dir == nullptr) {
- return {};
- }
- const auto system_dir = romfs_dir->GetSubdirectory("System");
- const auto resource_dir = system_dir != nullptr ? system_dir->GetSubdirectory("Resource") : nullptr;
- if (resource_dir == nullptr) {
- return {};
- }
- constexpr std::string_view Prefix = "ResourceSizeTable.Product.";
+// The newest three-digit tag in a directory's file names of the form <prefix>NNN.<anything>.
+std::string NewestProductTag(const FileSys::VirtualDir& dir, std::string_view prefix) {
std::string best;
- for (const auto& file : resource_dir->GetFiles()) {
+ if (dir == nullptr) {
+ return best;
+ }
+ for (const auto& file : dir->GetFiles()) {
const std::string file_name = file->GetName();
- if (!file_name.starts_with(Prefix) || file_name.size() < Prefix.size() + 4) {
+ if (!file_name.starts_with(prefix) || file_name.size() < prefix.size() + 4) {
continue;
}
- const std::string_view digits = std::string_view{file_name}.substr(Prefix.size(), 3);
- if (file_name[Prefix.size() + 3] != '.' ||
+ const std::string_view digits = std::string_view{file_name}.substr(prefix.size(), 3);
+ if (file_name[prefix.size() + 3] != '.' ||
!std::all_of(digits.begin(), digits.end(), [](char c) { return c >= '0' && c <= '9'; })) {
continue;
}
@@ -125,6 +155,27 @@ std::string RomFSProductVersion(const FileSys::VirtualDir& romfs_dir) {
best = version;
}
}
+ return best;
+}
+
+// The display version of a dump without control data, from its RomFS: Nintendo EPD titles name
+// their resource size tables after it (System/Resource/ResourceSizeTable.Product.180.* in Mario
+// Kart World 1.8.0), and some name their game data list instead (GameData/GameDataList.Product.320.*
+// in Donkey Kong Bananza 3.2.0). A hotfix can keep its minor version's tables (Product.110 in a
+// 1.1.1).
+std::string RomFSProductVersion(const FileSys::VirtualDir& romfs_dir) {
+ if (romfs_dir == nullptr) {
+ return {};
+ }
+ const auto system_dir = romfs_dir->GetSubdirectory("System");
+ std::string best = NewestProductTag(
+ system_dir != nullptr ? system_dir->GetSubdirectory("Resource") : nullptr,
+ "ResourceSizeTable.Product.");
+ if (const auto game_data = NewestProductTag(romfs_dir->GetSubdirectory("GameData"),
+ "GameDataList.Product.");
+ game_data > best) {
+ best = game_data;
+ }
if (best.empty()) {
return {};
}
@@ -233,9 +284,9 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(FileSys
name = control->GetApplicationName();
}
- romfs = FindLooseRomFS(file_dir);
+ romfs_exefs_dir = file_dir;
if (const auto title_dir = file_dir->GetParentDirectory()) {
- romfs_version = RomFSProductVersion(title_dir->GetSubdirectory("romfs"));
+ romfs_version = RomFSProductVersion(FindLooseRomFSDirectory(title_dir));
}
}
@@ -245,7 +296,17 @@ AppLoader_DeconstructedRomDirectory::AppLoader_DeconstructedRomDirectory(
, dir(std::move(directory))
, override_update(override_update_)
{
- romfs = FindLooseRomFS(dir);
+ romfs_exefs_dir = dir;
+}
+
+// Loaders made only for metadata (the game list, title lookups) never need the RomFS, and several
+// of them were alive with a whole copy each.
+const FileSys::VirtualFile& AppLoader_DeconstructedRomDirectory::LooseRomFS() {
+ if (!romfs_resolved) {
+ romfs_resolved = true;
+ romfs = FindLooseRomFS(romfs_exefs_dir);
+ }
+ return romfs;
}
FileType AppLoader_DeconstructedRomDirectory::IdentifyType(const FileSys::VirtualFile& dir_file) {
@@ -304,7 +365,7 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
// Loose-ExeFS loads bypass the NCA/NSP loaders, which are the ones that normally apply
// LayeredFS to the RomFS; apply it here so mods in user/load/<titleid>/ work too.
- if (romfs != nullptr) {
+ if (LooseRomFS() != nullptr) {
const FileSys::PatchManager patch_manager(metadata.GetTitleID(),
system.GetFileSystemController(),
system.GetContentProvider());
@@ -414,7 +475,7 @@ AppLoader_DeconstructedRomDirectory::LoadResult AppLoader_DeconstructedRomDirect
}
ResultStatus AppLoader_DeconstructedRomDirectory::ReadRomFS(FileSys::VirtualFile& out_dir) {
- if (romfs == nullptr) {
+ if (LooseRomFS() == nullptr) {
return ResultStatus::ErrorNoRomFS;
}
diff --git a/src/core/loader/deconstructed_rom_directory.h b/src/core/loader/deconstructed_rom_directory.h
index b976b71ecb..db0120f4ad 100644
--- a/src/core/loader/deconstructed_rom_directory.h
+++ b/src/core/loader/deconstructed_rom_directory.h
@@ -58,8 +58,13 @@ public:
ResultStatus ReadNSOModules(Modules& out_modules) override;
private:
+ /// The loose RomFS next to the ExeFS, found (and built) on first use.
+ const FileSys::VirtualFile& LooseRomFS();
+
FileSys::ProgramMetadata metadata;
FileSys::VirtualFile romfs;
+ FileSys::VirtualDir romfs_exefs_dir; ///< Where LooseRomFS looks
+ bool romfs_resolved{};
FileSys::VirtualDir dir;
std::vector<u8> icon_data;
diff --git a/src/core/loader/loader.cpp b/src/core/loader/loader.cpp
index 33a93573cf..839bb4d80b 100644
--- a/src/core/loader/loader.cpp
+++ b/src/core/loader/loader.cpp
@@ -29,6 +29,7 @@
#include "core/loader/nro.h"
#include "core/loader/nso.h"
#include "core/loader/nsp.h"
+#include "core/loader/nup.h"
#include "core/loader/xci.h"
namespace Loader {
@@ -130,6 +131,8 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
return *nsp_type;
} else if (const auto xci_type = IdentifyFileLoader<AppLoader_XCI>(file)) {
return *xci_type;
+ } else if (const auto nup_type = IdentifyFileLoader<AppLoader_NUP>(file)) {
+ return *nup_type;
} else if (const auto nro_type = IdentifyFileLoader<AppLoader_NRO>(file)) {
return *nro_type;
} else if (const auto nca_type = IdentifyFileLoader<AppLoader_NCA>(file)) {
@@ -150,7 +153,7 @@ FileType IdentifyFile(FileSys::VirtualFile file) {
bool IsContainerType(FileType type) {
return type == FileType::NSP || type == FileType::NSZ || type == FileType::XCI ||
- type == FileType::XCZ;
+ type == FileType::XCZ || type == FileType::NUP;
}
bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 program_id,
@@ -163,6 +166,24 @@ bool IsBootableGameContainer(FileSys::VirtualFile file, FileType type, u64 progr
type = IdentifyFile(file);
}
+ if (type == FileType::NUP) {
+ FileSys::NupContainer nup{file};
+ if (nup.GetStatus() != ResultStatus::Success) {
+ return false;
+ }
+ // A base package boots normally; a standalone update (no base) carries
+ // a complete patched tree and boots on its own (SPEC section 4.1.2).
+ if (const auto base = nup.FindBasePackage();
+ base.has_value() && nup.GetExeFSDir(*base) != nullptr) {
+ return true;
+ }
+ if (const auto update = nup.FindBestUpdatePackage();
+ update.has_value() && nup.GetExeFSDir(*update) != nullptr) {
+ return true;
+ }
+ return false;
+ }
+
if (!IsContainerType(type)) {
return false;
}
@@ -190,6 +211,8 @@ FileType GuessFromFilename(const std::string& name) {
return FileType::XCZ;
else if (extension == "nsp")
return FileType::NSP;
+ else if (extension == "nup")
+ return FileType::NUP;
else if (extension == "nsz")
return FileType::NSZ;
else if (extension == "kip")
@@ -215,6 +238,8 @@ std::string GetFileTypeString(FileType type) {
return "NSP";
case FileType::NSZ:
return "NSZ";
+ case FileType::NUP:
+ return "NUP";
case FileType::KIP:
return "KIP";
case FileType::DeconstructedRomDirectory:
@@ -227,7 +252,7 @@ std::string GetFileTypeString(FileType type) {
return "unknown";
}
-constexpr std::array<const char*, 68> RESULT_MESSAGES{
+constexpr std::array<const char*, 70> RESULT_MESSAGES{
"The operation completed successfully.",
"The loader requested to load is already loaded.",
"The operation is not implemented.",
@@ -296,6 +321,8 @@ constexpr std::array<const char*, 68> RESULT_MESSAGES{
"The INI file contains more than the maximum allowable number of KIP files.",
"Integrity verification could not be performed for this file.",
"Integrity verification failed.",
+ "The NUP container is malformed or its TOC hash does not match.",
+ "The NUP container does not contain a loadable base package.",
};
std::string GetResultStatusString(ResultStatus status) {
@@ -380,6 +407,10 @@ static std::unique_ptr<AppLoader> GetFileLoader(Core::System& system, FileSys::V
case FileType::KIP:
return std::make_unique<AppLoader_KIP>(std::move(file));
+ // NUP (NX Package) container format.
+ case FileType::NUP:
+ return std::make_unique<AppLoader_NUP>(std::move(file));
+
// NX deconstructed ROM directory.
case FileType::DeconstructedRomDirectory:
return std::make_unique<AppLoader_DeconstructedRomDirectory>(std::move(file));
diff --git a/src/core/loader/loader.h b/src/core/loader/loader.h
index 431aeace73..23be3597f5 100644
--- a/src/core/loader/loader.h
+++ b/src/core/loader/loader.h
@@ -47,6 +47,7 @@ enum class FileType {
XCZ,
NAX,
KIP,
+ NUP,
DeconstructedRomDirectory,
};
@@ -157,6 +158,8 @@ enum class ResultStatus : u16 {
ErrorINITooManyKIPs,
ErrorIntegrityVerificationNotImplemented,
ErrorIntegrityVerificationFailed,
+ ErrorNUPBadFormat,
+ ErrorNUPNoBasePackage,
};
std::string GetResultStatusString(ResultStatus status);
diff --git a/src/core/loader/nup.cpp b/src/core/loader/nup.cpp
new file mode 100644
index 0000000000..7ddfb8947e
--- /dev/null
+++ b/src/core/loader/nup.cpp
@@ -0,0 +1,280 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "core/loader/nup.h"
+
+#include <algorithm>
+#include <cstring>
+
+#include "common/logging.h"
+#include "common/string_util.h"
+#include "core/core.h"
+#include "core/file_sys/nup_container.h"
+#include "core/file_sys/control_metadata.h"
+#include "core/file_sys/patch_manager.h"
+#include "core/file_sys/romfs.h"
+#include "core/file_sys/romfs_factory.h"
+#include "core/file_sys/vfs/vfs_vector.h"
+#include "core/hle/kernel/k_process.h"
+#include "core/hle/service/filesystem/filesystem.h"
+#include "core/loader/deconstructed_rom_directory.h"
+
+namespace Loader {
+
+namespace {
+
+using FileSys::NupContainer;
+
+// NACP stores 16 uncompressed language entries; map BCP-47 tags onto them.
+struct LanguageSlot {
+ std::string_view tag;
+ std::size_t index;
+};
+
+constexpr std::array<LanguageSlot, 18> LanguageSlots{{
+ {"en-us", 0}, {"en-gb", 1}, {"ja", 2}, {"fr", 3}, {"de", 4},
+ {"es-419", 5}, {"es", 6}, {"it", 7}, {"nl", 8}, {"fr-ca", 9},
+ {"pt", 10}, {"ru", 11}, {"ko", 12}, {"zh-hant", 13}, {"zh-hans", 14},
+ {"pt-br", 15}, {"pl", 16}, {"th", 17},
+}};
+
+void CopyNameToEntry(FileSys::LanguageEntry& entry, std::string_view name) {
+ const std::size_t length = std::min(name.size(), entry.application_name.size() - 1);
+ std::memcpy(entry.application_name.data(), name.data(), length);
+ entry.application_name[length] = '\0';
+}
+
+std::unique_ptr<FileSys::NACP> BuildNacp(const NupContainer::PackageInfo& base,
+ const std::string& version) {
+ if (base.title_name.empty() && base.localized_titles.empty() && version.empty()) {
+ return nullptr;
+ }
+ auto nacp = std::make_unique<FileSys::NACP>();
+ nacp->raw.supported_languages = static_cast<FileSys::SupportedLanguage>(0x3FFFF);
+
+ for (const auto& [lang, name] : base.localized_titles) {
+ for (const auto& slot : LanguageSlots) {
+ // The uncompressed RawNACP only stores 16 entries; Polish/Thai exist
+ // only in the compressed format, so they fall back to the default.
+ if (slot.index < 16 && slot.tag == lang) {
+ CopyNameToEntry(nacp->raw.language_entries.language_entries[slot.index], name);
+ }
+ }
+ }
+ if (!base.title_name.empty()) {
+ CopyNameToEntry(nacp->raw.language_entries.language_entries[0], base.title_name);
+ }
+ if (!version.empty()) {
+ const std::size_t length = std::min(version.size(), nacp->raw.version_string.size() - 1);
+ std::memcpy(nacp->raw.version_string.data(), version.data(), length);
+ nacp->raw.version_string[length] = '\0';
+ }
+ nacp->raw.save_data_owner_id = base.title_id;
+
+ nacp->language_entries.resize(16);
+ std::memcpy(nacp->language_entries.data(), nacp->raw.language_entries.language_entries.data(),
+ sizeof(nacp->raw.language_entries.language_entries));
+ return nacp;
+}
+
+std::string DecodeVersion(u32 version) {
+ if (version == 0) {
+ return {};
+ }
+ return fmt::format("{}.{}.{}", version >> 16, (version >> 8) & 0xFF, version & 0xFF);
+}
+
+} // namespace
+
+AppLoader_NUP::AppLoader_NUP(FileSys::VirtualFile file_)
+ : AppLoader(std::move(file_)), nup(std::make_unique<NupContainer>(file)) {
+ if (nup->GetStatus() != ResultStatus::Success) {
+ return;
+ }
+
+ const auto base_index = nup->FindBasePackage();
+ const auto update_index = nup->FindBestUpdatePackage();
+
+ // The program comes from the base package; a standalone update container
+ // (SPEC §4.1.2) carries a complete patched tree and boots on its own.
+ std::optional<std::size_t> program_index;
+ std::optional<std::size_t> exefs_index;
+ if (base_index && nup->GetExeFSDir(*base_index) != nullptr) {
+ program_index = base_index;
+ exefs_index = update_index && nup->GetExeFSDir(*update_index) != nullptr
+ ? update_index
+ : base_index;
+ } else if (update_index && nup->GetExeFSDir(*update_index) != nullptr) {
+ LOG_INFO(Loader,
+ "NUP container is a standalone update; booting the update program directly");
+ program_index = update_index;
+ exefs_index = update_index;
+ }
+ if (!program_index || !exefs_index) {
+ LOG_ERROR(Loader, "NUP container has no package with a usable ExeFS");
+ return;
+ }
+
+ const auto& program = nup->GetPackages()[*program_index];
+ base_title_id = program.title_id;
+ has_program = true;
+
+ const auto exefs = nup->GetExeFSDir(*exefs_index);
+ if (exefs == nullptr || exefs->GetFile("main") == nullptr ||
+ exefs->GetFile("main.npdm") == nullptr) {
+ LOG_ERROR(Loader, "NUP package has no usable ExeFS");
+ return;
+ }
+ secondary_loader = std::make_unique<AppLoader_DeconstructedRomDirectory>(exefs, true);
+
+ if (update_index) {
+ const auto& update = nup->GetPackages()[*update_index];
+ display_version = update.display_version.empty() ? DecodeVersion(update.version)
+ : update.display_version;
+ } else {
+ display_version = program.display_version.empty() ? DecodeVersion(program.version)
+ : program.display_version;
+ }
+ nacp_file = BuildNacp(program, display_version);
+ if (nacp_file != nullptr && !program.icon.empty()) {
+ icon_file = std::make_shared<FileSys::VectorVfsFile>(program.icon, "icon.dat");
+ } else if (nacp_file != nullptr) {
+ for (const auto& [lang, icon] : program.localized_icons) {
+ if (lang == "en-us") {
+ icon_file = std::make_shared<FileSys::VectorVfsFile>(icon, "icon_" + lang + ".dat");
+ break;
+ }
+ }
+ if (icon_file == nullptr && !program.localized_icons.empty()) {
+ icon_file = std::make_shared<FileSys::VectorVfsFile>(
+ program.localized_icons.front().second, "icon.dat");
+ }
+ }
+
+ const auto addon_count = std::count_if(nup->GetPackages().begin(), nup->GetPackages().end(),
+ [](const auto& package) {
+ return package.kind ==
+ NupContainer::PackageKind::Addon;
+ });
+ if (addon_count > 0) {
+ LOG_WARNING(Loader,
+ "NUP container has {} add-on content package(s); AoC mounting is not "
+ "supported yet",
+ addon_count);
+ }
+}
+
+AppLoader_NUP::~AppLoader_NUP() = default;
+
+FileType AppLoader_NUP::IdentifyType(const FileSys::VirtualFile& nup_file) {
+ return FileSys::NupContainer::IsNupFile(nup_file) ? FileType::NUP : FileType::Error;
+}
+
+AppLoader::LoadResult AppLoader_NUP::Load(Kernel::KProcess& process, Core::System& system) {
+ if (is_loaded) {
+ return {ResultStatus::ErrorAlreadyLoaded, {}};
+ }
+ if (nup->GetStatus() != ResultStatus::Success) {
+ return {nup->GetStatus(), {}};
+ }
+ if (!has_program || secondary_loader == nullptr) {
+ return {ResultStatus::ErrorNUPNoBasePackage, {}};
+ }
+
+ const auto result = secondary_loader->Load(process, system);
+ if (result.first != ResultStatus::Success) {
+ return result;
+ }
+
+ system.GetFileSystemController().RegisterProcess(
+ process.GetProcessId(), base_title_id,
+ std::make_shared<FileSys::RomFSFactory>(*this, system.GetContentProvider(),
+ system.GetFileSystemController()));
+
+ is_loaded = true;
+ return result;
+}
+
+ResultStatus AppLoader_NUP::VerifyIntegrity(
+ std::function<bool(size_t, size_t)> progress_callback) {
+ if (nup->GetStatus() != ResultStatus::Success) {
+ return nup->GetStatus();
+ }
+ return nup->VerifyIntegrity(std::move(progress_callback));
+}
+
+ResultStatus AppLoader_NUP::ReadRomFS(FileSys::VirtualFile& out_file) {
+ const auto base_index = nup->FindBasePackage();
+ const auto base_romfs = base_index ? nup->GetRomFSDir(*base_index) : FileSys::VirtualDir{};
+ const auto update_index = nup->FindBestUpdatePackage();
+ const auto update_romfs =
+ update_index ? nup->GetRomFSDir(*update_index) : FileSys::VirtualDir{};
+ // A standalone update carries the complete merged tree; a base+update pair
+ // overlays the update onto the base (SPEC §4.1.4).
+ const auto merged = FileSys::NupContainer::MergeShadowDirs(base_romfs, update_romfs);
+ if (merged == nullptr) {
+ return ResultStatus::ErrorNoRomFS;
+ }
+ out_file = FileSys::CreateRomFS(merged);
+ if (out_file == nullptr) {
+ return ResultStatus::ErrorNoRomFS;
+ }
+ return ResultStatus::Success;
+}
+
+bool AppLoader_NUP::IsRomFSUpdatable() const {
+ // The update is already merged into the exposed RomFS.
+ return false;
+}
+
+ResultStatus AppLoader_NUP::ReadProgramId(u64& out_program_id) {
+ if (!has_program) {
+ return ResultStatus::ErrorNotInitialized;
+ }
+ out_program_id = base_title_id;
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadProgramIds(std::vector<u64>& out_program_ids) {
+ if (!has_program) {
+ return ResultStatus::ErrorNotInitialized;
+ }
+ out_program_ids.push_back(base_title_id);
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadIcon(std::vector<u8>& buffer) {
+ if (icon_file == nullptr) {
+ return ResultStatus::ErrorNoIcon;
+ }
+ buffer = icon_file->ReadAllBytes();
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadTitle(std::string& title) {
+ if (nacp_file == nullptr) {
+ return ResultStatus::ErrorNoControl;
+ }
+ title = nacp_file->GetApplicationName();
+ if (title.empty()) {
+ return ResultStatus::ErrorNoControl;
+ }
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadControlData(FileSys::NACP& control) {
+ if (nacp_file == nullptr) {
+ return ResultStatus::ErrorNoControl;
+ }
+ control = *nacp_file;
+ return ResultStatus::Success;
+}
+
+ResultStatus AppLoader_NUP::ReadNSOModules(Modules& modules) {
+ if (secondary_loader == nullptr) {
+ return ResultStatus::ErrorNotInitialized;
+ }
+ return secondary_loader->ReadNSOModules(modules);
+}
+
+} // namespace Loader
diff --git a/src/core/loader/nup.h b/src/core/loader/nup.h
new file mode 100644
index 0000000000..1f9dd5922b
--- /dev/null
+++ b/src/core/loader/nup.h
@@ -0,0 +1,65 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <memory>
+#include <optional>
+#include <string>
+
+#include "core/file_sys/nup_container.h"
+#include "core/loader/loader.h"
+
+namespace FileSys {
+class NACP;
+class NupContainer;
+} // namespace FileSys
+
+namespace Loader {
+
+/// Loads a NUP (NX Package) container: loose ExeFS/RomFS trees for a base
+/// program plus its updates in one file. The newest update package is layered
+/// over the base; add-on content packages are recognized but not yet mounted.
+class AppLoader_NUP final : public AppLoader {
+public:
+ explicit AppLoader_NUP(FileSys::VirtualFile file_);
+ ~AppLoader_NUP() override;
+
+ /**
+ * Identifies whether or not the given file is a NUP container.
+ *
+ * @param nup_file The file to identify.
+ *
+ * @return FileType::NUP, or FileType::Error if the file is not a NUP.
+ */
+ static FileType IdentifyType(const FileSys::VirtualFile& nup_file);
+
+ FileType GetFileType() const override {
+ return FileType::NUP;
+ }
+
+ LoadResult Load(Kernel::KProcess& process, Core::System& system) override;
+
+ ResultStatus VerifyIntegrity(std::function<bool(size_t, size_t)> progress_callback) override;
+
+ ResultStatus ReadRomFS(FileSys::VirtualFile& out_file) override;
+ bool IsRomFSUpdatable() const override;
+ ResultStatus ReadProgramId(u64& out_program_id) override;
+ ResultStatus ReadProgramIds(std::vector<u64>& out_program_ids) override;
+ ResultStatus ReadIcon(std::vector<u8>& buffer) override;
+ ResultStatus ReadTitle(std::string& title) override;
+ ResultStatus ReadControlData(FileSys::NACP& control) override;
+ ResultStatus ReadNSOModules(Modules& modules) override;
+
+private:
+ std::unique_ptr<FileSys::NupContainer> nup;
+ std::unique_ptr<AppLoader> secondary_loader;
+
+ std::unique_ptr<FileSys::NACP> nacp_file;
+ FileSys::VirtualFile icon_file;
+ u64 base_title_id{};
+ std::string display_version;
+ bool has_program{};
+};
+
+} // namespace Loader
diff --git a/src/core/memory.cpp b/src/core/memory.cpp
index cfc39916db..1f7315f3c1 100644
--- a/src/core/memory.cpp
+++ b/src/core/memory.cpp
@@ -549,6 +549,8 @@ struct Memory::Impl {
}
void RasterizerArmReadTraps(u64 vaddr, u64 size) {
+ // Clean cached read areas also need invalidation when reactive flushing is enabled.
+ read_trap_generation.fetch_add(1, std::memory_order_release);
if (vaddr == 0 || size == 0 || !AddressSpaceContains(*current_page_table, vaddr, size) ||
!ReadTrapsEnabled()) {
return;
@@ -781,6 +783,19 @@ struct Memory::Impl {
gpu_device_memory = &system.Host1x().MemoryManager();
}
const size_t core = system.GetCurrentHostThreadID();
+ if (core >= Core::Hardware::NUM_CPU_CORES) {
+ // A service thread has no slot in the arrays below, and it does not use a slot of a
+ // core. It keeps no area, so it asks on each read.
+ if (static std::atomic_flag logged; !logged.test_and_set(std::memory_order_relaxed)) {
+ LOG_INFO(HW_Memory, "Read of cached guest memory from a thread that is not a core");
+ }
+ Common::ScratchBuffer<u32> scratch;
+ gpu_device_memory->ApplyOpOnPointer(p, scratch, [&](DAddr address) {
+ DisarmReadTraps(v_address, size);
+ (void)system.GPU().OnCPURead(address, size);
+ });
+ return;
+ }
auto& current_area = rasterizer_read_areas[core];
if (const u64 generation = read_trap_generation.load(std::memory_order_acquire);
read_area_generations[core] != generation) {
diff --git a/src/dynarmic/src/dynarmic/backend/block_range_information.cpp b/src/dynarmic/src/dynarmic/backend/block_range_information.cpp
index 1a56b7adb6..068e86c2bc 100644
--- a/src/dynarmic/src/dynarmic/backend/block_range_information.cpp
+++ b/src/dynarmic/src/dynarmic/backend/block_range_information.cpp
@@ -8,7 +8,8 @@
#include "dynarmic/backend/block_range_information.h"
-#include <boost/icl/interval_map.hpp>
+#include <algorithm>
+
#include <boost/icl/interval_set.hpp>
#include "common/common_types.h"
#include "common/container/unordered_set.h"
@@ -17,24 +18,28 @@ namespace Dynarmic::Backend {
template<typename P>
void BlockRangeInformation<P>::AddRange(boost::icl::discrete_interval<P> range, IR::LocationDescriptor location) {
- block_ranges.add(std::make_pair(range, ::Common::unordered_set<IR::LocationDescriptor>{location}));
+ if (!boost::icl::is_empty(range)) {
+ ranges.push_back({boost::icl::first(range), boost::icl::last(range), location});
+ }
}
template<typename P>
void BlockRangeInformation<P>::ClearCache() {
- block_ranges.clear();
+ ranges.clear();
}
template<typename P>
-::Common::unordered_set<IR::LocationDescriptor> BlockRangeInformation<P>::InvalidateRanges(const boost::icl::interval_set<P>& ranges) {
+::Common::unordered_set<IR::LocationDescriptor> BlockRangeInformation<P>::InvalidateRanges(const boost::icl::interval_set<P>& invalidated) {
::Common::unordered_set<IR::LocationDescriptor> erase_locations;
- for (auto invalidate_interval : ranges) {
- auto pair = block_ranges.equal_range(invalidate_interval);
- for (auto it = pair.first; it != pair.second; ++it)
- for (const auto& descriptor : it->second)
- erase_locations.insert(descriptor);
- }
- // TODO: EFFICIENCY: Remove ranges that are to be erased.
+ std::erase_if(ranges, [&](const Range& range) {
+ for (const auto& interval : invalidated) {
+ if (range.first <= boost::icl::last(interval) && boost::icl::first(interval) <= range.last) {
+ erase_locations.insert(range.location);
+ return true;
+ }
+ }
+ return false;
+ });
return erase_locations;
}
diff --git a/src/dynarmic/src/dynarmic/backend/block_range_information.h b/src/dynarmic/src/dynarmic/backend/block_range_information.h
index 8f7daa9d9c..f894240603 100644
--- a/src/dynarmic/src/dynarmic/backend/block_range_information.h
+++ b/src/dynarmic/src/dynarmic/backend/block_range_information.h
@@ -8,11 +8,9 @@
#pragma once
-#include <set>
+#include <vector>
-#include <boost/icl/interval_map.hpp>
#include <boost/icl/interval_set.hpp>
-#include "common/container/unordered_map.h"
#include "common/container/unordered_set.h"
#include "dynarmic/ir/location_descriptor.h"
@@ -24,8 +22,18 @@ class BlockRangeInformation {
public:
void AddRange(boost::icl::discrete_interval<P> range, IR::LocationDescriptor location);
void ClearCache();
- ::Common::unordered_set<IR::LocationDescriptor> InvalidateRanges(const boost::icl::interval_set<P>& ranges);
- boost::icl::interval_map<P, ::Common::unordered_set<IR::LocationDescriptor>> block_ranges;
+ ::Common::unordered_set<IR::LocationDescriptor> InvalidateRanges(const boost::icl::interval_set<P>& invalidated);
+
+private:
+ /// The guest code [first, last] a block was translated from. Code is invalidated rarely (when
+ /// it is unmapped or reprotected), so a list scanned then costs least: an interval map of
+ /// block sets held ~300 bytes per block.
+ struct Range {
+ P first;
+ P last;
+ IR::LocationDescriptor location;
+ };
+ std::vector<Range> ranges;
};
} // namespace Dynarmic::Backend
diff --git a/src/dynarmic/src/dynarmic/backend/x64/a32_emit_x64.cpp b/src/dynarmic/src/dynarmic/backend/x64/a32_emit_x64.cpp
index 5c6a90e15a..e0e3c463d1 100644
--- a/src/dynarmic/src/dynarmic/backend/x64/a32_emit_x64.cpp
+++ b/src/dynarmic/src/dynarmic/backend/x64/a32_emit_x64.cpp
@@ -1168,7 +1168,7 @@ bool EmitTerminalImpl(A32EmitX64& e, IR::Term::LinkBlock terminal, IR::LocationD
} else {
if (e.conf.enable_cycle_counting) {
e.code.cmp(qword[rsp + ABI_SHADOW_SPACE + offsetof(StackLayout, cycles_remaining)], 0);
- e.patch_information[terminal.next].jg.push_back(e.code.getCurr());
+ e.AddPatchLocation(terminal.next, EmitX64::PatchKind::Jg);
if (const auto next_bb = e.GetBasicBlock(terminal.next)) {
e.EmitPatchJg(terminal.next, next_bb->entrypoint);
} else {
@@ -1176,7 +1176,7 @@ bool EmitTerminalImpl(A32EmitX64& e, IR::Term::LinkBlock terminal, IR::LocationD
}
} else {
e.code.cmp(dword[e.code.ABI_JIT_PTR + offsetof(A32JitState, halt_reason)], 0);
- e.patch_information[terminal.next].jz.push_back(e.code.getCurr());
+ e.AddPatchLocation(terminal.next, EmitX64::PatchKind::Jz);
if (const auto next_bb = e.GetBasicBlock(terminal.next)) {
e.EmitPatchJz(terminal.next, next_bb->entrypoint);
} else {
@@ -1196,7 +1196,7 @@ bool EmitTerminalImpl(A32EmitX64& e, IR::Term::LinkBlockFast terminal, IR::Locat
e.code.mov(MJitStateReg(A32::Reg::PC), A32::LocationDescriptor{terminal.next}.PC());
e.code.ReturnFromRunCode();
} else {
- e.patch_information[terminal.next].jmp.push_back(e.code.getCurr());
+ e.AddPatchLocation(terminal.next, EmitX64::PatchKind::Jmp);
if (const auto next_bb = e.GetBasicBlock(terminal.next)) {
e.EmitPatchJmp(terminal.next, next_bb->entrypoint);
} else {
diff --git a/src/dynarmic/src/dynarmic/backend/x64/a32_emit_x64.h b/src/dynarmic/src/dynarmic/backend/x64/a32_emit_x64.h
index a106bb6482..eb36bffd88 100644
--- a/src/dynarmic/src/dynarmic/backend/x64/a32_emit_x64.h
+++ b/src/dynarmic/src/dynarmic/backend/x64/a32_emit_x64.h
@@ -87,13 +87,17 @@ public:
// Fastmem information
using DoNotFastmemMarker = std::tuple<IR::LocationDescriptor, unsigned>;
+ /// Where a fastmem access that faults goes: code addresses are offsets in the code space.
struct FastmemPatchInfo {
- u64 resume_rip;
- u64 callback;
- DoNotFastmemMarker marker;
- bool recompile;
+ u32 rip;
+ u32 resume_rip;
+ u32 callback;
+ u32 inst_name : 31;
+ u32 recompile : 1;
+ IR::LocationDescriptor location;
};
std::optional<DoNotFastmemMarker> ShouldFastmem(A32EmitContext& ctx, IR::Inst* inst) const;
+ void AddFastmemPatch(CodePtr location, CodePtr resume_location, void (*callback)(), const DoNotFastmemMarker& marker, bool recompile);
FakeCall FastmemCallback(u64 rip);
// Memory access helpers
@@ -127,7 +131,7 @@ public:
RegAlloc reg_alloc; //reusable reg alloc
BlockRangeInformation<u32> block_ranges;
std::array<FastDispatchEntry, fast_dispatch_table_size> fast_dispatch_table;
- ::Common::unordered_map<u64, FastmemPatchInfo> fastmem_patch_info;
+ std::vector<FastmemPatchInfo> fastmem_patch_info; // sorted by rip
::Common::unordered_map<std::tuple<bool, size_t, int, int>, void (*)()> read_fallbacks;
::Common::unordered_map<std::tuple<bool, size_t, int, int>, void (*)()> write_fallbacks;
::Common::unordered_map<std::tuple<bool, size_t, int, int>, void (*)()> exclusive_write_fallbacks;
diff --git a/src/dynarmic/src/dynarmic/backend/x64/a64_emit_x64.cpp b/src/dynarmic/src/dynarmic/backend/x64/a64_emit_x64.cpp
index 71e5e62cd9..2307226744 100644
--- a/src/dynarmic/src/dynarmic/backend/x64/a64_emit_x64.cpp
+++ b/src/dynarmic/src/dynarmic/backend/x64/a64_emit_x64.cpp
@@ -628,7 +628,7 @@ bool EmitTerminalImpl(A64EmitX64& e, IR::Term::LinkBlock terminal, IR::LocationD
if (e.conf.HasOptimization(OptimizationFlag::BlockLinking) && !is_single_step) {
if (e.conf.enable_cycle_counting) {
e.code.cmp(qword[rsp + ABI_SHADOW_SPACE + offsetof(StackLayout, cycles_remaining)], 0);
- e.patch_information[terminal.next].jg.push_back(e.code.getCurr());
+ e.AddPatchLocation(terminal.next, EmitX64::PatchKind::Jg);
if (const auto next_bb = e.GetBasicBlock(terminal.next)) {
e.EmitPatchJg(terminal.next, next_bb->entrypoint);
} else {
@@ -636,7 +636,7 @@ bool EmitTerminalImpl(A64EmitX64& e, IR::Term::LinkBlock terminal, IR::LocationD
}
} else {
e.code.cmp(dword[e.code.ABI_JIT_PTR + offsetof(A64JitState, halt_reason)], 0);
- e.patch_information[terminal.next].jz.push_back(e.code.getCurr());
+ e.AddPatchLocation(terminal.next, EmitX64::PatchKind::Jz);
if (const auto next_bb = e.GetBasicBlock(terminal.next)) {
e.EmitPatchJz(terminal.next, next_bb->entrypoint);
} else {
@@ -656,7 +656,7 @@ bool EmitTerminalImpl(A64EmitX64& e, IR::Term::LinkBlock terminal, IR::LocationD
bool EmitTerminalImpl(A64EmitX64& e, IR::Term::LinkBlockFast terminal, IR::LocationDescriptor, bool is_single_step) {
if (e.conf.HasOptimization(OptimizationFlag::BlockLinking) && !is_single_step) {
- e.patch_information[terminal.next].jmp.push_back(e.code.getCurr());
+ e.AddPatchLocation(terminal.next, EmitX64::PatchKind::Jmp);
if (auto next_bb = e.GetBasicBlock(terminal.next)) {
e.EmitPatchJmp(terminal.next, next_bb->entrypoint);
} else {
diff --git a/src/dynarmic/src/dynarmic/backend/x64/a64_emit_x64.h b/src/dynarmic/src/dynarmic/backend/x64/a64_emit_x64.h
index 1310b4ad34..a9f2d9b650 100644
--- a/src/dynarmic/src/dynarmic/backend/x64/a64_emit_x64.h
+++ b/src/dynarmic/src/dynarmic/backend/x64/a64_emit_x64.h
@@ -83,13 +83,17 @@ public:
// Fastmem information
using DoNotFastmemMarker = std::tuple<IR::LocationDescriptor, unsigned>;
+ /// Where a fastmem access that faults goes: code addresses are offsets in the code space.
struct FastmemPatchInfo {
- u64 resume_rip;
- u64 callback;
- DoNotFastmemMarker marker;
- bool recompile;
+ u32 rip;
+ u32 resume_rip;
+ u32 callback;
+ u32 inst_name : 31;
+ u32 recompile : 1;
+ IR::LocationDescriptor location;
};
std::optional<DoNotFastmemMarker> ShouldFastmem(A64EmitContext& ctx, IR::Inst* inst) const;
+ void AddFastmemPatch(CodePtr location, CodePtr resume_location, void (*callback)(), const DoNotFastmemMarker& marker, bool recompile);
FakeCall FastmemCallback(u64 rip);
// Memory access helpers
@@ -123,7 +127,7 @@ public:
RegAlloc reg_alloc; //reusable reg alloc
BlockRangeInformation<u64> block_ranges;
std::array<FastDispatchEntry, fast_dispatch_table_size> fast_dispatch_table;
- ::Common::unordered_map<u64, FastmemPatchInfo> fastmem_patch_info;
+ std::vector<FastmemPatchInfo> fastmem_patch_info; // sorted by rip
::Common::unordered_map<std::tuple<bool, size_t, int, int>, void (*)()> read_fallbacks;
::Common::unordered_map<std::tuple<bool, size_t, int, int>, void (*)()> write_fallbacks;
::Common::unordered_map<std::tuple<bool, size_t, int, int>, void (*)()> exclusive_write_fallbacks;
diff --git a/src/dynarmic/src/dynarmic/backend/x64/block_of_code.cpp b/src/dynarmic/src/dynarmic/backend/x64/block_of_code.cpp
index 384d6203a4..57001d4ef0 100644
--- a/src/dynarmic/src/dynarmic/backend/x64/block_of_code.cpp
+++ b/src/dynarmic/src/dynarmic/backend/x64/block_of_code.cpp
@@ -60,6 +60,34 @@ namespace {
constexpr size_t CONSTANT_POOL_SIZE = 2 * 1024 * 1024;
constexpr size_t PRELUDE_COMMIT_SIZE = 16 * 1024 * 1024;
+#ifdef _WIN32
+constexpr size_t COMMIT_STEP = 4 * 1024 * 1024;
+
+// Reserves the code space; EnsureMemoryCommitted commits it as code is emitted. Xbyak's default
+// allocator commits all of it up front: 512 MiB per core, 3 GiB of commit charge for six cores
+// of which a game fills a fraction.
+struct ReservingAllocator final : Xbyak::Allocator {
+ uint8_t* alloc(size_t size) override {
+ return static_cast<uint8_t*>(VirtualAlloc(nullptr, size, MEM_RESERVE, PAGE_NOACCESS));
+ }
+ void free(uint8_t* p) override {
+ VirtualFree(p, 0, MEM_RELEASE);
+ }
+ bool useProtect() const override {
+ return false;
+ }
+};
+
+Xbyak::Allocator* CodeAllocator() {
+ // Never destroyed: code blocks may outlive static destruction.
+ static auto* const allocator = new ReservingAllocator;
+ return allocator;
+}
+#else
+Xbyak::Allocator* CodeAllocator() {
+ return nullptr;
+}
+#endif
#ifdef DYNARMIC_ENABLE_NO_EXECUTE_SUPPORT
void ProtectMemory(const void* base, size_t size, bool is_executable) {
@@ -149,7 +177,7 @@ BlockOfCode::BlockOfCode(RunCodeCallbacks cb, JitStateInfo jsi, size_t total_cod
#else
, nullptr //Allow RWE
#endif
- , nullptr)
+ , CodeAllocator())
, constant_pool(*this, CONSTANT_POOL_SIZE)
, jsi(jsi)
, cb(std::move(cb))
@@ -206,7 +234,9 @@ size_t BlockOfCode::SpaceRemaining() const {
void BlockOfCode::EnsureMemoryCommitted([[maybe_unused]] size_t codesize) {
#ifdef _WIN32
if (committed_size < size_ + codesize) {
- committed_size = std::min<size_t>(maxSize_, committed_size + codesize);
+ // The code space is only reserved (CodeAllocator), so everything up to the end of the next
+ // emission has to be committed; in steps, as each VirtualAlloc call is a kernel call.
+ committed_size = std::min<size_t>(maxSize_, (std::max)(committed_size + COMMIT_STEP, size_ + codesize));
# ifdef DYNARMIC_ENABLE_NO_EXECUTE_SUPPORT
VirtualAlloc(top_, committed_size, MEM_COMMIT, PAGE_READWRITE);
# else
diff --git a/src/dynarmic/src/dynarmic/backend/x64/emit_x64.cpp b/src/dynarmic/src/dynarmic/backend/x64/emit_x64.cpp
index b9df3987ca..84b9888bf5 100644
--- a/src/dynarmic/src/dynarmic/backend/x64/emit_x64.cpp
+++ b/src/dynarmic/src/dynarmic/backend/x64/emit_x64.cpp
@@ -102,7 +102,7 @@ void EmitX64::PushRSBHelper(Xbyak::Reg64 loc_desc_reg, Xbyak::Reg64 index_reg, I
code.mov(index_reg.cvt32(), dword[code.ABI_JIT_PTR + code.GetJitStateInfo().offsetof_rsb_ptr]);
code.mov(loc_desc_reg, target.Value());
- patch_information[target].mov_rcx.push_back(code.getCurr());
+ AddPatchLocation(target, PatchKind::MovRcx);
EmitPatchMovRcx(target_code_ptr);
code.mov(qword[code.ABI_JIT_PTR + index_reg * 8 + code.GetJitStateInfo().offsetof_rsb_location_descriptors], loc_desc_reg);
code.mov(qword[code.ABI_JIT_PTR + index_reg * 8 + code.GetJitStateInfo().offsetof_rsb_codeptrs], rcx);
@@ -357,37 +357,61 @@ EmitX64::BlockDescriptor EmitX64::RegisterBlock(const IR::LocationDescriptor& de
return block_desc;
}
-void EmitX64::Patch(const IR::LocationDescriptor& target_desc, CodePtr target_code_ptr) {
- const CodePtr save_code_ptr = code.getCurr();
- const PatchInformation& patch_info = patch_information[target_desc];
-
- for (CodePtr location : patch_info.jg) {
- code.SetCodePtr(location);
- EmitPatchJg(target_desc, target_code_ptr);
+void EmitX64::AddPatchLocation(const IR::LocationDescriptor& target_desc, PatchKind kind) {
+ const size_t offset = code.getCurr<const u8*>() - static_cast<const u8*>(code.getCode());
+ ASSERT(offset > 0 && offset < (size_t(1) << 30));
+ const u32 entry = u32(offset << 2) | u32(kind);
+ PatchInformation& patch_info = patch_information[target_desc];
+ for (u32& slot : patch_info.first) {
+ if (slot == 0) {
+ slot = entry;
+ return;
+ }
}
-
- for (CodePtr location : patch_info.jz) {
- code.SetCodePtr(location);
- EmitPatchJz(target_desc, target_code_ptr);
+ if (!patch_info.rest) {
+ patch_info.rest = std::make_unique<std::vector<u32>>();
}
+ patch_info.rest->push_back(entry);
+}
- for (CodePtr location : patch_info.jmp) {
- code.SetCodePtr(location);
- EmitPatchJmp(target_desc, target_code_ptr);
+void EmitX64::Patch(const IR::LocationDescriptor& target_desc, CodePtr target_code_ptr) {
+ const auto it = patch_information.find(target_desc);
+ if (it == patch_information.end()) {
+ return;
}
-
- for (CodePtr location : patch_info.mov_rcx) {
- code.SetCodePtr(location);
- EmitPatchMovRcx(target_code_ptr);
+ const CodePtr save_code_ptr = code.getCurr();
+ const auto patch = [&](u32 entry) {
+ code.SetCodePtr(static_cast<const u8*>(code.getCode()) + (entry >> 2));
+ switch (PatchKind(entry & 3)) {
+ case PatchKind::Jg:
+ EmitPatchJg(target_desc, target_code_ptr);
+ break;
+ case PatchKind::Jz:
+ EmitPatchJz(target_desc, target_code_ptr);
+ break;
+ case PatchKind::Jmp:
+ EmitPatchJmp(target_desc, target_code_ptr);
+ break;
+ case PatchKind::MovRcx:
+ EmitPatchMovRcx(target_code_ptr);
+ break;
+ }
+ };
+ for (const u32 entry : it->second.first) {
+ if (entry != 0) {
+ patch(entry);
+ }
+ }
+ if (it->second.rest) {
+ for (const u32 entry : *it->second.rest) {
+ patch(entry);
+ }
}
-
code.SetCodePtr(save_code_ptr);
}
void EmitX64::Unpatch(const IR::LocationDescriptor& target_desc) {
- if (patch_information.count(target_desc)) {
- Patch(target_desc, nullptr);
- }
+ Patch(target_desc, nullptr);
}
void EmitX64::ClearCache() {
diff --git a/src/dynarmic/src/dynarmic/backend/x64/emit_x64.h b/src/dynarmic/src/dynarmic/backend/x64/emit_x64.h
index 5e540e5f70..327cac7668 100644
--- a/src/dynarmic/src/dynarmic/backend/x64/emit_x64.h
+++ b/src/dynarmic/src/dynarmic/backend/x64/emit_x64.h
@@ -116,12 +116,16 @@ public:
virtual bool EmitTerminal(IR::Term::Terminal const& terminal, IR::LocationDescriptor initial_location, bool is_single_step) noexcept = 0;
// Patching
+ enum class PatchKind : u32 { Jg, Jz, Jmp, MovRcx };
+ /// Places in emitted code that jump to a block (or load its entrypoint), patched when it is
+ /// compiled or invalidated: each the place's offset in the code space times four plus its
+ /// PatchKind, 0 when unused. Most blocks are linked from one or two places.
struct PatchInformation {
- boost::container::small_vector<CodePtr, 4> jg; //4*8=32
- boost::container::small_vector<CodePtr, 4> jz; //4*8=32
- boost::container::small_vector<CodePtr, 4> jmp; //4*8=32
- boost::container::small_vector<CodePtr, 4> mov_rcx; //4*8=32
+ std::array<u32, 2> first{};
+ std::unique_ptr<std::vector<u32>> rest;
};
+ /// Records that the code about to be emitted at the current position is a patch of `kind`.
+ void AddPatchLocation(const IR::LocationDescriptor& target_desc, PatchKind kind);
void Patch(const IR::LocationDescriptor& target_desc, CodePtr target_code_ptr);
virtual void Unpatch(const IR::LocationDescriptor& target_desc);
virtual void EmitPatchJg(const IR::LocationDescriptor& target_desc, CodePtr target_code_ptr = nullptr) = 0;
diff --git a/src/dynarmic/src/dynarmic/backend/x64/emit_x64_memory.cpp.inc b/src/dynarmic/src/dynarmic/backend/x64/emit_x64_memory.cpp.inc
index 8bb8be3d5b..689e507abe 100644
--- a/src/dynarmic/src/dynarmic/backend/x64/emit_x64_memory.cpp.inc
+++ b/src/dynarmic/src/dynarmic/backend/x64/emit_x64_memory.cpp.inc
@@ -30,15 +30,48 @@ std::optional<AxxEmitX64::DoNotFastmemMarker> AxxEmitX64::ShouldFastmem(AxxEmitC
return marker;
}
+void AxxEmitX64::AddFastmemPatch(CodePtr location, CodePtr resume_location, void (*callback)(), const DoNotFastmemMarker& marker, bool recompile) {
+ const u64 base = std::bit_cast<u64>(code.getCode());
+ const auto offset = [base](u64 address) {
+ ASSERT(address >= base && address - base <= UINT32_MAX);
+ return u32(address - base);
+ };
+ FastmemPatchInfo info{
+ .rip = offset(std::bit_cast<u64>(location)),
+ .resume_rip = offset(std::bit_cast<u64>(resume_location)),
+ .callback = offset(std::bit_cast<u64>(callback)),
+ .inst_name = 0,
+ .recompile = 0,
+ .location = std::get<0>(marker),
+ };
+ info.inst_name = std::get<1>(marker);
+ info.recompile = recompile ? 1 : 0;
+ // Code is emitted in address order; only a block's deferred fallbacks can come after a later
+ // access of the same block.
+ auto it = fastmem_patch_info.end();
+ if (!fastmem_patch_info.empty() && fastmem_patch_info.back().rip >= info.rip) {
+ it = std::lower_bound(fastmem_patch_info.begin(), fastmem_patch_info.end(), info.rip,
+ [](const FastmemPatchInfo& entry, u32 value) { return entry.rip < value; });
+ if (it != fastmem_patch_info.end() && it->rip == info.rip) {
+ return;
+ }
+ }
+ fastmem_patch_info.insert(it, info);
+}
+
FakeCall AxxEmitX64::FastmemCallback(u64 rip_) {
- const auto iter = fastmem_patch_info.find(rip_);
- if (iter != fastmem_patch_info.end()) {
+ const u64 base = std::bit_cast<u64>(code.getCode());
+ const auto iter = rip_ >= base && rip_ - base <= UINT32_MAX
+ ? std::lower_bound(fastmem_patch_info.begin(), fastmem_patch_info.end(), u32(rip_ - base),
+ [](const FastmemPatchInfo& entry, u32 value) { return entry.rip < value; })
+ : fastmem_patch_info.end();
+ if (iter != fastmem_patch_info.end() && iter->rip == u32(rip_ - base)) {
FakeCall result{
- .call_rip = iter->second.callback,
- .ret_rip = iter->second.resume_rip,
+ .call_rip = base + iter->callback,
+ .ret_rip = base + iter->resume_rip,
};
- if (iter->second.recompile) {
- const auto marker = iter->second.marker;
+ if (iter->recompile) {
+ const DoNotFastmemMarker marker{iter->location, static_cast<unsigned>(iter->inst_name)};
do_not_fastmem.insert(marker);
InvalidateBasicBlocks({std::get<0>(marker)});
}
@@ -99,14 +132,7 @@ void AxxEmitX64::EmitMemoryRead(AxxEmitContext& ctx, IR::Inst* inst) {
code.L(*abort);
code.call(wrapped_fn);
- fastmem_patch_info.emplace(
- std::bit_cast<u64>(location),
- FastmemPatchInfo{
- std::bit_cast<u64>(code.getCurr()),
- std::bit_cast<u64>(wrapped_fn),
- *fastmem_marker,
- conf.recompile_on_fastmem_failure,
- });
+ AddFastmemPatch(location, code.getCurr(), wrapped_fn, *fastmem_marker, conf.recompile_on_fastmem_failure);
EmitCheckMemoryAbort(ctx, inst, end);
code.jmp(*end, code.T_NEAR);
@@ -186,14 +212,7 @@ void AxxEmitX64::EmitMemoryWrite(AxxEmitContext& ctx, IR::Inst* inst) {
code.L(*abort);
code.call(wrapped_fn);
- fastmem_patch_info.emplace(
- std::bit_cast<u64>(location),
- FastmemPatchInfo{
- std::bit_cast<u64>(code.getCurr()),
- std::bit_cast<u64>(wrapped_fn),
- *fastmem_marker,
- conf.recompile_on_fastmem_failure,
- });
+ AddFastmemPatch(location, code.getCurr(), wrapped_fn, *fastmem_marker, conf.recompile_on_fastmem_failure);
EmitCheckMemoryAbort(ctx, inst, end);
code.jmp(*end, code.T_NEAR);
@@ -357,14 +376,7 @@ void AxxEmitX64::EmitExclusiveReadMemoryInline(AxxEmitContext& ctx, IR::Inst* in
const auto location = EmitReadMemoryMov<bitsize>(code, value_idx, src_ptr, ordered);
- fastmem_patch_info.emplace(
- std::bit_cast<u64>(location),
- FastmemPatchInfo{
- std::bit_cast<u64>(code.getCurr()),
- std::bit_cast<u64>(wrapped_fn),
- *fastmem_marker,
- conf.recompile_on_exclusive_fastmem_failure,
- });
+ AddFastmemPatch(location, code.getCurr(), wrapped_fn, *fastmem_marker, conf.recompile_on_exclusive_fastmem_failure);
code.L(*end);
@@ -495,14 +507,7 @@ void AxxEmitX64::EmitExclusiveWriteMemoryInline(AxxEmitContext& ctx, IR::Inst* i
code.L(*abort);
code.call(wrapped_fn);
- fastmem_patch_info.emplace(
- std::bit_cast<u64>(location),
- FastmemPatchInfo{
- std::bit_cast<u64>(code.getCurr()),
- std::bit_cast<u64>(wrapped_fn),
- *fastmem_marker,
- conf.recompile_on_exclusive_fastmem_failure,
- });
+ AddFastmemPatch(location, code.getCurr(), wrapped_fn, *fastmem_marker, conf.recompile_on_exclusive_fastmem_failure);
code.xor_(status.cvt32(), status.cvt32()); //dep-break
code.test(code.al, code.al);
diff --git a/src/dynarmic/src/dynarmic/backend/x64/reg_alloc.cpp b/src/dynarmic/src/dynarmic/backend/x64/reg_alloc.cpp
index 5a828839c1..4f3d253441 100644
--- a/src/dynarmic/src/dynarmic/backend/x64/reg_alloc.cpp
+++ b/src/dynarmic/src/dynarmic/backend/x64/reg_alloc.cpp
@@ -465,22 +465,24 @@ std::optional<HostLoc> RegAlloc::ValueLocation(const IR::Inst* value) const noex
return std::nullopt;
}
+// The checks that scan every location for def_inst are debug-only: per IR instruction they cost as
+// much as the rest of register allocation.
void RegAlloc::DefineValueImpl(BlockOfCode& code, IR::Inst* def_inst, HostLoc host_loc) noexcept {
- ASSERT(!ValueLocation(def_inst) && "def_inst has already been defined");
+ DEBUG_ASSERT(!ValueLocation(def_inst) && "def_inst has already been defined");
LocInfo(host_loc).AddValue(host_loc, def_inst);
- ASSERT(*ValueLocation(def_inst) == host_loc);
+ DEBUG_ASSERT(*ValueLocation(def_inst) == host_loc);
}
void RegAlloc::DefineValueImpl(BlockOfCode& code, IR::Inst* def_inst, const IR::Value& use_inst) noexcept {
- ASSERT(!ValueLocation(def_inst) && "def_inst has already been defined");
+ DEBUG_ASSERT(!ValueLocation(def_inst) && "def_inst has already been defined");
if (use_inst.IsImmediate()) {
const HostLoc location = ScratchImpl(code, gpr_order);
DefineValueImpl(code, def_inst, location);
LoadImmediate(code, use_inst, location);
} else {
- ASSERT(ValueLocation(use_inst.GetInst()) && "use_inst must already be defined");
- const HostLoc location = *ValueLocation(use_inst.GetInst());
- DefineValueImpl(code, def_inst, location);
+ const auto location = ValueLocation(use_inst.GetInst());
+ ASSERT(location && "use_inst must already be defined");
+ DefineValueImpl(code, def_inst, *location);
}
}
diff --git a/src/dynarmic/src/dynarmic/backend/x64/reg_alloc.h b/src/dynarmic/src/dynarmic/backend/x64/reg_alloc.h
index 455c6a970a..0cad07796c 100644
--- a/src/dynarmic/src/dynarmic/backend/x64/reg_alloc.h
+++ b/src/dynarmic/src/dynarmic/backend/x64/reg_alloc.h
@@ -65,6 +65,11 @@ public:
}
void ReleaseOne() noexcept;
void ReleaseAll() noexcept;
+ /// True when ReleaseAll would change nothing, as for most of the spill slots most of the time.
+ inline bool IsReleased() const noexcept {
+ return current_references == 0 && is_being_used_count == 0 && !is_scratch && !is_set_last_use &&
+ (total_uses != accumulated_uses || (values.empty() && total_uses == 0 && max_bit_width == 0));
+ }
constexpr size_t GetMaxBitWidth() const noexcept { return 1 << max_bit_width; }
void AddValue(HostLoc loc, IR::Inst* inst) noexcept;
/// Checks if the given instruction is in our values set
@@ -201,8 +206,10 @@ public:
void ReleaseStackSpace(BlockOfCode& code, const size_t stack_space) noexcept;
inline void EndOfAllocScope() noexcept {
+ // After every IR instruction: skip the locations it left alone.
for (auto& iter : hostloc_info)
- iter.ReleaseAll();
+ if (!iter.IsReleased())
+ iter.ReleaseAll();
}
inline void AssertNoMoreUses() noexcept {
ASSERT(std::all_of(hostloc_info.begin(), hostloc_info.end(), [](const auto& i) noexcept { return i.IsEmpty(); }));
diff --git a/src/input_common/drivers/sdl_driver.cpp b/src/input_common/drivers/sdl_driver.cpp
index c3e9a066b3..a308554071 100644
--- a/src/input_common/drivers/sdl_driver.cpp
+++ b/src/input_common/drivers/sdl_driver.cpp
@@ -3,6 +3,7 @@
// SPDX-FileCopyrightText: 2018 Citra Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
+#include <algorithm>
#include <array>
#include <numbers>
#include "common/logging.h"
@@ -115,7 +116,15 @@ public:
}
}
- LOG_INFO(Input, "Controller motion capabilities: accel={} gyro={}", has_accel, has_gyro);
+ // EnableMotion is also called to recover a stalled sensor, so only log when the reported
+ // capabilities actually change instead of on every restart.
+ if (!motion_capabilities_logged || logged_accel != has_accel || logged_gyro != has_gyro) {
+ LOG_INFO(Input, "Controller motion capabilities: accel={} gyro={}", has_accel,
+ has_gyro);
+ motion_capabilities_logged = true;
+ logged_accel = has_accel;
+ logged_gyro = has_gyro;
+ }
}
bool HasMotion() const {
@@ -163,16 +172,21 @@ public:
// Motion data is invalid
if (motion.accel_x == 0 && motion.gyro_x == 0 && motion.accel_y == 0 &&
motion.gyro_y == 0 && motion.accel_z == 0 && motion.gyro_z == 0) {
- if (motion_error_count++ < 200) {
+ if (motion_error_count++ < motion_restart_threshold) {
return false;
}
- // Try restarting the sensor
+ // Try restarting the sensor. Back off exponentially so a sensor that keeps reporting
+ // zeros (observed on the Steam Deck) isn't toggled several times a
+ // second for the whole session.
motion_error_count = 0;
+ motion_restart_threshold =
+ std::min(motion_restart_threshold * 2, MaxMotionRestartThreshold);
EnableMotion();
return false;
}
motion_error_count = 0;
+ motion_restart_threshold = InitialMotionRestartThreshold;
motion.delta_timestamp = time_difference;
return true;
}
@@ -393,7 +407,13 @@ private:
mutable std::mutex mutex;
u64 last_motion_update{};
+ static constexpr std::size_t InitialMotionRestartThreshold = 200;
+ static constexpr std::size_t MaxMotionRestartThreshold = 200 * 64;
std::size_t motion_error_count{};
+ std::size_t motion_restart_threshold{InitialMotionRestartThreshold};
+ bool motion_capabilities_logged{false};
+ bool logged_accel{false};
+ bool logged_gyro{false};
bool has_gyro{false};
bool has_accel{false};
bool has_vibration{false};
diff --git a/src/input_common/drivers/udp_client.cpp b/src/input_common/drivers/udp_client.cpp
index 620e0cf630..a7840db32f 100644
--- a/src/input_common/drivers/udp_client.cpp
+++ b/src/input_common/drivers/udp_client.cpp
@@ -603,7 +603,7 @@ CalibrationConfigurationJob::CalibrationConfigurationJob(
const std::string& host, u16 port, std::function<void(Status)> status_callback,
std::function<void(u16, u16, u16, u16)> data_callback) {
- std::thread([=, this] {
+ worker = std::jthread([=, this] {
u16 min_x{UINT16_MAX};
u16 min_y{UINT16_MAX};
u16 max_x{};
@@ -647,7 +647,7 @@ CalibrationConfigurationJob::CalibrationConfigurationJob(
complete_event.Wait();
socket.Stop();
worker_thread.join();
- }).detach();
+ });
}
CalibrationConfigurationJob::~CalibrationConfigurationJob() {
diff --git a/src/input_common/drivers/udp_client.h b/src/input_common/drivers/udp_client.h
index cea9f579ac..eac3df07aa 100644
--- a/src/input_common/drivers/udp_client.h
+++ b/src/input_common/drivers/udp_client.h
@@ -183,6 +183,7 @@ public:
private:
Common::Event complete_event;
+ std::jthread worker;
};
void TestCommunication(const std::string& host, u16 port,
diff --git a/src/qt_common/config/uisettings.cpp b/src/qt_common/config/uisettings.cpp
index 6f1e7df3d5..8d593b8d9e 100644
--- a/src/qt_common/config/uisettings.cpp
+++ b/src/qt_common/config/uisettings.cpp
@@ -30,7 +30,7 @@ namespace UISettings {
// This must be in alphabetical order according to action name as it must have the same order as
// UISetting::values.shortcuts, which is alphabetically ordered.
// clang-format off
-const std::array<Shortcut, 33> default_hotkeys{{
+const std::array<Shortcut, 32> default_hotkeys{{
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Mute/Unmute")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+M"), std::string("Home+Dpad_Right"), Qt::WindowShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Volume Down")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("-"), std::string("Home+Dpad_Down"), Qt::ApplicationShortcut, true}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Audio Volume Up")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("="), std::string("Home+Dpad_Up"), Qt::ApplicationShortcut, true}},
@@ -45,7 +45,6 @@ const std::array<Shortcut, 33> default_hotkeys{{
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Exit Eglantine")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+Q"), std::string("Home+Minus"), Qt::WindowShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Fullscreen")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F11"), std::string("Home+B"), Qt::WindowShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Load File")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+O"), std::string(""), Qt::WidgetWithChildrenShortcut, false}},
- {QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Load/Remove Amiibo")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("F2"), std::string("Home+A"), Qt::WidgetWithChildrenShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Browse Public Game Lobby")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+B"), std::string(""), Qt::ApplicationShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Create Room")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+N"), std::string(""), Qt::ApplicationShortcut, false}},
{QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Direct Connect to Room")).toStdString(), QStringLiteral(QT_TRANSLATE_NOOP("Hotkeys", "Main Window")).toStdString(), {std::string("Ctrl+C"), std::string(""), Qt::ApplicationShortcut, false}},
diff --git a/src/qt_common/config/uisettings.h b/src/qt_common/config/uisettings.h
index a790cfcc22..89185061cc 100644
--- a/src/qt_common/config/uisettings.h
+++ b/src/qt_common/config/uisettings.h
@@ -244,7 +244,7 @@ void SaveWindowState();
void RestoreWindowState(std::unique_ptr<QtConfig>& qtConfig);
// sync with uisettings.cpp
-extern const std::array<Shortcut, 33> default_hotkeys;
+extern const std::array<Shortcut, 32> default_hotkeys;
} // namespace UISettings
diff --git a/src/qt_common/game_list/discovery.h b/src/qt_common/game_list/discovery.h
new file mode 100644
index 0000000000..8d2054d308
--- /dev/null
+++ b/src/qt_common/game_list/discovery.h
@@ -0,0 +1,113 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <algorithm>
+#include <cctype>
+#include <filesystem>
+#include <unordered_set>
+#include <utility>
+#include <vector>
+
+namespace GameListDiscovery {
+
+struct Result {
+ std::vector<std::filesystem::path> files;
+ std::vector<std::filesystem::path> directories;
+ std::size_t skipped_romfs{};
+ std::size_t errors{};
+};
+
+inline bool IsExeFS(const std::filesystem::path& path) {
+ std::error_code ec;
+ return std::filesystem::is_regular_file(path / "main", ec) &&
+ std::filesystem::is_regular_file(path / "main.npdm", ec);
+}
+
+template <typename Supported, typename Stopped>
+Result Discover(const std::filesystem::path& root, bool deep, Supported supported,
+ Stopped stopped) {
+ namespace fs = std::filesystem;
+ Result result;
+ std::unordered_set<fs::path> files;
+ std::unordered_set<fs::path> directories;
+ const auto add_file = [&](const fs::path& path) {
+ if (files.insert(path).second) {
+ result.files.push_back(path);
+ }
+ };
+ const auto watch = [&](const fs::path& path) {
+ if (directories.insert(path).second) {
+ result.directories.push_back(path);
+ }
+ };
+ const auto probe = [&](const fs::path& path) {
+ if (IsExeFS(path)) {
+ watch(path);
+ add_file(path / "main");
+ }
+ };
+ const auto probe_layouts = [&](const fs::path& path) {
+ for (const auto& content : {path, path / "base"}) {
+ probe(content / "exefs");
+ const auto updates = content / "updates";
+ std::error_code ec;
+ if (!fs::is_directory(updates, ec)) {
+ continue;
+ }
+ watch(updates);
+ for (fs::directory_iterator it{updates, fs::directory_options::skip_permission_denied,
+ ec};
+ !ec && it != fs::directory_iterator{} && !stopped(); it.increment(ec)) {
+ probe(it->path() / "exefs");
+ }
+ }
+ };
+
+ std::vector<std::pair<fs::path, unsigned>> pending{{root, 0}};
+ while (!pending.empty() && !stopped()) {
+ auto [directory, depth] = std::move(pending.back());
+ pending.pop_back();
+ watch(directory);
+ if (IsExeFS(directory)) {
+ add_file(directory / "main");
+ continue;
+ }
+ if (!deep) {
+ probe_layouts(directory);
+ }
+ std::error_code ec;
+ for (fs::directory_iterator it{directory, fs::directory_options::skip_permission_denied,
+ ec};
+ !ec && it != fs::directory_iterator{} && !stopped(); it.increment(ec)) {
+ const auto& entry = *it;
+ std::error_code entry_ec;
+ if (entry.is_directory(entry_ec)) {
+ if (entry.is_symlink(entry_ec)) {
+ continue;
+ }
+ auto name = entry.path().filename().u8string();
+ std::transform(name.begin(), name.end(), name.begin(), [](char8_t c) {
+ return static_cast<char8_t>(std::tolower(static_cast<unsigned char>(c)));
+ });
+ if (name == u8"romfs" && IsExeFS(directory / "exefs")) {
+ ++result.skipped_romfs;
+ continue;
+ }
+ if (deep || depth == 0) {
+ pending.emplace_back(entry.path(), depth + 1);
+ }
+ } else if (entry.is_regular_file(entry_ec) && depth == 0 && !deep) {
+ if (supported(entry.path())) {
+ add_file(entry.path());
+ }
+ } else if (entry.is_regular_file(entry_ec) && deep && supported(entry.path())) {
+ add_file(entry.path());
+ }
+ }
+ result.errors += ec ? 1 : 0;
+ }
+ return result;
+}
+
+} // namespace GameListDiscovery
diff --git a/src/qt_common/game_list/model.cpp b/src/qt_common/game_list/model.cpp
index 4f3916b04d..23d8ec1f43 100644
--- a/src/qt_common/game_list/model.cpp
+++ b/src/qt_common/game_list/model.cpp
@@ -44,9 +44,21 @@ GameListModel::GameListModel(std::shared_ptr<FileSys::VfsFilesystem> vfs_,
setSortRole(GameListItemPath::SortRole);
}
-GameListModel::~GameListModel() = default;
+GameListModel::~GameListModel() {
+ StopWorker();
+}
+
+void GameListModel::SetScanEnabled(bool enabled) {
+ scan_enabled = enabled;
+ if (!enabled) {
+ StopWorker();
+ }
+}
void GameListModel::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
+ if (!scan_enabled || system.IsPoweredOn()) {
+ return;
+ }
emit PopulatingStarted();
StopWorker();
@@ -62,11 +74,14 @@ void GameListModel::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
}
void GameListModel::WorkerEvent() {
- current_worker->ProcessEvents(this);
+ if (current_worker) {
+ current_worker->ProcessEvents(this);
+ }
}
void GameListModel::AddDirEntry(GameListDir* entry_items) {
if (m_flat) {
+ delete entry_items;
return;
}
invisibleRootItem()->appendRow(entry_items);
@@ -172,7 +187,7 @@ void GameListModel::StopWorker() {
void GameListModel::Repopulate() {
// CreateFactories replaces the content providers a running game reads from. Stopping
// emulation repopulates the list anyway, so skip watcher-driven reloads until then.
- if (QtCommon::system->IsPoweredOn()) {
+ if (!scan_enabled || QtCommon::system->IsPoweredOn()) {
LOG_INFO(Frontend, "Emulation is running, deferring game list reload.");
return;
}
diff --git a/src/qt_common/game_list/model.h b/src/qt_common/game_list/model.h
index b850895d2f..e664623640 100644
--- a/src/qt_common/game_list/model.h
+++ b/src/qt_common/game_list/model.h
@@ -51,6 +51,7 @@ public:
void PopulateAsync(QVector<UISettings::GameDir>& game_dirs);
void WorkerEvent();
+ void SetScanEnabled(bool enabled);
bool IsEmpty() const;
@@ -81,6 +82,7 @@ private:
void StopWorker();
bool m_flat = false;
+ bool scan_enabled = true;
std::shared_ptr<FileSys::VfsFilesystem> vfs;
FileSys::ManualContentProvider* provider;
diff --git a/src/qt_common/game_list/worker.cpp b/src/qt_common/game_list/worker.cpp
index d18f84089a..bd3d81dac4 100644
--- a/src/qt_common/game_list/worker.cpp
+++ b/src/qt_common/game_list/worker.cpp
@@ -5,6 +5,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
+#include <chrono>
#include <filesystem>
#include <memory>
#include <string>
@@ -36,10 +37,23 @@
#include "qt_common/game_list/game_list_p.h"
#include "qt_common/game_list/model.h"
+#include "qt_common/game_list/discovery.h"
#include "qt_common/game_list/worker.h"
namespace {
+struct PendingRow {
+ explicit PendingRow(QList<QStandardItem*> row) : items{std::move(row)} {}
+ PendingRow(const PendingRow&) = delete;
+ QList<QStandardItem*> items;
+ ~PendingRow() {
+ qDeleteAll(items);
+ }
+ QList<QStandardItem*> Release() {
+ return std::exchange(items, {});
+ }
+};
+
QString GetGameListCachedObject(const std::string& filename, const std::string& ext,
const std::function<QString()>& generator) {
if (!UISettings::values.cache_game_list || filename == "0000000000000000") {
@@ -212,36 +226,6 @@ QString LooseExeFSName(const std::string& physical_name) {
return name;
}
-// A loose dump's game list entry is its small main executable, so the size column would only
-// show a few dozen MB. Sum the dump's real contents instead: the ExeFS directory plus the
-// RomFS directory next to it, matching the loader's loose RomFS discovery.
-u64 LooseDumpSize(const std::filesystem::path& main_path) {
- const auto exefs_dir = main_path.parent_path();
-
- u64 total{};
- std::error_code ec;
- const auto accumulate = [&total, &ec](const std::filesystem::path& dir) {
- for (std::filesystem::recursive_directory_iterator it{
- dir, std::filesystem::directory_options::skip_permission_denied, ec};
- !ec && it != std::filesystem::recursive_directory_iterator(); it.increment(ec)) {
- std::error_code entry_ec;
- if (it->is_regular_file(entry_ec)) {
- const auto size = it->file_size(entry_ec);
- if (!entry_ec) {
- total += size;
- }
- }
- }
- };
-
- accumulate(exefs_dir);
- const auto romfs_dir = exefs_dir.parent_path() / "romfs";
- if (std::filesystem::is_directory(romfs_dir, ec)) {
- accumulate(romfs_dir);
- }
- return total;
-}
-
QString FormatPatchNameVersions(const FileSys::PatchManager& patch_manager,
Loader::AppLoader& loader, bool updatable = true) {
QString out;
@@ -359,7 +343,7 @@ void GameListWorker::RecordEvent(F&& func) {
emit DataAvailable();
}
-void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
+void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir, GameListItemType parent_type) {
using namespace FileSys;
const auto& cache = system.GetContentProviderUnion();
@@ -367,13 +351,13 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
auto installed_games = cache.ListEntriesFilterOrigin(std::nullopt, TitleType::Application,
ContentRecordType::Program);
- if (parent_dir->type() == static_cast<int>(GameListItemType::SdmcDir)) {
+ if (parent_type == GameListItemType::SdmcDir) {
installed_games = cache.ListEntriesFilterOrigin(
ContentProviderUnionSlot::SDMC, TitleType::Application, ContentRecordType::Program);
- } else if (parent_dir->type() == static_cast<int>(GameListItemType::UserNandDir)) {
+ } else if (parent_type == GameListItemType::UserNandDir) {
installed_games = cache.ListEntriesFilterOrigin(
ContentProviderUnionSlot::UserNAND, TitleType::Application, ContentRecordType::Program);
- } else if (parent_dir->type() == static_cast<int>(GameListItemType::SysNandDir)) {
+ } else if (parent_type == GameListItemType::SysNandDir) {
installed_games = cache.ListEntriesFilterOrigin(
ContentProviderUnionSlot::SysNAND, TitleType::Application, ContentRecordType::Program);
}
@@ -408,11 +392,12 @@ void GameListWorker::AddTitlesToGameList(GameListDir* parent_dir) {
auto entry = MakeGameListEntry(file->GetFullPath(), name, file->GetSize(), icon, *loader,
program_id, play_time_manager, patch);
- RecordEvent([=](GameListModel* model) { model->AddEntry(entry, parent_dir); });
+ auto pending = std::make_shared<PendingRow>(std::move(entry));
+ RecordEvent([=](GameListModel* model) { model->AddEntry(pending->Release(), parent_dir); });
}
}
-void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
+void GameListWorker::ScanFileSystem(ScanTarget target, const std::vector<std::filesystem::path>& files,
GameListDir* parent_dir) {
const auto process_candidate = [this, target,
parent_dir](const std::filesystem::path& path) {
@@ -434,7 +419,8 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
if (target == ScanTarget::PopulateGameList &&
(file_type == Loader::FileType::XCI || file_type == Loader::FileType::XCZ ||
- file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ) &&
+ file_type == Loader::FileType::NSP || file_type == Loader::FileType::NSZ ||
+ file_type == Loader::FileType::NUP) &&
!Loader::IsBootableGameContainer(file, file_type)) {
return;
}
@@ -458,7 +444,7 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
std::vector<u64> program_ids;
loader->ReadProgramIds(program_ids);
- const auto addEntry = [this, &path, physical_name,
+ const auto addEntry = [this, physical_name,
parent_dir](std::unique_ptr<Loader::AppLoader>& app_loader,
const u64 id) {
std::vector<u8> icon;
@@ -476,14 +462,21 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
const FileSys::PatchManager patch{id, system.GetFileSystemController(),
system.GetContentProvider()};
- const u64 size = IsExtractedNCAMain(physical_name)
- ? LooseDumpSize(path)
- : Common::FS::GetSize(physical_name);
+ const bool loose_dump = IsExtractedNCAMain(physical_name);
+ const u64 size = loose_dump ? 0 : Common::FS::GetSize(physical_name);
auto entry = MakeGameListEntry(physical_name, name, size, icon, *app_loader, id,
play_time_manager, patch);
- RecordEvent([=](GameListModel* model) { model->AddEntry(entry, parent_dir); });
+ if (loose_dump) {
+ entry[GameListModel::COLUMN_SIZE]->setText(tr("Not scanned"));
+ entry[GameListModel::COLUMN_SIZE]->setToolTip(
+ tr("Folder size is not calculated during game discovery."));
+ }
+ auto pending = std::make_shared<PendingRow>(std::move(entry));
+ RecordEvent([=](GameListModel* model) {
+ model->AddEntry(pending->Release(), parent_dir);
+ });
};
if (res2 == Loader::ResultStatus::Success && program_ids.size() > 1 &&
@@ -508,67 +501,27 @@ void GameListWorker::ScanFileSystem(ScanTarget target, const std::string& dir_pa
}
};
- const auto callback = [this, target, deep_scan,
- &process_candidate](const std::filesystem::path& path) -> bool {
+ for (const auto& path : files) {
if (stop_requested) {
- // Breaks the callback loop.
- return false;
- }
-
- if (Common::FS::IsDir(path)) {
- watch_list.append(QString::fromStdString(Common::FS::PathToUTF8String(path)));
-
- // Shallow scans stop at the first level, so probe the standard layouts of loose
- // ExeFS dumps: <game>/exefs/main, <game>/base/exefs/main and their updates in
- // <root>/updates/<title id>/exefs/main.
- if (!deep_scan && target == ScanTarget::PopulateGameList) {
- for (const auto& content_root : {path, path / "base"}) {
- const auto exefs_main = content_root / "exefs" / "main";
- if (Common::FS::IsFile(exefs_main)) {
- process_candidate(exefs_main);
- }
-
- const auto updates = content_root / "updates";
- if (Common::FS::IsDir(updates)) {
- Common::FS::IterateDirEntries(
- updates,
- [&process_candidate](const std::filesystem::path& entry) {
- const auto update_main = entry / "exefs" / "main";
- if (Common::FS::IsFile(update_main)) {
- process_candidate(update_main);
- }
- return true;
- },
- Common::FS::DirEntryFilter::Directory);
- }
- }
- }
-
- return true;
+ break;
}
-
- const auto physical_name = Common::FS::PathToUTF8String(path);
- if (HasSupportedFileExtension(physical_name) || IsExtractedNCAMain(physical_name)) {
- process_candidate(path);
+ // Loose executables cannot contribute NCA content to the manual provider.
+ if (target == ScanTarget::FillManualContentProvider &&
+ IsExtractedNCAMain(Common::FS::PathToUTF8String(path))) {
+ continue;
}
-
- return true;
- };
-
- if (deep_scan) {
- Common::FS::IterateDirEntriesRecursively(dir_path, callback,
- Common::FS::DirEntryFilter::All);
- } else {
- Common::FS::IterateDirEntries(dir_path, callback, Common::FS::DirEntryFilter::All);
+ process_candidate(path);
}
}
void GameListWorker::run() {
+ const auto started = std::chrono::steady_clock::now();
watch_list.clear();
provider->ClearAllEntries();
const auto DirEntryReady = [&](GameListDir* game_list_dir) {
- RecordEvent([=](GameListModel* model) { model->AddDirEntry(game_list_dir); });
+ auto pending = std::make_shared<std::unique_ptr<GameListDir>>(game_list_dir);
+ RecordEvent([=](GameListModel* model) { model->AddDirEntry(pending->release()); });
};
for (UISettings::GameDir& game_dir : game_dirs) {
@@ -595,17 +548,35 @@ void GameListWorker::run() {
scan = true;
}
+ const auto parent_type = static_cast<GameListItemType>(game_list_dir->type());
DirEntryReady(game_list_dir);
if (scan) {
- ScanFileSystem(ScanTarget::FillManualContentProvider, game_dir.path, game_dir.deep_scan,
- game_list_dir);
- ScanFileSystem(ScanTarget::PopulateGameList, game_dir.path, game_dir.deep_scan,
- game_list_dir);
+ const auto discovery = GameListDiscovery::Discover(
+ std::filesystem::u8path(game_dir.path), game_dir.deep_scan,
+ [](const auto& path) {
+ const auto name = Common::FS::PathToUTF8String(path);
+ return HasSupportedFileExtension(name) || IsExtractedNCAMain(name);
+ },
+ [this] { return stop_requested.load(); });
+ for (const auto& directory : discovery.directories) {
+ watch_list.append(QString::fromStdString(Common::FS::PathToUTF8String(directory)));
+ }
+ LOG_INFO(Frontend,
+ "Discovery: candidates={} directories={} skipped_romfs={} errors={} cancelled={}",
+ discovery.files.size(), discovery.directories.size(), discovery.skipped_romfs,
+ discovery.errors, stop_requested.load());
+ ScanFileSystem(ScanTarget::FillManualContentProvider, discovery.files, game_list_dir);
+ ScanFileSystem(ScanTarget::PopulateGameList, discovery.files, game_list_dir);
} else {
- AddTitlesToGameList(game_list_dir);
+ AddTitlesToGameList(game_list_dir, parent_type);
}
}
+ watch_list.removeDuplicates();
+ LOG_INFO(Frontend, "Game discovery finished: elapsed_ms={} watched={} cancelled={}",
+ std::chrono::duration_cast<std::chrono::milliseconds>(
+ std::chrono::steady_clock::now() - started).count(),
+ watch_list.size(), stop_requested.load());
RecordEvent([this](GameListModel* model) { model->DonePopulating(watch_list); });
processing_completed.Set();
}
diff --git a/src/qt_common/game_list/worker.h b/src/qt_common/game_list/worker.h
index cbc1fa7021..b627d9eb94 100644
--- a/src/qt_common/game_list/worker.h
+++ b/src/qt_common/game_list/worker.h
@@ -8,8 +8,10 @@
#include <atomic>
#include <deque>
+#include <filesystem>
#include <memory>
#include <string>
+#include <vector>
#include <QList>
#include <QObject>
@@ -28,6 +30,7 @@ class System;
class GameListDir;
class GameListModel;
class QStandardItem;
+enum class GameListItemType;
namespace FileSys {
class NCA;
@@ -73,14 +76,14 @@ private:
void RecordEvent(F&& func);
private:
- void AddTitlesToGameList(GameListDir* parent_dir);
+ void AddTitlesToGameList(GameListDir* parent_dir, GameListItemType parent_type);
enum class ScanTarget {
FillManualContentProvider,
PopulateGameList,
};
- void ScanFileSystem(ScanTarget target, const std::string& dir_path, bool deep_scan,
+ void ScanFileSystem(ScanTarget target, const std::vector<std::filesystem::path>& files,
GameListDir* parent_dir);
std::shared_ptr<FileSys::VfsFilesystem> vfs;
diff --git a/src/qt_common/qt_common.cpp b/src/qt_common/qt_common.cpp
index 975738dbe0..7ad7a477d2 100644
--- a/src/qt_common/qt_common.cpp
+++ b/src/qt_common/qt_common.cpp
@@ -58,7 +58,8 @@ std::unique_ptr<EmuThread> emu_thread = nullptr;
const QStringList supported_file_extensions = {QStringLiteral("nro"), QStringLiteral("nso"),
QStringLiteral("nca"), QStringLiteral("xci"),
QStringLiteral("xcz"), QStringLiteral("nsp"),
- QStringLiteral("nsz"), QStringLiteral("kip")};
+ QStringLiteral("nsz"), QStringLiteral("nup"),
+ QStringLiteral("kip")};
Core::Frontend::WindowSystemType GetWindowSystemType() {
// Determine WSI type based on Qt platform.
diff --git a/src/qt_common/util/game.cpp b/src/qt_common/util/game.cpp
index ddbe979765..ba669c3acf 100644
--- a/src/qt_common/util/game.cpp
+++ b/src/qt_common/util/game.cpp
@@ -430,8 +430,10 @@ void CreateShortcut(const std::string& game_path, const u64 program_id,
game_title = fmt::format("{:016X}", program_id);
if (control.first != nullptr) {
game_title = control.first->GetApplicationName();
- } else {
+ } else if (loader != nullptr) {
loader->ReadTitle(game_title);
+ } else {
+ LOG_WARNING(Frontend, "Could not load game title from {:s}", game_path);
}
}
@@ -448,7 +450,8 @@ void CreateShortcut(const std::string& game_path, const u64 program_id,
std::vector<u8> icon_image_file{};
if (control.second != nullptr) {
icon_image_file = control.second->ReadAllBytes();
- } else if (loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) {
+ } else if (loader == nullptr ||
+ loader->ReadIcon(icon_image_file) != Loader::ResultStatus::Success) {
LOG_WARNING(Frontend, "Could not read icon from {:s}", game_path);
}
@@ -535,6 +538,10 @@ void CreateHomeMenuShortcut(ShortcutTarget target) {
}
bool SaveIconToFile(const std::filesystem::path& icon_path, const QImage& image) {
+ if (image.isNull()) {
+ return false;
+ }
+
#if defined(WIN32)
#pragma pack(push, 2)
struct IconDir {
diff --git a/src/shader_recompiler/CMakeLists.txt b/src/shader_recompiler/CMakeLists.txt
index e60f26d47f..e4df9c045d 100644
--- a/src/shader_recompiler/CMakeLists.txt
+++ b/src/shader_recompiler/CMakeLists.txt
@@ -24,6 +24,7 @@ add_library(shader_recompiler STATIC
backend/spirv/emit_spirv_memory.cpp
backend/spirv/emit_spirv_select.cpp
backend/spirv/emit_spirv_shared_memory.cpp
+ backend/spirv/emit_spirv_mesh.cpp
backend/spirv/emit_spirv_special.cpp
backend/spirv/emit_spirv_undefined.cpp
backend/spirv/emit_spirv_warp.cpp
diff --git a/src/shader_recompiler/backend/glasm/emit_glasm.cpp b/src/shader_recompiler/backend/glasm/emit_glasm.cpp
index b795c01797..c18eec63cd 100644
--- a/src/shader_recompiler/backend/glasm/emit_glasm.cpp
+++ b/src/shader_recompiler/backend/glasm/emit_glasm.cpp
@@ -341,6 +341,9 @@ std::string_view StageHeader(Stage stage) {
return "!!NVfp5.0\n";
case Stage::Compute:
return "!!NVcp5.0\n";
+ case Stage::Task:
+ case Stage::Mesh:
+ break;
}
throw InvalidArgument("Invalid stage {}", stage);
}
diff --git a/src/shader_recompiler/backend/glasm/emit_glasm_instructions.h b/src/shader_recompiler/backend/glasm/emit_glasm_instructions.h
index 6f522ab5c2..d66fe2f3d6 100644
--- a/src/shader_recompiler/backend/glasm/emit_glasm_instructions.h
+++ b/src/shader_recompiler/backend/glasm/emit_glasm_instructions.h
@@ -58,6 +58,10 @@ void EmitGetAttributeIndexed(EmitContext& ctx, IR::Inst& inst, ScalarS32 offset,
void EmitSetAttributeIndexed(EmitContext& ctx, ScalarU32 offset, ScalarF32 value, ScalarU32 vertex);
void EmitGetPatch(EmitContext& ctx, IR::Inst& inst, IR::Patch patch);
void EmitSetPatch(EmitContext& ctx, IR::Patch patch, ScalarF32 value);
+void EmitIsbeRead(EmitContext& ctx);
+void EmitIsbeWriteAttribute(EmitContext& ctx);
+void EmitIsbeWriteMapU8(EmitContext& ctx);
+void EmitIsbeWriteMapU32(EmitContext& ctx);
void EmitSetFragColor(EmitContext& ctx, u32 index, u32 component, ScalarF32 value);
void EmitSetSampleMask(EmitContext& ctx, ScalarS32 value);
void EmitSetFragDepth(EmitContext& ctx, ScalarF32 value);
diff --git a/src/shader_recompiler/backend/glasm/emit_glasm_not_implemented.cpp b/src/shader_recompiler/backend/glasm/emit_glasm_not_implemented.cpp
index 1f4ffdd624..41c829ca5d 100644
--- a/src/shader_recompiler/backend/glasm/emit_glasm_not_implemented.cpp
+++ b/src/shader_recompiler/backend/glasm/emit_glasm_not_implemented.cpp
@@ -97,4 +97,20 @@ void EmitGetInBoundsFromOp(EmitContext& ctx) {
NotImplemented();
}
+void EmitIsbeRead(EmitContext& ctx) {
+ NotImplemented();
+}
+
+void EmitIsbeWriteAttribute(EmitContext& ctx) {
+ NotImplemented();
+}
+
+void EmitIsbeWriteMapU8(EmitContext& ctx) {
+ NotImplemented();
+}
+
+void EmitIsbeWriteMapU32(EmitContext& ctx) {
+ NotImplemented();
+}
+
} // namespace Shader::Backend::GLASM
diff --git a/src/shader_recompiler/backend/glasm/glasm_emit_context.cpp b/src/shader_recompiler/backend/glasm/glasm_emit_context.cpp
index 333a91cc58..02b6992b52 100644
--- a/src/shader_recompiler/backend/glasm/glasm_emit_context.cpp
+++ b/src/shader_recompiler/backend/glasm/glasm_emit_context.cpp
@@ -80,6 +80,9 @@ EmitContext::EmitContext(IR::Program& program, Bindings& bindings, const Profile
case Stage::Compute:
stage_name = "invocation";
break;
+ case Stage::Task:
+ case Stage::Mesh:
+ throw NotImplementedException("GLASM task and mesh programs");
}
const std::string_view attr_stage{stage == Stage::Fragment ? "fragment" : "vertex"};
const VaryingState loads{info.loads.mask | info.passthrough.mask};
diff --git a/src/shader_recompiler/backend/glsl/emit_glsl_instructions.h b/src/shader_recompiler/backend/glsl/emit_glsl_instructions.h
index 96aac502d5..06069d4c56 100644
--- a/src/shader_recompiler/backend/glsl/emit_glsl_instructions.h
+++ b/src/shader_recompiler/backend/glsl/emit_glsl_instructions.h
@@ -72,6 +72,10 @@ void EmitSetAttributeIndexed(EmitContext& ctx, std::string_view offset, std::str
std::string_view vertex);
void EmitGetPatch(EmitContext& ctx, IR::Inst& inst, IR::Patch patch);
void EmitSetPatch(EmitContext& ctx, IR::Patch patch, std::string_view value);
+void EmitIsbeRead(EmitContext& ctx);
+void EmitIsbeWriteAttribute(EmitContext& ctx);
+void EmitIsbeWriteMapU8(EmitContext& ctx);
+void EmitIsbeWriteMapU32(EmitContext& ctx);
void EmitSetFragColor(EmitContext& ctx, u32 index, u32 component, std::string_view value);
void EmitSetSampleMask(EmitContext& ctx, std::string_view value);
void EmitSetFragDepth(EmitContext& ctx, std::string_view value);
diff --git a/src/shader_recompiler/backend/glsl/emit_glsl_not_implemented.cpp b/src/shader_recompiler/backend/glsl/emit_glsl_not_implemented.cpp
index 9f1ed95a47..18779e51b8 100644
--- a/src/shader_recompiler/backend/glsl/emit_glsl_not_implemented.cpp
+++ b/src/shader_recompiler/backend/glsl/emit_glsl_not_implemented.cpp
@@ -97,4 +97,20 @@ void EmitGetInBoundsFromOp(EmitContext& ctx) {
NotImplemented();
}
+void EmitIsbeRead(EmitContext& ctx) {
+ NotImplemented();
+}
+
+void EmitIsbeWriteAttribute(EmitContext& ctx) {
+ NotImplemented();
+}
+
+void EmitIsbeWriteMapU8(EmitContext& ctx) {
+ NotImplemented();
+}
+
+void EmitIsbeWriteMapU32(EmitContext& ctx) {
+ NotImplemented();
+}
+
} // namespace Shader::Backend::GLSL
diff --git a/src/shader_recompiler/backend/glsl/glsl_emit_context.cpp b/src/shader_recompiler/backend/glsl/glsl_emit_context.cpp
index a2df6159fb..e4b11d6844 100644
--- a/src/shader_recompiler/backend/glsl/glsl_emit_context.cpp
+++ b/src/shader_recompiler/backend/glsl/glsl_emit_context.cpp
@@ -317,6 +317,9 @@ EmitContext::EmitContext(IR::Program& program, Bindings& bindings, const Profile
header += "layout(early_fragment_tests)in;";
}
break;
+ case Stage::Task:
+ case Stage::Mesh:
+ throw NotImplementedException("GLSL task and mesh programs");
case Stage::Compute:
stage_name = "cs";
const u32 local_x{(std::max)(program.workgroup_size[0], 1u)};
diff --git a/src/shader_recompiler/backend/spirv/emit_spirv.cpp b/src/shader_recompiler/backend/spirv/emit_spirv.cpp
index a903480c78..633ab25935 100644
--- a/src/shader_recompiler/backend/spirv/emit_spirv.cpp
+++ b/src/shader_recompiler/backend/spirv/emit_spirv.cpp
@@ -248,6 +248,20 @@ void DefineEntryPoint(const IR::Program& program, EmitContext& ctx, Id main) {
case Stage::VertexB:
execution_model = spv::ExecutionModel::Vertex;
break;
+ case Stage::Task:
+ execution_model = spv::ExecutionModel::TaskEXT;
+ ctx.AddExecutionMode(main, spv::ExecutionMode::LocalSize, program.workgroup_size[0], 1U,
+ 1U);
+ break;
+ case Stage::Mesh:
+ execution_model = spv::ExecutionModel::MeshEXT;
+ ctx.AddExecutionMode(main, spv::ExecutionMode::LocalSize, program.workgroup_size[0], 1U,
+ 1U);
+ ctx.AddExecutionMode(main, spv::ExecutionMode::OutputVertices, program.mesh.max_vertices);
+ ctx.AddExecutionMode(main, spv::ExecutionMode::OutputPrimitivesEXT,
+ program.mesh.max_primitives);
+ ctx.AddExecutionMode(main, spv::ExecutionMode::OutputTrianglesEXT);
+ break;
case Stage::TessellationControl:
execution_model = spv::ExecutionModel::TessellationControl;
ctx.AddCapability(spv::Capability::Tessellation);
@@ -548,6 +562,51 @@ void RewriteOpcodes(std::vector<u32>& code, std::span<const std::pair<u32, spv::
offset += word_count;
}
}
+
+// Replaces the OpStore markers of the mesh instructions Sirit cannot emit: OpStore(vertices,
+// primitives) becomes OpSetMeshOutputsEXT, and OpStore(x, y), OpStore(z, payload), OpReturn
+// becomes the OpEmitMeshTasksEXT terminator.
+void PatchMeshInstructions(std::vector<u32>& code, const EmitContext& ctx) {
+ const bool set_outputs{Sirit::ValidId(ctx.mesh_outputs_marker[0])};
+ const bool emit_tasks{Sirit::ValidId(ctx.mesh_tasks_marker[0])};
+ if (!set_outputs && !emit_tasks) {
+ return;
+ }
+ const auto store_at = [&](size_t offset, Id pointer, Id object) {
+ return offset + 2 < code.size() &&
+ code[offset] == ((3U << 16) | static_cast<u32>(spv::Op::OpStore)) &&
+ code[offset + 1] == pointer.value && code[offset + 2] == object.value;
+ };
+ const auto& tasks{ctx.mesh_tasks_marker};
+ size_t offset = 5;
+ bool patched{};
+ while (offset < code.size()) {
+ const u32 word_count{code[offset] >> 16};
+ if (word_count == 0) {
+ break;
+ }
+ if (set_outputs &&
+ store_at(offset, ctx.mesh_outputs_marker[0], ctx.mesh_outputs_marker[1])) {
+ code[offset] = (3U << 16) | static_cast<u32>(spv::Op::OpSetMeshOutputsEXT);
+ patched = true;
+ } else if (emit_tasks && store_at(offset, tasks[0], tasks[1]) &&
+ store_at(offset + 3, tasks[2], tasks[3]) && offset + 6 < code.size() &&
+ code[offset + 6] == ((1U << 16) | static_cast<u32>(spv::Op::OpReturn))) {
+ const std::array<u32, 5> emit{(5U << 16) | static_cast<u32>(spv::Op::OpEmitMeshTasksEXT),
+ tasks[0].value, tasks[1].value, tasks[2].value,
+ tasks[3].value};
+ code.erase(code.begin() + offset, code.begin() + offset + 7);
+ code.insert(code.begin() + offset, emit.begin(), emit.end());
+ patched = true;
+ offset += emit.size();
+ continue;
+ }
+ offset += word_count;
+ }
+ if (!patched) {
+ throw LogicError("Mesh instruction markers not found");
+ }
+}
} // Anonymous namespace
std::vector<u32> EmitSPIRV(const Profile& profile, const RuntimeInfo& runtime_info, IR::Program& program, Bindings& bindings) {
@@ -564,6 +623,7 @@ std::vector<u32> EmitSPIRV(const Profile& profile, const RuntimeInfo& runtime_in
PatchPhiNodes(program, ctx);
std::vector<u32> code{ctx.Assemble()};
RewriteOpcodes(code, ctx.opcode_rewrites);
+ PatchMeshInstructions(code, ctx);
return code;
}
diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h
index 165ec3fe84..ec526ff354 100644
--- a/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h
+++ b/src/shader_recompiler/backend/spirv/emit_spirv_instructions.h
@@ -61,6 +61,12 @@ Id EmitGetAttributeIndexed(EmitContext& ctx, Id offset, Id vertex);
void EmitSetAttributeIndexed(EmitContext& ctx, Id offset, Id value, Id vertex);
Id EmitGetPatch(EmitContext& ctx, IR::Patch patch);
void EmitSetPatch(EmitContext& ctx, IR::Patch patch, Id value);
+Id EmitIsbeRead(EmitContext& ctx, Id address);
+void EmitIsbeWriteAttribute(EmitContext& ctx, Id address, Id value);
+void EmitIsbeWriteMapU8(EmitContext& ctx, Id address, Id value);
+void EmitIsbeWriteMapU32(EmitContext& ctx, const IR::Value& address, Id value);
+void EmitMeshPrologue(EmitContext& ctx);
+void EmitMeshEpilogue(EmitContext& ctx);
void EmitSetFragColor(EmitContext& ctx, u32 index, u32 component, Id value);
void EmitSetSampleMask(EmitContext& ctx, Id value);
void EmitSetFragDepth(EmitContext& ctx, Id value);
diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_mesh.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_mesh.cpp
new file mode 100644
index 0000000000..6c599528cc
--- /dev/null
+++ b/src/shader_recompiler/backend/spirv/emit_spirv_mesh.cpp
@@ -0,0 +1,227 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// Switch 2 (NVN) task and mesh programs. They talk to the hardware through the stage buffer
+// (ISBE): a task program writes the number of mesh workgroups at byte 4 of its output region and
+// its payload after it; each mesh workgroup reads that region (its own index at byte 0) and
+// writes its vertices and primitives to an attribute region in groups of 32, one 32-word line per
+// attribute, plus the primitive count and 8-bit vertex indices to a map region. Here the task's
+// output region is the task payload, the mesh output regions live in workgroup memory, and the
+// epilogue (reached by every invocation, see structured_control_flow.cpp) hands them to Vulkan.
+
+#include <algorithm>
+#include <span>
+
+#include "shader_recompiler/backend/spirv/emit_spirv.h"
+#include "shader_recompiler/backend/spirv/emit_spirv_instructions.h"
+#include "shader_recompiler/backend/spirv/spirv_emit_context.h"
+
+namespace Shader::Backend::SPIRV {
+namespace {
+void Barrier(EmitContext& ctx) {
+ ctx.OpControlBarrier(ctx.Const(static_cast<u32>(spv::Scope::Workgroup)),
+ ctx.Const(static_cast<u32>(spv::Scope::Workgroup)),
+ ctx.Const(static_cast<u32>(spv::MemorySemanticsMask::AcquireRelease |
+ spv::MemorySemanticsMask::WorkgroupMemory)));
+}
+
+Id LocalInvocation(EmitContext& ctx) {
+ return ctx.OpCompositeExtract(ctx.U32[1], ctx.OpLoad(ctx.U32[3], ctx.local_invocation_id), 0U);
+}
+
+Id Clamped(EmitContext& ctx, Id index, u32 count) {
+ return ctx.OpUMin(ctx.U32[1], index, ctx.Const(count - 1));
+}
+
+Id PayloadWord(EmitContext& ctx, Id index) {
+ return ctx.OpAccessChain(ctx.mesh_payload_u32, ctx.mesh_payload,
+ Clamped(ctx, index, ctx.mesh_info.payload_words));
+}
+
+Id SharedWord(EmitContext& ctx, Id array, Id index) {
+ return ctx.OpAccessChain(ctx.mesh_shared_u32, array, index);
+}
+
+// Runs body under `if (condition)`.
+template <typename Func>
+void If(EmitContext& ctx, Id condition, Func&& body) {
+ const Id then_label{ctx.OpLabel()};
+ const Id merge_label{ctx.OpLabel()};
+ ctx.OpSelectionMerge(merge_label, spv::SelectionControlMask::MaskNone);
+ ctx.OpBranchConditional(condition, then_label, merge_label);
+ ctx.AddLabel(then_label);
+ body();
+ ctx.OpBranch(merge_label);
+ ctx.AddLabel(merge_label);
+}
+
+// Copies the lines of element `index` (a vertex or a primitive) from the attribute region.
+void CopyLines(EmitContext& ctx, Id index, u32 base, u32 num_lines,
+ const std::vector<EmitContext::MeshLineTarget>& targets) {
+ // Word of line 0: base + (index / 32) * num_lines * 32 + index % 32.
+ const Id group{ctx.OpShiftRightLogical(ctx.U32[1], index, ctx.Const(5U))};
+ const Id slot{ctx.OpBitwiseAnd(ctx.U32[1], index, ctx.Const(31U))};
+ const Id first{ctx.OpIAdd(ctx.U32[1],
+ ctx.OpIMul(ctx.U32[1], group, ctx.Const(num_lines * 32)),
+ ctx.OpIAdd(ctx.U32[1], slot, ctx.Const(base)))};
+ for (u32 line = 0; line < num_lines; ++line) {
+ const EmitContext::MeshLineTarget& target{targets[line]};
+ if (!Sirit::ValidId(target.variable)) {
+ continue;
+ }
+ const Id word{ctx.OpLoad(
+ ctx.U32[1],
+ SharedWord(ctx, ctx.mesh_attributes, ctx.OpIAdd(ctx.U32[1], first, ctx.Const(line * 32))))};
+ const Id value{target.is_float ? ctx.OpBitcast(ctx.F32[1], word) : word};
+ const Id pointer_type{target.is_float ? ctx.output_f32 : ctx.output_u32};
+ const Id pointer{target.element == ~0U
+ ? ctx.OpAccessChain(pointer_type, target.variable, index)
+ : ctx.OpAccessChain(pointer_type, target.variable, index,
+ ctx.Const(target.element))};
+ ctx.OpStore(pointer, value);
+ }
+}
+
+// OpenGL-style clip space: z = (z + w) / 2, from the stage buffer's position lines.
+void ConvertDepth(EmitContext& ctx, Id vertex) {
+ const auto& lines{ctx.mesh_info.vertex_lines};
+ const auto z_line{std::ranges::find(lines, static_cast<u32>(IR::Attribute::PositionZ))};
+ const auto w_line{std::ranges::find(lines, static_cast<u32>(IR::Attribute::PositionW))};
+ if (z_line == lines.end() || w_line == lines.end()) {
+ return;
+ }
+ const u32 num_lines{static_cast<u32>(lines.size())};
+ const Id group{ctx.OpShiftRightLogical(ctx.U32[1], vertex, ctx.Const(5U))};
+ const Id slot{ctx.OpBitwiseAnd(ctx.U32[1], vertex, ctx.Const(31U))};
+ const Id first{ctx.OpIAdd(ctx.U32[1], ctx.OpIMul(ctx.U32[1], group, ctx.Const(num_lines * 32)),
+ slot)};
+ const auto read = [&](auto line) {
+ const u32 offset{static_cast<u32>(line - lines.begin()) * 32};
+ return ctx.OpBitcast(ctx.F32[1],
+ ctx.OpLoad(ctx.U32[1], SharedWord(ctx, ctx.mesh_attributes,
+ ctx.OpIAdd(ctx.U32[1], first,
+ ctx.Const(offset)))));
+ };
+ const Id z{ctx.OpFMul(ctx.F32[1], ctx.OpFAdd(ctx.F32[1], read(z_line), read(w_line)),
+ ctx.Const(0.5f))};
+ ctx.OpStore(ctx.OpAccessChain(ctx.output_f32, ctx.output_position, vertex, ctx.Const(2U)), z);
+}
+
+void TaskEpilogue(EmitContext& ctx) {
+ Barrier(ctx);
+ const Id count{ctx.OpLoad(ctx.U32[1], PayloadWord(ctx, ctx.Const(1U)))};
+ const Id one{ctx.Const(1U)};
+ // Marker for OpEmitMeshTasksEXT count, 1, 1, payload (PatchMeshInstructions).
+ ctx.OpStore(count, one);
+ ctx.OpStore(one, ctx.mesh_payload);
+ ctx.mesh_tasks_marker = {count, one, one, ctx.mesh_payload};
+}
+
+void MeshEpilogue(EmitContext& ctx) {
+ const MeshInfo& mesh{ctx.mesh_info};
+ Barrier(ctx);
+ const Id count{ctx.OpUMin(ctx.U32[1], ctx.OpLoad(ctx.U32[1], ctx.mesh_primitive_count),
+ ctx.Const(mesh.max_primitives))};
+ const Id max_vertices{ctx.Const(mesh.max_vertices)};
+ // Marker for OpSetMeshOutputsEXT max_vertices, count (PatchMeshInstructions).
+ ctx.OpStore(max_vertices, count);
+ ctx.mesh_outputs_marker = {max_vertices, count};
+
+ const Id invocation{LocalInvocation(ctx)};
+ const u32 threads{ctx.mesh_threads};
+ const bool convert_depth{ctx.runtime_info.convert_depth_mode && !ctx.profile.support_native_ndc &&
+ Sirit::ValidId(ctx.output_position)};
+ const u32 vertex_lines{static_cast<u32>(mesh.vertex_lines.size())};
+ for (u32 first = 0; first < mesh.max_vertices; first += threads) {
+ const Id vertex{ctx.OpIAdd(ctx.U32[1], invocation, ctx.Const(first))};
+ const auto copy = [&] {
+ CopyLines(ctx, vertex, 0, vertex_lines, ctx.mesh_vertex_targets);
+ if (convert_depth) {
+ ConvertDepth(ctx, vertex);
+ }
+ };
+ if (first + threads > mesh.max_vertices) {
+ If(ctx, ctx.OpULessThan(ctx.U1, vertex, max_vertices), copy);
+ } else {
+ copy();
+ }
+ }
+ const u32 primitive_lines{static_cast<u32>(mesh.primitive_lines.size())};
+ for (u32 first = 0; first < mesh.max_primitives; first += threads) {
+ const Id primitive{ctx.OpIAdd(ctx.U32[1], invocation, ctx.Const(first))};
+ If(ctx, ctx.OpULessThan(ctx.U1, primitive, count), [&] {
+ // Vertex indices: map bytes 4 + 3 * primitive.
+ const Id byte{ctx.OpIAdd(ctx.U32[1], ctx.OpIMul(ctx.U32[1], primitive, ctx.Const(3U)),
+ ctx.Const(4U))};
+ std::array<Id, 3> indices;
+ for (u32 element = 0; element < 3; ++element) {
+ const Id address{ctx.OpIAdd(ctx.U32[1], byte, ctx.Const(element))};
+ indices[element] = ctx.OpLoad(ctx.U32[1], SharedWord(ctx, ctx.mesh_map, address));
+ }
+ const Id output_indices{ctx.TypePointer(spv::StorageClass::Output, ctx.U32[3])};
+ ctx.OpStore(ctx.OpAccessChain(output_indices, ctx.mesh_primitive_indices, primitive),
+ ctx.OpCompositeConstruct(ctx.U32[3], std::span<const Id>(indices)));
+ CopyLines(ctx, primitive, ctx.mesh_primitive_base, primitive_lines,
+ ctx.mesh_primitive_targets);
+ });
+ }
+}
+} // Anonymous namespace
+
+Id EmitIsbeRead(EmitContext& ctx, Id address) {
+ const Id workgroup{
+ ctx.OpCompositeExtract(ctx.U32[1], ctx.OpLoad(ctx.U32[3], ctx.workgroup_id), 0U)};
+ if (ctx.stage == Stage::Task) {
+ return workgroup; // the input region holds the workgroup index (VERTEX_ID)
+ }
+ const Id index{ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.Const(2U))};
+ const Id word{ctx.OpLoad(ctx.U32[1], PayloadWord(ctx, index))};
+ return ctx.OpSelect(ctx.U32[1], ctx.OpIEqual(ctx.U1, address, ctx.u32_zero_value), workgroup,
+ word);
+}
+
+void EmitIsbeWriteAttribute(EmitContext& ctx, Id address, Id value) {
+ const Id index{ctx.OpShiftRightLogical(ctx.U32[1], address, ctx.Const(2U))};
+ if (ctx.stage == Stage::Task) {
+ ctx.OpStore(PayloadWord(ctx, index), value);
+ return;
+ }
+ ctx.OpStore(SharedWord(ctx, ctx.mesh_attributes, Clamped(ctx, index, ctx.mesh_attribute_words)),
+ value);
+}
+
+void EmitIsbeWriteMapU8(EmitContext& ctx, Id address, Id value) {
+ if (ctx.stage != Stage::Mesh) {
+ throw NotImplementedException("Stage buffer map write outside mesh programs");
+ }
+ ctx.OpStore(SharedWord(ctx, ctx.mesh_map, Clamped(ctx, address, ctx.mesh_map_bytes)),
+ ctx.OpBitwiseAnd(ctx.U32[1], value, ctx.Const(0xffU)));
+}
+
+void EmitIsbeWriteMapU32(EmitContext& ctx, const IR::Value& address, Id value) {
+ // The primitive count: the map is written from its end, so its first word is at byte 3.
+ if (ctx.stage != Stage::Mesh || !address.IsImmediate() || address.U32() != 3) {
+ throw NotImplementedException("32-bit stage buffer map write");
+ }
+ ctx.OpStore(ctx.mesh_primitive_count, value);
+}
+
+void EmitMeshPrologue(EmitContext& ctx) {
+ if (ctx.stage != Stage::Mesh) {
+ return;
+ }
+ // Every invocation stores the zero: no branch here, the entry block's label is a phi
+ // predecessor for later blocks.
+ ctx.OpStore(ctx.mesh_primitive_count, ctx.u32_zero_value);
+ Barrier(ctx);
+}
+
+void EmitMeshEpilogue(EmitContext& ctx) {
+ if (ctx.stage == Stage::Task) {
+ TaskEpilogue(ctx);
+ } else if (ctx.stage == Stage::Mesh) {
+ MeshEpilogue(ctx);
+ }
+}
+
+} // namespace Shader::Backend::SPIRV
diff --git a/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp b/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp
index 8eca1fac29..2b36c1fd8d 100644
--- a/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp
+++ b/src/shader_recompiler/backend/spirv/emit_spirv_special.cpp
@@ -137,9 +137,16 @@ void EmitPrologue(EmitContext& ctx) {
if (ctx.stage == Stage::VertexB || ctx.stage == Stage::Geometry) {
SetFixedPipelinePointSize(ctx);
}
+ if (ctx.stage == Stage::Task || ctx.stage == Stage::Mesh) {
+ EmitMeshPrologue(ctx);
+ }
}
void EmitEpilogue(EmitContext& ctx) {
+ if (ctx.stage == Stage::Task || ctx.stage == Stage::Mesh) {
+ EmitMeshEpilogue(ctx);
+ return;
+ }
if (ctx.stage == Stage::VertexB && ctx.runtime_info.convert_depth_mode &&
!ctx.profile.support_native_ndc) {
ConvertDepthMode(ctx);
diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp
index b562e82983..f80f0440df 100644
--- a/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp
+++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.cpp
@@ -257,6 +257,10 @@ std::string_view StageName(Stage stage) {
return "fs";
case Stage::Compute:
return "cs";
+ case Stage::Task:
+ return "ts";
+ case Stage::Mesh:
+ return "ms";
}
throw InvalidArgument("Invalid stage {}", stage);
}
@@ -475,6 +479,7 @@ EmitContext::EmitContext(const Profile& profile_, const RuntimeInfo& runtime_inf
DefineCommonTypes(program.info);
DefineCommonConstants();
DefineInterfaces(program);
+ DefineMesh(program);
DefineLocalMemory(program);
DefineSharedMemory(program);
DefineSharedMemoryFunctions(program);
@@ -1586,6 +1591,16 @@ void EmitContext::DefineInputs(const IR::Program& program) {
}
const bool is_integer = input_type == AttributeType::SignedInt ||
input_type == AttributeType::UnsignedInt;
+ if (runtime_info.per_primitive_generics.test(index)) {
+ // Written per primitive by a mesh program: constant over the primitive.
+ AddExtension("SPV_EXT_mesh_shader");
+ AddCapability(spv::Capability::MeshShadingEXT);
+ Decorate(id, spv::Decoration::PerPrimitiveEXT);
+ if (is_integer) {
+ Decorate(id, spv::Decoration::Flat);
+ }
+ continue;
+ }
if (is_integer) {
Decorate(id, spv::Decoration::Flat);
} else {
@@ -1614,8 +1629,120 @@ void EmitContext::DefineInputs(const IR::Program& program) {
}
}
+void EmitContext::DefineMesh(const IR::Program& program) {
+ if (stage != Stage::Task && stage != Stage::Mesh) {
+ return;
+ }
+ AddExtension("SPV_EXT_mesh_shader");
+ AddCapability(spv::Capability::MeshShadingEXT);
+ mesh_info = program.mesh;
+ mesh_threads = program.workgroup_size[0];
+ // The task's output region, which its mesh workgroups read as their input region.
+ const Id payload_type{TypeArray(U32[1], Const(mesh_info.payload_words))};
+ mesh_payload = DefineVariable(*this, payload_type, std::nullopt,
+ spv::StorageClass::TaskPayloadWorkgroupEXT);
+ Name(mesh_payload, "task_payload");
+ mesh_payload_u32 = TypePointer(spv::StorageClass::TaskPayloadWorkgroupEXT, U32[1]);
+ if (stage == Stage::Task) {
+ return;
+ }
+ // Output attribute region: groups of 32 vertices, then groups of 32 primitives, one 32-word
+ // line per attribute. The map region keeps one map byte per word.
+ const u32 vertex_lines{static_cast<u32>(mesh_info.vertex_lines.size())};
+ const u32 primitive_lines{static_cast<u32>(mesh_info.primitive_lines.size())};
+ mesh_primitive_base = Common::DivCeil(mesh_info.max_vertices, 32U) * vertex_lines * 32;
+ mesh_attribute_words = (std::max)(
+ mesh_primitive_base + Common::DivCeil(mesh_info.max_primitives, 32U) * primitive_lines * 32,
+ 1U);
+ mesh_map_bytes = 4 + 3 * mesh_info.max_primitives;
+ mesh_shared_u32 = TypePointer(spv::StorageClass::Workgroup, U32[1]);
+ mesh_attributes = DefineVariable(*this, TypeArray(U32[1], Const(mesh_attribute_words)),
+ std::nullopt, spv::StorageClass::Workgroup);
+ Name(mesh_attributes, "isbe_attributes");
+ mesh_map = DefineVariable(*this, TypeArray(U32[1], Const(mesh_map_bytes)), std::nullopt,
+ spv::StorageClass::Workgroup);
+ Name(mesh_map, "isbe_map");
+ mesh_primitive_count =
+ DefineVariable(*this, U32[1], std::nullopt, spv::StorageClass::Workgroup);
+ Name(mesh_primitive_count, "isbe_primitive_count");
+
+ const Id vertex_count{Const(mesh_info.max_vertices)};
+ const Id primitive_count{Const(mesh_info.max_primitives)};
+ mesh_primitive_indices =
+ DefineVariable(*this, TypeArray(U32[3], primitive_count),
+ spv::BuiltIn::PrimitiveTriangleIndicesEXT, spv::StorageClass::Output);
+ std::array<Id, IR::NUM_GENERICS> generics[2]{};
+ Id point_size{}, primitive_layer{}, primitive_viewport{}, primitive_id_output{};
+ const auto per_primitive_builtin = [&](Id& variable, spv::BuiltIn builtin) {
+ if (!Sirit::ValidId(variable)) {
+ variable = DefineVariable(*this, TypeArray(U32[1], primitive_count), builtin,
+ spv::StorageClass::Output);
+ Decorate(variable, spv::Decoration::PerPrimitiveEXT);
+ }
+ return MeshLineTarget{variable, ~0U, false};
+ };
+ const auto target = [&](u32 line, bool per_primitive) -> MeshLineTarget {
+ const IR::Attribute attribute{line};
+ if (IR::IsGeneric(attribute)) {
+ const u32 index{IR::GenericAttributeIndex(attribute)};
+ Id& variable{generics[per_primitive][index]};
+ if (!Sirit::ValidId(variable)) {
+ variable = DefineVariable(
+ *this, TypeArray(F32[4], per_primitive ? primitive_count : vertex_count),
+ std::nullopt, spv::StorageClass::Output);
+ Decorate(variable, spv::Decoration::Location, index);
+ if (per_primitive) {
+ Decorate(variable, spv::Decoration::PerPrimitiveEXT);
+ }
+ }
+ return {variable, IR::GenericAttributeElement(attribute), true};
+ }
+ if (per_primitive) {
+ switch (attribute) {
+ case IR::Attribute::PrimitiveId:
+ return per_primitive_builtin(primitive_id_output, spv::BuiltIn::PrimitiveId);
+ case IR::Attribute::Layer:
+ return per_primitive_builtin(primitive_layer, spv::BuiltIn::Layer);
+ case IR::Attribute::ViewportIndex:
+ return per_primitive_builtin(primitive_viewport, spv::BuiltIn::ViewportIndex);
+ default:
+ return {};
+ }
+ }
+ switch (attribute) {
+ case IR::Attribute::PositionX:
+ case IR::Attribute::PositionY:
+ case IR::Attribute::PositionZ:
+ case IR::Attribute::PositionW:
+ if (!Sirit::ValidId(output_position)) {
+ output_position = DefineVariable(*this, TypeArray(F32[4], vertex_count),
+ spv::BuiltIn::Position, spv::StorageClass::Output);
+ }
+ return {output_position,
+ static_cast<u32>(attribute) - static_cast<u32>(IR::Attribute::PositionX), true};
+ case IR::Attribute::PointSize:
+ if (!Sirit::ValidId(point_size)) {
+ point_size = DefineVariable(*this, TypeArray(F32[1], vertex_count),
+ spv::BuiltIn::PointSize, spv::StorageClass::Output);
+ }
+ return {point_size, ~0U, true};
+ default:
+ return {}; // per-primitive values in vertex lines have no vertex output
+ }
+ };
+ for (const u32 line : mesh_info.vertex_lines) {
+ mesh_vertex_targets.push_back(target(line, false));
+ }
+ for (const u32 line : mesh_info.primitive_lines) {
+ mesh_primitive_targets.push_back(target(line, true));
+ }
+}
+
void EmitContext::DefineOutputs(const IR::Program& program) {
const Info& info{program.info};
+ if (stage == Stage::Task || stage == Stage::Mesh) {
+ return; // DefineMesh
+ }
const std::optional<u32> invocations{program.invocations};
if (runtime_info.convert_depth_mode || info.stores.AnyComponent(IR::Attribute::PositionX) ||
stage == Stage::VertexB) {
diff --git a/src/shader_recompiler/backend/spirv/spirv_emit_context.h b/src/shader_recompiler/backend/spirv/spirv_emit_context.h
index a5821e9e93..1f9b534aaf 100644
--- a/src/shader_recompiler/backend/spirv/spirv_emit_context.h
+++ b/src/shader_recompiler/backend/spirv/spirv_emit_context.h
@@ -363,6 +363,32 @@ public:
std::vector<Id> interfaces;
std::vector<std::pair<u32, spv::Op>> opcode_rewrites;
+ // Task and mesh programs. Their stage buffer: the task's output region is the task payload
+ // (the mesh stage's input region); the mesh output regions live in workgroup memory until
+ // the epilogue copies them to the outputs. The two mesh instructions Sirit lacks are emitted
+ // as OpStore markers with these operands and replaced after assembly (PatchMeshInstructions).
+ struct MeshLineTarget {
+ Id variable; ///< arrayed output
+ u32 element; ///< vector element, or ~0 for a scalar output
+ bool is_float; ///< bitcast the stage buffer word to F32
+ };
+ Id mesh_payload{};
+ Id mesh_payload_u32{}; ///< pointer type to one payload word
+ Id mesh_attributes{};
+ Id mesh_map{};
+ Id mesh_primitive_count{};
+ Id mesh_shared_u32{}; ///< pointer type to one workgroup word
+ Id mesh_primitive_indices{};
+ MeshInfo mesh_info{};
+ u32 mesh_threads{};
+ u32 mesh_attribute_words{};
+ u32 mesh_map_bytes{};
+ u32 mesh_primitive_base{}; ///< first word of the per-primitive groups
+ std::vector<MeshLineTarget> mesh_vertex_targets;
+ std::vector<MeshLineTarget> mesh_primitive_targets;
+ std::array<Id, 2> mesh_outputs_marker{};
+ std::array<Id, 4> mesh_tasks_marker{};
+
Id load_const_func_u8{};
Id load_const_func_u16{};
Id load_const_func_u32{};
@@ -402,6 +428,7 @@ private:
void DefineInputs(const IR::Program& program);
void DefineOutputs(const IR::Program& program);
+ void DefineMesh(const IR::Program& program);
};
} // namespace Shader::Backend::SPIRV
diff --git a/src/shader_recompiler/environment.h b/src/shader_recompiler/environment.h
index 67486430c8..20364944cf 100644
--- a/src/shader_recompiler/environment.h
+++ b/src/shader_recompiler/environment.h
@@ -66,6 +66,7 @@ struct ConstBufferAddr {
IR::U32 dynamic_offset;
u32 count;
bool has_secondary;
+ u32 handle_program{~0U}; ///< index in Info::handle_programs for a handle computed per draw
};
class Environment {
@@ -121,9 +122,22 @@ public:
return is_proprietary_driver;
}
+ /// Task and mesh programs: SET_MESH_SHADER_A, SET_MESH_SHADER_B and SET_MESH_INIT_SHADER.
+ [[nodiscard]] const std::array<u32, 3>& MeshRegisters() const noexcept {
+ return mesh_registers;
+ }
+
+ /// Mesh programs: the header of the pass-through geometry program bound with them, whose
+ /// output map lists the per-primitive outputs.
+ [[nodiscard]] const ProgramHeader& MeshPrimitiveHeader() const noexcept {
+ return mesh_primitive_header;
+ }
+
protected:
ProgramHeader sph{};
std::array<u32, 8> gp_passthrough_mask{};
+ std::array<u32, 3> mesh_registers{};
+ ProgramHeader mesh_primitive_header{};
Stage stage{};
u32 start_address{};
bool is_proprietary_driver{};
diff --git a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h
index 031f7fac75..81bd4b9e74 100644
--- a/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h
+++ b/src/shader_recompiler/frontend/ampere/lower_to_maxwell.h
@@ -34,6 +34,9 @@ struct LoweredProgram {
std::vector<u32> source_instruction_offsets;
u32 source_size{};
u32 required_texture_buffer = ~0U;
+ // UR28 and up seen without a mirror: lowering again with this many registers below R216
+ // reserved for them (even, so pairs stay adjacent) maps them.
+ u32 high_uniform_registers{};
u32 error_offset{};
std::string_view error;
@@ -43,8 +46,10 @@ struct LoweredProgram {
// Lower only operations whose semantics are representable by the existing
// Maxwell frontend. These are new instructions, not reinterpretations of the
// native bytes. Scheduling/control words are added by the environment adapter.
-inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
+inline LoweredProgram LowerToMaxwellWith(std::span<const Instruction> code, u32 high_uniforms) {
LoweredProgram result;
+ // UR28 and up (when used) live just below R216: UR28 at R(216 - high_uniforms).
+ const u32 high_uniform_base = 216 - high_uniforms;
struct Relocation { u32 output_index; s64 target; };
std::vector<Relocation> relocations;
std::vector<u32> barrier_tokens;
@@ -55,8 +60,8 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
bool uses_uniform_bank = false;
for (const auto& instruction : code) {
const u32 op = instruction.Bits(0, 12);
- uses_uniform_bank |= op == 0x3c4 || op == 0x2bd || op == 0xc0c || op == 0x986 || op == 0x9a8 || op == 0xc12 || op == 0xc17 || op == 0xe24 || op == 0xc24 || op == 0xd00 || op == 0xd09 || op == 0x9c3 || op == 0xc45 || op == 0x2a4 || op == 0x891 || op == 0x890 || op == 0x290 || op == 0x28c || op == 0x88c || op == 0x287 || op == 0x887 || op == 0x958 || op == 0xab9 || op == 0xabb || op == 0x899 || op == 0xc20 || op == 0xe21 || op == 0xe23 || op == 0xc02 ||
- op == 0x886 || op == 0x3c2 || op == 0xc82 || op == 0x882 || op == 0x292 || op == 0x892 || op == 0x291 || op == 0x981 || op == 0xc13 || op == 0xc10 || op == 0xc23 || op == 0x526 || op == 0x2be || op == 0x8a4 || op == 0xd05 || (op == 0x984 && instruction.Bits(91, 1)) || op == 0x89c || op == 0x883 || op == 0x2a5 || op == 0xd06 || op == 0x299 || op == 0xc09 || (op == 0x81c && instruction.Bits(67, 1));
+ uses_uniform_bank |= op == 0x3c4 || op == 0x2bd || op == 0x896 || op == 0xf60 || op == 0xc11 || op == 0xd08 || op == 0xc0c || op == 0x986 || op == 0x9a8 || op == 0xc12 || op == 0xc17 || op == 0xe24 || op == 0xc24 || op == 0xd00 || op == 0xd09 || op == 0x9c3 || op == 0xc45 || op == 0x2a4 || op == 0x891 || op == 0x890 || op == 0x290 || op == 0x28c || op == 0x88c || op == 0x287 || op == 0x887 || op == 0x958 || op == 0xab9 || op == 0xabb || op == 0x899 || op == 0xc20 || op == 0xe21 || op == 0xe23 || op == 0xc02 ||
+ op == 0x886 || op == 0x3c2 || op == 0xc82 || op == 0x882 || op == 0x292 || op == 0x892 || op == 0x291 || op == 0x981 || op == 0xc13 || op == 0xc10 || op == 0xc23 || op == 0x526 || op == 0x2be || op == 0x8a4 || op == 0xd05 || (op == 0x984 && instruction.Bits(91, 1)) || op == 0x89c || op == 0x883 || op == 0x2a5 || op == 0xd06 || op == 0xf63 || op == 0xf69 || op == 0xc0b || op == 0x8a5 || op == 0x299 || op == 0xc09 || (op == 0x81c && instruction.Bits(67, 1)) || op == 0xc0a;
}
const auto address_of = [](u32 index) { return 0x50U + index * 8 + ((index + 1) / 3) * 8; };
const auto fail = [&](u32 index, std::string_view reason) {
@@ -83,6 +88,7 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
result.source_instruction_offsets.push_back(static_cast<u32>(result.instructions.size()));
u32 opcode = insn.Bits(0, 12);
if (insn.Bits(12, 4) == 15) continue; // @!PT: never executes (compiler padding)
+ u32 f16_dest = 255, f16_second = 255, f16_components = 0;
std::optional<u32> active_link;
u32 active_entry = 0;
if (main_exited) {
@@ -92,8 +98,13 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
active_link = call_links[call];
}
}
- // RET has no destination GPR; bits 16..23 are reserved, not a write to R0.
- if (active_link && opcode != 0x950 && insn.Bits(16, 8) == *active_link)
+ // RET, BRA, NOP and ISETP/FSETP (predicate outputs) have no destination GPR; bits 16..23
+ // are not a write to R0. IMAD.WIDE writes a register pair.
+ const bool no_destination = opcode == 0x950 || opcode == 0x947 || opcode == 0x918 ||
+ (opcode & 0x1ff) == 0x00c || (opcode & 0x1ff) == 0x00b;
+ const bool pair = opcode == 0x225 || opcode == 0x625 || opcode == 0x825;
+ if (active_link && !no_destination &&
+ (insn.Bits(16, 8) == *active_link || (pair && insn.Bits(16, 8) + 1 == *active_link)))
return fail(index, "native callee overwrites link register");
}
// Scratch values and emulated uniform state occupy reserved high GPRs.
@@ -101,22 +112,37 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
for (u32 bit : {16U, 24U, 32U, 64U}) {
if (bit == 32 && insn.Bits(9, 3) != 1 && opcode != 0xb60 && opcode != 0xb66) continue;
const u32 reg = insn.Bits(bit, 8);
- if (reg >= (uses_uniform_bank ? 216U : 246U) && reg < 255) return fail(index, "native program uses lowering scratch registers");
+ if (reg >= (uses_uniform_bank ? high_uniform_base : 246U) && reg < 255) return fail(index, "native program uses lowering scratch registers");
+ }
+ // TLD/TLD4 .F16 (bit 79): the components come packed in pairs, (0, 1) in Rd and (2, 3) in
+ // the second destination. Lowered as the F32 form into R248/R249 and R246/R247, then packed.
+ if ((opcode == 0xb66 || opcode == 0xb63) && insn.Bits(79, 2) == 1) {
+ f16_dest = insn.Bits(16, 8);
+ f16_second = insn.Bits(64, 8);
+ f16_components = static_cast<u32>(std::popcount(insn.Bits(72, 4)));
+ if (f16_dest >= 246 || (f16_second != 255 && f16_second >= 246) || insn.Bits(61, 3) == 5 ||
+ (f16_second == 255 && f16_components > 2))
+ return fail(index, "native half-precision texture result registers");
+ insn.words[2] &= ~(1U << 15);
+ insn.words[0] = (insn.words[0] & ~0x00ff0000U) | (f16_dest == 255 ? 0x00ff0000U : (248U << 16));
+ if (f16_second != 255) insn.words[2] = (insn.words[2] & ~0xffU) | 246U;
}
// UR0-UR21 live in R216-R237 and UR22-UR27 in R240-R245 (R239 holds the uniform
- // predicates), so even/odd pairs stay adjacent. Sites that still check reg < 16 rely on
- // UR0-UR15 being R216-R231.
+ // predicates), so even/odd pairs stay adjacent; UR28 and up below R216. Sites that
+ // still check reg < 16 rely on UR0-UR15 being R216-R231.
const auto uniform_register = [&](u32 reg) -> u32 {
if (reg < 22) return 216 + reg;
if (reg < 28) return 218 + reg;
if (reg == 63) return 255;
+ if (reg < 63 && reg - 28 < high_uniforms) return high_uniform_base + (reg - 28);
+ if (reg < 63) result.high_uniform_registers = (std::max)(result.high_uniform_registers, (reg - 26) & ~1U);
return ~0U;
};
bool rewritten_uniform = false;
bool uniform_guard = false;
u32 uniform_output = 7; // ULOP3's UPn output: computed in P(6-n), then merged into R239
if (opcode == 0x2a4 || opcode == 0x8a4 || opcode == 0x891 || opcode == 0x890 || opcode == 0x290 ||
- opcode == 0x292 || opcode == 0x892 || opcode == 0x291) {
+ opcode == 0x292 || opcode == 0x892 || opcode == 0x291 || opcode == 0x896 || opcode == 0x8a5) {
if (insn.Bits(12, 4) != 7 && opcode != 0x292 && opcode != 0x892)
return fail(index, "native uniform arithmetic predication");
uniform_guard = insn.Bits(12, 4) != 7;
@@ -138,7 +164,8 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
insn.words[2] = (insn.words[2] & ~(7U << 17)) | ((6 - uniform_output) << 17);
}
opcode = opcode == 0x2a4 ? 0x224 : opcode == 0x8a4 ? 0x824 : opcode == 0x891 ? 0x811 : opcode == 0x890 ? 0x810
- : opcode == 0x292 ? 0x212 : opcode == 0x892 ? 0x812 : opcode == 0x291 ? 0x211 : 0x210;
+ : opcode == 0x292 ? 0x212 : opcode == 0x892 ? 0x812 : opcode == 0x291 ? 0x211
+ : opcode == 0x896 ? 0x816 : opcode == 0x8a5 ? 0x825 : 0x210;
}
const bool uniform_predication = opcode == 0xabb || opcode == 0xab9 || opcode == 0xc82 || opcode == 0x882;
const u32 native_predicate = insn.Bits(12, 4);
@@ -163,6 +190,13 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
};
std::string_view operand_error;
const auto emit = [&](u64 word) { result.instructions.push_back(word); };
+ // Bound Maxwell texture instructions share one descriptor constant buffer per program.
+ const auto bind_texture_buffer = [&](u32 binding) {
+ if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)
+ return false;
+ result.required_texture_buffer = binding;
+ return true;
+ };
// P(guard slot) while a uniform guard is swapped in; P6 for lowerings that use it as
// scratch (R2UR, MATCH, HFMA2.RELU), which save and restore it with the defaults below.
const u32 guard_mask = guard_slot < 7 ? 1U << guard_slot : 0U;
@@ -466,6 +500,20 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
emit(base(0x5c98000000000000ULL) | (u64{15} << 39) | (u64{insn.Bits(64, 8)} << 20));
break;
}
+ if (insn.Bits(24, 8) == 255 && insn.words[1] == 255 && (insn.words[2] & ~0x07800affU) == 0x000e0400 &&
+ (!rewritten_uniform || insn.Bits(87, 3) == 7)) {
+ // IMAD.X RZ, RZ, c, Pn: the high word of a 64-bit add (bit 75 inverts c).
+ u32 c = insn.Bits(64, 8);
+ mov_imm(250, 1);
+ emit(0x5ca0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{255} << 20) |
+ (u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42));
+ if (insn.Bits(75, 1)) {
+ emit(0x0400000000000000ULL | predicate | 251 | (u64{c} << 8) | (u64{0xffffffffU} << 20) | (u64{2} << 53));
+ c = 251;
+ }
+ emit(base(0x5c10000000000000ULL) | (u64{c} << 8) | (u64{250} << 20));
+ break;
+ }
if ((insn.words[2] & ~0x2ffU) != 0x078e0000 || (insn.words[1] & ~0xffU))
return fail(index, "native integer register multiply mode");
multiply_low(insn.Bits(24, 8), insn.Bits(32, 8), insn.Bits(64, 8), false);
@@ -496,16 +544,87 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
emit(0x5c10000000070000ULL | d_low | (u64{250} << 8) | (u64{255} << 20));
break;
}
- case 0x827: { // IMAD.HI.U32, captured zero addend
- if (insn.words[2] != 0x078e00ff) return fail(index, "native high multiply mode");
- const u32 a = insn.Bits(24, 8), b = insn.words[1];
+ case 0x226: { // IDP.2A.LO.U16.U8: a.lo16 * b.byte0 + a.hi16 * b.byte1 + c
+ if ((insn.words[2] & ~0xffU) != 0x1000 || (insn.words[1] & ~0xffU))
+ return fail(index, "native packed dot-product mode");
+ const u32 a = insn.Bits(24, 8), b = insn.Bits(32, 8), c = insn.Bits(64, 8);
+ for (u32 component = 0; component < 2; ++component) {
+ emit(0x3828000000000000ULL | predicate | (250 + component) | (u64{b} << 8) | (u64{component * 8} << 20));
+ emit(0x0400000000000000ULL | predicate | (250 + component) | (u64{250 + component} << 8) | (u64{0xff} << 20));
+ }
+ xmad(252, a, 250, c, false, false, 0, false);
+ xmad(static_cast<u32>(dest), a, 251, 252, true, false, 0, false);
+ break;
+ }
+ case 0x225: case 0x227: case 0x625: case 0x825: case 0x827: case 0xa27: {
+ // IMAD.HI Rd / IMAD.WIDE Rd:Rd+1 = a * b + c:c+1 (.U32 or signed, bit 73), with an optional
+ // carry-out (bits 81-83). b is a register, an immediate (0x8xx) or a cbuf (0xa27); 0x625 takes
+ // b from bits 64-71 and adds a cbuf pair.
+ const bool wide = opcode == 0x225 || opcode == 0x625 || opcode == 0x825;
+ const bool immediate = opcode == 0x825 || opcode == 0x827, signed_product = insn.Bits(73, 1);
+ const u32 c = opcode == 0x625 ? 255U : insn.Bits(64, 8), carry_output = insn.Bits(81, 3);
+ if ((insn.words[2] & ~0x000e02ffU) != 0x07800000 ||
+ ((opcode == 0x225 || opcode == 0x227) && (insn.words[1] & ~0xffU)) ||
+ ((opcode == 0xa27 || opcode == 0x625) && (!valid_cbuf() || insn.Bits(62, 2))) ||
+ (opcode == 0x625 && insn.Bits(38, 16) > 0xfff8) || (c != 255 && (c & 1)) ||
+ (wide && (dest == 255 || (dest & 1))) || (signed_product && carry_output != 7))
+ return fail(index, "native high multiply mode");
+ const u32 a = insn.Bits(24, 8);
+ const u32 b = immediate ? insn.words[1] : opcode == 0xa27 ? 254U : insn.Bits(opcode == 0x625 ? 64 : 32, 8);
+ if (opcode == 0xa27) emit(0x4c98000000000000ULL | predicate | 254 | (u64{15} << 39) | cbuf());
+ if (signed_product) {
+ // The signed high word is the unsigned one minus (a < 0 ? b : 0) + (b < 0 ? a : 0),
+ // which R253 holds (R252 is free until the multiplication).
+ const u32 b_register = immediate ? 254U : b;
+ if (immediate) mov_imm(254, b);
+ emit(0x3828000000000000ULL | predicate | 253 | (u64{a} << 8) | (u64{31} << 20) | (u64{1} << 48));
+ emit(0x5c40000000000000ULL | predicate | 253 | (u64{253} << 8) | (u64{b_register} << 20) | (u64{7} << 48));
+ emit(0x3828000000000000ULL | predicate | 252 | (u64{b_register} << 8) | (u64{31} << 20) | (u64{1} << 48));
+ emit(0x5c40000000000000ULL | predicate | 252 | (u64{252} << 8) | (u64{a} << 20) | (u64{7} << 48));
+ emit(0x5c10000000000000ULL | predicate | 253 | (u64{253} << 8) | (u64{252} << 20));
+ }
// Base-2^16 long multiplication. Each intermediate sum fits U32.
- xmad(250, a, b, 255, false, false, 0, true);
- xmad(251, a, b, 250, true, false, 2, true);
- xmad(250, a, b, 251, false, true, 1, true);
- xmad(252, a, b, 251, true, true, 2, true);
+ xmad(250, a, b, 255, false, false, 0, immediate);
+ xmad(251, a, b, 250, true, false, 2, immediate);
+ xmad(250, a, b, 251, false, true, 1, immediate);
+ xmad(252, a, b, 251, true, true, 2, immediate);
emit(0x3828000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{16} << 20));
- emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{252} << 20));
+ if (signed_product)
+ emit(0x5c10000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{253} << 20) | (u64{1} << 48));
+ if (!wide && c == 255 && carry_output == 7) {
+ emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{252} << 20));
+ break;
+ }
+ // High word in R253 and low word in R250 (as UIMAD.WIDE.U32), then c:c+1 is added.
+ emit(0x5c10000000000000ULL | predicate | 253 | (u64{250} << 8) | (u64{252} << 20));
+ xmad(250, a, b, 255, false, false, 0, immediate);
+ xmad(251, a, b, 255, true, false, 0, immediate);
+ xmad(251, a, b, 251, false, true, 0, immediate);
+ emit(0x5c18000000000000ULL | predicate | 250 | (u64{251} << 8) | (u64{250} << 20) | (u64{16} << 39));
+ u32 c_low = c, c_high = c == 255 ? 255U : c + 1;
+ if (opcode == 0x625) {
+ emit(0x4c98000000000000ULL | predicate | 252 | (u64{15} << 39) | cbuf());
+ emit(0x4c98000000000000ULL | predicate | 254 | (u64{15} << 39) | (cbuf() + (u64{1} << 20)));
+ c_low = 252;
+ c_high = 254;
+ }
+ const u32 high = wide ? static_cast<u32>(dest) + 1 : static_cast<u32>(dest);
+ emit(0x5c10000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{c_low} << 20) | (u64{1} << 47));
+ if (carry_output == 7) {
+ emit(0x5c10000000000000ULL | predicate | high | (u64{253} << 8) | (u64{c_high} << 20) | (u64{1} << 43));
+ if (wide) emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{255} << 20));
+ break;
+ }
+ // Maxwell IADD has no .X with .CC: R251 takes the low carry, R254 counts the high word's.
+ emit(0x5c10000000000000ULL | predicate | 251 | (u64{255} << 8) | (u64{255} << 20) | (u64{1} << 43));
+ emit(0x5c10000000000000ULL | predicate | 253 | (u64{253} << 8) | (u64{c_high} << 20) | (u64{1} << 47));
+ emit(0x5c10000000000000ULL | predicate | 254 | (u64{255} << 8) | (u64{255} << 20) | (u64{1} << 43));
+ emit(0x5c10000000000000ULL | predicate | 253 | (u64{253} << 8) | (u64{251} << 20) | (u64{1} << 47));
+ emit(0x5c10000000000000ULL | predicate | 254 | (u64{254} << 8) | (u64{255} << 20) | (u64{1} << 43));
+ emit(0x5c98000000000000ULL | predicate | high | (u64{253} << 20) | (u64{15} << 39));
+ if (wide) emit(base(0x5c98000000000000ULL) | (u64{250} << 20) | (u64{15} << 39));
+ emit(0x5b60000000000000ULL | predicate | 7 | (u64{carry_output} << 3) | (u64{254} << 8) |
+ (u64{255} << 20) | (u64{7} << 39) | (u64{5} << 49));
break;
}
case 0xa24: { // IMAD, constant-buffer multiplier
@@ -547,6 +666,21 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
}
case 0x824: { // Captured low-word IMAD forms; bit 75 negates the addend.
const bool negate_c = insn.Bits(75, 1) != 0;
+ if (insn.words[1] == 1 && (insn.words[2] & ~(0x07800800U | 0xffU)) == 0x000e0600U) {
+ // IMAD.X Rd, Ra, 1, Rc, Pn: Ra + Rc plus the carry predicate in bits 87-90 (~Rc with bit 75).
+ u32 c = insn.Bits(64, 8);
+ mov_imm(252, 1);
+ emit(0x5ca0000000000000ULL | predicate | 252 | (u64{252} << 8) | (u64{255} << 20) |
+ (u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42));
+ if (negate_c) {
+ emit(0x0400000000000000ULL | predicate | 251 | (u64{c} << 8) | (u64{0xffffffffU} << 20) | (u64{2} << 53));
+ c = 251;
+ }
+ emit(0x5c10000000000000ULL | predicate | 250 | (u64{insn.Bits(24, 8)} << 8) |
+ (u64{c} << 20));
+ emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{252} << 20));
+ break;
+ }
if (insn.words[1] == 1 && (insn.words[2] & 0xfffff700U) == 0x078e0200) {
emit(base(0x5c10000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) |
(u64{insn.Bits(64, 8)} << 20) | (u64{negate_c} << 48));
@@ -614,8 +748,15 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
emit(0x5c30000000070000ULL | 250 | (u64{250} << 20));
emit(0xef10000000070000ULL | 251 | (u64{insn.Bits(24, 8)} << 8) |
(u64{250} << 20) | (u64{1} << 29) | (u64{31} << 34) | (u64{7} << 48));
- emit(0x5ca0000000070000ULL | target | (u64{251} << 8) |
- (u64{target} << 20) | (u64{6} << 39));
+ if (native_predicate == 7) {
+ // Every lane executing it is in the ballot, so no lane-less fallback can be taken.
+ // Leaving the old value selectable would make it a source of the result for the
+ // texture and global memory tracking (a handle traced to the uniform's last load).
+ emit(0x5c98000000070000ULL | target | (u64{15} << 39) | (u64{251} << 20));
+ } else {
+ emit(0x5ca0000000070000ULL | target | (u64{251} << 8) |
+ (u64{target} << 20) | (u64{6} << 39));
+ }
restore_predicate();
break;
}
@@ -708,36 +849,42 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
}
case 0x899: { // USHF.L.U32; accept a zero word outside the selected half.
const u32 dst = insn.Bits(16, 8), low = insn.Bits(24, 8), high = insn.Bits(64, 8);
- if (insn.words[2] == (0x08011400U | high) && low == 63 && high < 16 &&
- dst < 16 && native_predicate == 7 && insn.words[1] <= 32) {
- emit(0x3828000000000000ULL | predicate | (dst + 216) |
- (u64{high + 216} << 8) | (u64{insn.words[1]} << 20) | (u64{1} << 48));
+ const u32 dst_reg = uniform_register(dst), low_reg = uniform_register(low);
+ const u32 high_reg = uniform_register(high);
+ const bool dst_ok = dst != 63 && dst_reg != ~0U;
+ const bool low_ok = low != 63 && low_reg != ~0U, high_ok = high != 63 && high_reg != ~0U;
+ if (insn.words[2] == (0x08011400U | high) && low == 63 && high_ok &&
+ dst_ok && native_predicate == 7 && insn.words[1] <= 32) {
+ emit(0x3828000000000000ULL | predicate | dst_reg |
+ (u64{high_reg} << 8) | (u64{insn.words[1]} << 20) | (u64{1} << 48));
break;
}
// Logical right shifts of one register (bit 76 right, bit 80 high word, bits 73-74
// U32): USHF.R.U32 URd, URa, n, URZ and USHF.R.U32.HI URd, URZ, n, URb.
- const bool right_low = insn.words[2] == 0x0800163f && low < 16;
- const bool right_high = insn.words[2] == (0x08011600U | high) && low == 63 && high < 16;
- if ((right_low || right_high) && dst < 16 && native_predicate == 7 && insn.words[1] <= 32) {
- emit(0x3828000000000000ULL | predicate | (dst + 216) |
- (u64{(right_high ? high : low) + 216} << 8) | (u64{insn.words[1]} << 20));
+ const bool right_low = insn.words[2] == 0x0800163f && low_ok;
+ const bool right_high = insn.words[2] == (0x08011600U | high) && low == 63 && high_ok;
+ if ((right_low || right_high) && dst_ok && native_predicate == 7 && insn.words[1] <= 32) {
+ emit(0x3828000000000000ULL | predicate | dst_reg |
+ (u64{right_high ? high_reg : low_reg} << 8) | (u64{insn.words[1]} << 20));
break;
}
- const bool high_result = insn.words[2] == (0x08010600U | high) && low == 63 && high < 16;
- const bool low_result = insn.words[2] == 0x0800063f && low < 16;
- if (dst >= 16 || native_predicate != 7 || insn.words[1] > 32 || (!high_result && !low_result))
+ const bool high_result = insn.words[2] == (0x08010600U | high) && low == 63 && high_ok;
+ const bool low_result = insn.words[2] == 0x0800063f && low_ok;
+ if (!dst_ok || native_predicate != 7 || insn.words[1] > 32 || (!high_result && !low_result))
return fail(index, "native uniform shift mode");
- emit(0x3848000000000000ULL | predicate | (dst + 216) |
- (u64{(high_result ? high : low) + 216} << 8) | (u64{insn.words[1]} << 20));
+ emit(0x3848000000000000ULL | predicate | dst_reg |
+ (u64{high_result ? high_reg : low_reg} << 8) | (u64{insn.words[1]} << 20));
break;
}
- case 0xb82: { // LDC.64 indexed byte offset
- const u32 width = insn.Bits(73, 3);
- if ((width != 4 && width != 5) || insn.Bits(78, 2) || insn.Bits(91, 1) ||
+ case 0xb82: { // LDC.64 indexed byte offset; bits 78-79 the mode as Maxwell's (2 = IS)
+ // .IS: Ra + offset addresses all constant buffers as one space, the buffer in its high
+ // half (Kirby Air Riders' vertex shaders), which Eden's Maxwell LDC implements.
+ const u32 width = insn.Bits(73, 3), mode = insn.Bits(78, 2);
+ if ((width != 4 && width != 5) || (mode != 0 && mode != 2) || insn.Bits(91, 1) ||
bank() >= 18 ||
(width == 5 && ((dest & 1) || dest >= 254))) return fail(index, "native indexed constant load mode");
emit(base(0xef90000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) |
- (u64{insn.Bits(38, 16)} << 20) | (u64{bank()} << 36) | (u64{width} << 48));
+ (u64{insn.Bits(38, 16)} << 20) | (u64{bank()} << 36) | (u64{mode} << 44) | (u64{width} << 48));
break;
}
case 0xf89: { // SHFL with immediate lane and segmentation/clamp mask, any mode.
@@ -759,33 +906,39 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
(u64{insn.Bits(64, 8)} << 20) | (u64{maxwell} << 28));
break;
}
- case 0xb6c: { // TXD 2D or ARRAY_2D, four channels; the two destination pairs anywhere.
+ case 0xb6c: { // TXD 2D or ARRAY_2D, any channels; the two destination pairs anywhere.
const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14);
const u32 coordinates = insn.Bits(24, 8), derivatives = insn.Bits(32, 8);
// Ampere dimensions: 1 = 2D, 5 = ARRAY_2D; Maxwell TXD's are 2 and 3. The coordinates
// (x, y[, layer]) and the four gradients sit in the same registers for both.
const u32 dimension = insn.Bits(61, 3);
const u32 high = insn.Bits(64, 8); // destination of the third and fourth channels
- if ((insn.words[2] & ~0xffU) != 0x001e0f00 || insn.Bits(59, 2) ||
+ // Bit 90 is forwarded below (Maxwell's NODEP); Splatoon Raiders sets it. Bits 72-75 are
+ // the channel mask: the first two enabled channels go to Rd, Rd+1, the others to the
+ // second destination (RZ drops them; a third channel alone may be in an odd register).
+ const u32 mask = insn.Bits(72, 4), channels = static_cast<u32>(std::popcount(mask));
+ if ((insn.words[2] & ~(0xfffU | (1U << 26))) != 0x001e0000 || mask == 0 || insn.Bits(59, 2) ||
(dimension != 1 && dimension != 5) || binding >= 18 || offset > 0x1fff ||
- (dest & 1) || dest >= 248 || (high & 1) || (high >= 248 && high != 255) ||
+ (channels > 1 && (dest & 1)) || dest >= 248 || (channels <= 2 && high != 255) ||
+ (high != 255 && ((channels > 3 && (high & 1)) || high >= 248)) ||
coordinates >= 248 || derivatives >= 247)
return fail(index, "native gradient texture mode");
if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)
return fail(index, "native texture descriptor buffer conflict");
result.required_texture_buffer = binding;
- // Maxwell writes four consecutive registers: when the second pair is elsewhere (Splatoon
- // Raiders: R12/R18, R18/R44), write them to scratch and move each pair into place.
- const bool consecutive = high == dest + 2;
+ // Maxwell writes the channels to consecutive registers: when the second destination
+ // doesn't follow Rd (Splatoon Raiders: R12/R18, R18/R44), write them to scratch and move
+ // each into place.
+ const bool consecutive = channels <= 2 || high == dest + 2;
const u64 target = consecutive ? dest : 248;
emit(0xde00000000000000ULL | predicate | target | (u64{coordinates} << 8) |
- (u64{derivatives} << 20) | (u64{dimension == 5 ? 3U : 2U} << 28) | (u64{15} << 31) |
+ (u64{derivatives} << 20) | (u64{dimension == 5 ? 3U : 2U} << 28) | (u64{mask} << 31) |
(u64{offset} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51));
if (!consecutive) {
- for (u32 i = 0; i < 2; ++i) {
- emit(0x5c98000000000000ULL | predicate | (dest + i) | (u64{15} << 39) | (u64{248 + i} << 20));
- if (high != 255)
- emit(0x5c98000000000000ULL | predicate | (high + i) | (u64{15} << 39) | (u64{250 + i} << 20));
+ for (u32 i = 0; i < channels; ++i) {
+ if (i >= 2 && high == 255) break;
+ const u32 to = i < 2 ? static_cast<u32>(dest) + i : high + (i - 2);
+ emit(0x5c98000000000000ULL | predicate | to | (u64{15} << 39) | (u64{248 + i} << 20));
}
}
break;
@@ -836,44 +989,74 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
emit(0x3828000000000000ULL | predicate | (dest + 216) | (u64{insn.words[1] + 216} << 8) |
(u64{1} << 39) | (u64{1} << 40));
break;
- case 0x3a1: { // MATCH.ALL.U64 with only the all-equal predicate retained.
+ case 0x2bf: // UPOPC, as POPC of the uniform's register.
+ if (dest >= 16 || insn.words[1] >= 16 || insn.words[2] != 0x08000000 || native_predicate != 7)
+ return fail(index, "native uniform population count mode");
+ emit(0x5c08000000000000ULL | predicate | (dest + 216) | (u64{insn.words[1] + 216} << 20));
+ break;
+ case 0x3a1: { // MATCH.ALL (.U32, or .U64 on bit 73) with only the all-equal predicate retained.
const u32 source = insn.Bits(24, 8), target = insn.Bits(81, 3);
- if (dest != 255 || source >= 254 || (source & 1) || target >= 6 ||
- native_predicate != 7 || insn.words[1] || (insn.words[2] & ~0xe0000U) != 0x200)
+ const u32 words = insn.Bits(73, 1) ? 2 : 1;
+ const u32 scratch = target == 6 ? 5U : 6U; // the comparison, saved around the match
+ if (dest != 255 || source >= 254 || (words == 2 && (source & 1)) || target == 7 ||
+ native_predicate != 7 || insn.words[1] || (insn.words[2] & ~0xe0200U) != 0)
return fail(index, "native warp match mode");
- save_predicate();
+ save_predicate(1U << scratch);
+ // Compare every lane with the lowest active one, then require all to agree.
emit(0x50d8000000070000ULL | 250 | (u64{7} << 39) | (u64{7} << 45) | (u64{1} << 48));
emit(0x5c10000000070000ULL | 251 | (u64{250} << 8) | (u64{255} << 20) | (u64{1} << 49));
emit(0x5c40000000070000ULL | 250 | (u64{250} << 8) | (u64{251} << 20) | (u64{7} << 48));
emit(0x5c30000000070000ULL | 250 | (u64{250} << 20));
- for (u32 word = 0; word < 2; ++word)
+ for (u32 word = 0; word < words; ++word)
emit(0xef10000000070000ULL | (251 + word) | (u64{source + word} << 8) |
(u64{250} << 20) | (u64{1} << 29) | (u64{31} << 34) | (u64{7} << 48));
- emit(0x5b60000000070000ULL | 7 | (u64{6} << 3) | (u64{source} << 8) |
+ emit(0x5b60000000070000ULL | 7 | (u64{scratch} << 3) | (u64{source} << 8) |
(u64{251} << 20) | (u64{7} << 39) | (u64{2} << 49));
- emit(0x5b60000000070000ULL | 7 | (u64{6} << 3) | (u64{source + 1} << 8) |
- (u64{252} << 20) | (u64{6} << 39) | (u64{2} << 49));
- emit(0x50d8000000070000ULL | 255 | (u64{6} << 39) | (u64{target} << 45));
- restore_predicate();
- break;
- }
- case 0x3c4: { // REDUX.SUM (modulo 2^32) or .MAX (unsigned) over the actual active lanes.
+ if (words == 2)
+ emit(0x5b60000000070000ULL | 7 | (u64{scratch} << 3) | (u64{source + 1} << 8) |
+ (u64{252} << 20) | (u64{scratch} << 39) | (u64{2} << 49));
+ emit(0x50d8000000070000ULL | 255 | (u64{scratch} << 39) | (u64{target} << 45));
+ restore_predicate(1U << scratch);
+ break;
+ }
+ case 0x80d: { // CSMTEST: a test of shading-mode state Eden does not model, taken as false.
+ // Kirby Air Riders' vertex-side shaders run "CSMTEST.A.FCSM_TR P0" then "@P0 EXIT":
+ // never leave early. Outputs of PT discard the result.
+ const u32 first = insn.Bits(81, 3), second = insn.Bits(84, 3);
+ if (insn.Bits(87, 4) != 7 || insn.Bits(16, 16))
+ return fail(index, "native CSM test mode");
+ for (const u32 target : {first, second})
+ if (target != 7)
+ emit(0x5090000000000000ULL | predicate | (u64{target} << 3) | 7 | (u64{7} << 12) |
+ (u64{1} << 15) | (u64{7} << 29) | (u64{7} << 39));
+ break;
+ }
+ case 0x3c4: { // REDUX.SUM (modulo 2^32), .MIN/.MAX (unsigned), .OR or .XOR over the actual active lanes.
const u32 source = insn.Bits(24, 8), target = uniform_register(static_cast<u32>(dest));
- const bool maximum = insn.words[2] == 0x14000;
+ const bool maximum = insn.words[2] == 0x14000, minimum = insn.words[2] == 0x10000;
+ // OR and XOR: inactive lanes contribute zero, as for the sum. Maxwell LOP: 1 = OR, 2 = XOR.
+ const u32 logical = insn.words[2] == 0x4000 ? 1U : insn.words[2] == 0x8000 ? 2U : 0U;
if (target == ~0U || target == 255 || insn.words[1] ||
- (insn.words[2] != 0xc000 && !maximum) || native_predicate != 7)
+ (insn.words[2] != 0xc000 && !maximum && !minimum && !logical) || native_predicate != 7)
return fail(index, "native warp sum mode");
emit(0x50d8000000070000ULL | 250 | (u64{7} << 39) | (u64{7} << 45) | (u64{1} << 48));
- mov_imm(target, 0);
+ mov_imm(target, minimum ? ~0U : 0U);
for (u32 lane = 0; lane < 32; ++lane) {
emit(0xef10000000070000ULL | 251 | (u64{source} << 8) | (u64{lane} << 20) |
(u64{1} << 28) | (u64{1} << 29) | (u64{31} << 34) | (u64{7} << 48));
emit(0x3828000000070000ULL | 252 | (u64{250} << 8) | (u64{lane} << 20));
emit(0x0400000000070000ULL | 252 | (u64{252} << 8) | (u64{1} << 20));
emit(0x5c10000000070000ULL | 252 | (u64{255} << 8) | (u64{252} << 20) | (u64{1} << 48));
- emit(0x5c40000000070000ULL | 251 | (u64{251} << 8) | (u64{252} << 20) | (u64{7} << 48));
- if (maximum) {
+ // Inactive lanes: 0, or ~0 for the minimum (OR with the inverted lane mask).
+ emit(0x5c40000000070000ULL | 251 | (u64{251} << 8) | (u64{252} << 20) | (u64{7} << 48) |
+ (u64{minimum} << 40) | (u64{minimum} << 41));
+ if (minimum) {
+ emit(0x5c20000000070000ULL | target | (u64{target} << 8) | (u64{251} << 20) | (u64{7} << 39));
+ } else if (maximum) {
emit(0x5c20000000070000ULL | target | (u64{target} << 8) | (u64{251} << 20) | (u64{15} << 39));
+ } else if (logical) {
+ emit(0x5c40000000070000ULL | target | (u64{target} << 8) | (u64{251} << 20) |
+ (u64{logical} << 41) | (u64{7} << 48));
} else {
emit(0x5c10000000070000ULL | target | (u64{target} << 8) | (u64{251} << 20));
}
@@ -881,8 +1064,9 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
break;
}
case 0xf8c: { // ATOMS.POPC.INC.32 with no returned value: one increment per active lane.
- const u32 stride = insn.Bits(78, 2);
- if (dest != 255 || insn.words[1] || (insn.words[2] & ~0xc000U) != 0x0d00003f)
+ const u32 stride = insn.Bits(78, 2), offset = insn.Bits(40, 24);
+ if (dest != 255 || (insn.words[1] & 0xff) || (offset & 3) ||
+ (insn.words[2] & ~0xc000U) != 0x0d00003f)
return fail(index, "native shared increment mode");
u32 address = insn.Bits(24, 8);
if (stride && address != 255) {
@@ -891,15 +1075,18 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
address = 251;
}
mov_imm(250, 1);
- emit(0xec00000000000000ULL | predicate | 255 | (u64{address} << 8) | (u64{250} << 20));
+ emit(0xec00000000000000ULL | predicate | 255 | (u64{address} << 8) | (u64{250} << 20) |
+ (u64{offset >> 2} << 30));
break;
}
- case 0x38c: { // ATOMS.ADD.U32, naturally aligned shared byte offset.
- const u32 offset = insn.Bits(40, 24);
- if (insn.words[2] || (offset & 3)) return fail(index, "native shared atomic mode");
+ case 0x38c: { // ATOMS (ADD/MIN/MAX/INC/DEC/AND/OR/XOR/EXCH, U32 or S32), aligned byte offset.
+ // The operation (bits 87-90) is numbered as in Maxwell ATOMS (52-55); bit 73 is S32.
+ const u32 offset = insn.Bits(40, 24), operation = insn.Bits(87, 4);
+ if ((insn.words[2] & ~0x07800200U) || operation > 8 || (offset & 3))
+ return fail(index, "native shared atomic mode");
emit(0xec00000000000000ULL | predicate | dest |
(u64{insn.Bits(24, 8)} << 8) | (u64{insn.Bits(32, 8)} << 20) |
- (u64{offset >> 2} << 30));
+ (u64{insn.Bits(73, 1)} << 28) | (u64{offset >> 2} << 30) | (u64{operation} << 52));
break;
}
case 0x3a8: { // ATOMG.E.ADD.STRONG.GPU U32 at a 64-bit register address plus an offset.
@@ -924,14 +1111,14 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
(u64{1} << 48));
break;
}
- case 0xb97: { // SULD.P.2D/2D_ARRAY.IGN, direct descriptor, channel mask at 72-75.
+ case 0xb97: { // SULD.P.2D/2D_ARRAY/3D.IGN, direct descriptor, channel mask at 72-75.
const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14);
const u32 coordinate = insn.Bits(24, 8);
const u32 channels = insn.Bits(72, 4);
- const u32 dimension = insn.Bits(61, 3); // Maxwell's codes: 3 = 2D, 4 = 2D_ARRAY
- if ((insn.words[2] & ~0xf00U) != 0x001e0000 || channels == 0 || (dimension != 3 && dimension != 4) ||
+ const u32 dimension = insn.Bits(61, 3); // Maxwell's codes: 3 = 2D, 4 = 2D_ARRAY, 5 = 3D
+ if ((insn.words[2] & ~0xf00U) != 0x001e0000 || channels == 0 || dimension < 3 || dimension > 5 ||
binding >= 18 || offset > 0x1fff || insn.Bits(59, 2) || dest == 255 ||
- coordinate > (dimension == 4 ? 252U : 253U))
+ coordinate > (dimension >= 4 ? 252U : 253U))
return fail(index, "native formatted surface load mode");
if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)
return fail(index, "native surface descriptor buffer conflict");
@@ -944,14 +1131,35 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14);
const u32 data = insn.Bits(32, 8), coordinate = insn.Bits(24, 8);
const u32 dimension = insn.Bits(61, 3);
- if (insn.words[2] != 0x00100f00 || dimension < 3 || dimension > 5 ||
+ const bool strong = insn.words[2] == 0x0010af00; // .STRONG.GPU: fenced on both sides
+ if ((insn.words[2] != 0x00100f00 && !strong) || dimension < 3 || dimension > 5 ||
binding >= 18 || offset > 0x1fff || insn.Bits(59, 2) || data > 252 || coordinate > (dimension >= 4 ? 252U : 253U))
return fail(index, "native formatted surface store mode");
+ // A second descriptor buffer: SUST.B with the handle, loaded from that buffer, in R250.
+ const bool bindless = !bind_texture_buffer(binding);
+ if (bindless) {
+ emit(0x4c98000000070000ULL | 250 | (u64{15} << 39) | (u64{offset} << 20) | (u64{binding} << 34));
+ }
+ if (strong) emit(0xef98000000000000ULL | predicate | (u64{1} << 8)); // MEMBAR.GL
+ emit(0xeb20000000000000ULL | predicate | data | (u64{coordinate} << 8) |
+ (u64{15} << 20) | (u64{dimension} << 33) |
+ (bindless ? u64{250} << 39 : (u64{offset} << 36) | (u64{1} << 51)));
+ if (strong) emit(0xef98000000000000ULL | predicate | (u64{1} << 8));
+ break;
+ }
+ case 0x59f: { // SURED.D.2D.ADD.STRONG.GPU.IGN U32, direct descriptor (title 1's race compute).
+ const u32 binding = insn.Bits(54, 5), offset = insn.Bits(40, 14);
+ const u32 data = insn.Bits(32, 8), coordinate = insn.Bits(24, 8);
+ if (insn.words[2] != 0x0010e000 || insn.Bits(61, 3) != 3 || binding >= 18 ||
+ offset > 0x1fff || insn.Bits(59, 2) || coordinate > 253)
+ return fail(index, "native surface reduction mode");
if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)
return fail(index, "native surface descriptor buffer conflict");
result.required_texture_buffer = binding;
- emit(0xeb20000000000000ULL | predicate | data | (u64{coordinate} << 8) |
- (u64{15} << 20) | (u64{dimension} << 33) | (u64{offset} << 36) | (u64{1} << 51));
+ // Maxwell SURED (as the frontend decodes it): operand 0-7, coordinates 8-15, ADD and
+ // U32 are zero at 24 and 20, type 33 (3 = 2D), bound offset 36, bound 51.
+ emit(0xeb40000000000000ULL | predicate | data | (u64{coordinate} << 8) |
+ (u64{3} << 33) | (u64{offset} << 36) | (u64{1} << 51));
break;
}
case 0x301: // BREV register.
@@ -959,21 +1167,27 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
emit(base(0x3828000000000000ULL) | (u64{insn.words[1]} << 8) |
(u64{1} << 39) | (u64{1} << 40));
break;
- case 0x219: case 0xc19: { // U32 register (or uniform, 0xc19) shifts with a zero word outside the selected result.
+ case 0x219: case 0xa19: case 0xc19: { // U32 register (or cbuf, 0xa19, or uniform, 0xc19) shifts with a zero word outside the selected result.
const u32 low = insn.Bits(24, 8), high = insn.Bits(64, 8);
- const u32 shift = opcode == 0xc19 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8);
+ const bool constant = opcode == 0xa19;
+ // A constant shift amount is loaded into R251 once the form is known.
+ const u32 shift = opcode == 0xc19 ? uniform_register(insn.Bits(32, 8)) : constant ? 251U : insn.Bits(32, 8);
+ const bool shift_operand = constant ? valid_cbuf() && !insn.Bits(62, 2) : insn.words[1] <= 255;
+ const auto load_shift = [&] { if (constant) materialize(2, 0, 251, false, false); };
const bool right = insn.Bits(76, 1) != 0, high_result = insn.Bits(80, 1) != 0;
// SHF.R.U32 of the low word with a zero high word: a plain logical right shift.
if (right && !high_result && high == 255 && insn.Bits(73, 2) == 3 && shift != ~0U &&
- insn.words[1] <= 255 &&
+ shift_operand &&
(insn.words[2] & ~(0x000118ffU | (opcode == 0xc19 ? 0x08000000U : 0U))) == 0x00000600U) {
+ load_shift();
emit(base(0x5c28000000000000ULL) | (u64{low} << 8) | (u64{shift} << 20));
break;
}
- if (insn.words[1] > 255 || insn.Bits(73, 2) != 3 || shift == ~0U ||
+ if (!shift_operand || insn.Bits(73, 2) != 3 || shift == ~0U ||
(insn.words[2] & ~(0x000118ffU | (opcode == 0xc19 ? 0x08000000U : 0U))) != 0x00000600U ||
(right ? (!high_result || low != 255) : (high_result || high != 255)))
return fail(index, "native register shift mode");
+ load_shift();
emit(base(right ? 0x5cf8000000000000ULL : 0x5bf8000000000000ULL) |
(u64{right ? high : 255U} << 8) | (u64{shift} << 20) |
(u64{right ? 255U : low} << 39) | (u64{insn.Bits(75, 1)} << 50));
@@ -1012,8 +1226,8 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
if (insn.words[1] || insn.words[2]) return fail(index, "native error barrier mode");
emit(0xef98000000000000ULL | predicate | (u64{1} << 8));
break;
- case 0xb1d: // BAR.SYNC 0, full workgroup.
- if (insn.words[1] || insn.words[2] || native_predicate != 7)
+ case 0xb1d: // BAR.SYNC 0 (bit 80: .DEFER_BLOCKING), full workgroup.
+ if (insn.words[1] || (insn.words[2] & ~0x10000U) || native_predicate != 7)
return fail(index, "native workgroup barrier mode");
emit(0xf0a8000000000000ULL | predicate | (u64{1} << 43) |
(u64{1} << 44) | (u64{1} << 39) | (u64{7} << 39));
@@ -1188,10 +1402,21 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
}
break;
}
- case 0x919: case 0x9c3: { // S2R / S2UR thread and workgroup identities.
+ case 0x919: case 0x9c3: { // S2R / S2UR: special registers the Maxwell S2R handles.
+ // Same numbering as Maxwell: lane id (0), invocation id/info, Y direction, thread kill
+ // (17-19, 29), W scale factors (30-31, Kirby Air Riders), thread and workgroup ids,
+ // lane masks (56-60).
const u32 special = insn.Bits(72, 8);
+ if (special == 132 && !insn.words[1] && insn.words[2] == (special << 8)) {
+ // SR_VARIABLE_RATE: without variable-rate shading every fragment is 1x1, its width
+ // and height in bytes 1 and 2 (Nintendo Switch Sports Resort multiplies them).
+ const u32 target = opcode == 0x9c3 ? uniform_register(static_cast<u32>(dest)) : static_cast<u32>(dest);
+ if (target == ~0U) return fail(index, "native uniform special destination");
+ mov_imm(target, 0x00010100);
+ break;
+ }
if (insn.words[1] || insn.words[2] != (special << 8) ||
- (special != 0 && special != 33 && special != 34 && special != 35 &&
+ (special != 0 && (special < 17 || special > 19) && (special < 29 || special > 35) &&
special != 37 && special != 38 && special != 39 &&
special != 56 && special != 57 && special != 58 && special != 59 && special != 60))
return fail(index, "native special register");
@@ -1223,10 +1448,25 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
if (condition > 6 || target == ~0U || a == ~0U || b == ~0U ||
insn.Bits(12, 4) != 7 || (insn.words[2] & ~0x07800000U) != 0x08000000U)
return fail(index, "native uniform select mode");
- if (opcode == 0x887 && index > 0 && condition == 0 && insn.words[1] <= 0x7ffff &&
- code[index - 1].Bits(0, 12) == 0x88c &&
- code[index - 1].Bits(24, 8) == insn.Bits(24, 8) &&
- code[index - 1].words[1] == insn.words[1] && code[index - 1].words[2] == 0x0bf01070 &&
+ // The clamp's UISETP may sit a few instructions back, with only instructions between
+ // that write no uniform state and do not branch (Nintendo Switch Sports' jump tables),
+ // and no branch seen so far landing between them.
+ u32 compare = index;
+ for (u32 back = 1; back <= 8 && back <= index; ++back) {
+ const u32 earlier = code[index - back].Bits(0, 12), datapath = earlier & 0x1ff;
+ if (earlier == 0x88c) {
+ compare = index - back;
+ break;
+ }
+ if ((datapath >= 0x080 && datapath <= 0x0bf) || (datapath >= 0x1c2 && datapath <= 0x1c4) ||
+ (earlier >= 0x940 && earlier <= 0x95f))
+ break;
+ }
+ const bool target_between = std::any_of(relocations.begin(), relocations.end(),
+ [&](const Relocation& relocation) { return relocation.target > compare && relocation.target <= index; });
+ if (opcode == 0x887 && compare < index && !target_between && condition == 0 && insn.words[1] <= 0x7ffff &&
+ code[compare].Bits(12, 4) == 7 && code[compare].Bits(24, 8) == insn.Bits(24, 8) &&
+ code[compare].words[1] == insn.words[1] && code[compare].words[2] == 0x0bf01070 &&
insn.words[2] == 0x08000000) {
// Preserve the unsigned clamp idiom for indirect-table analysis.
emit(0x3820000000070000ULL | target | (u64{a} << 8) |
@@ -1257,10 +1497,17 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
// The uniform base sits where a register source would (bits 32-37).
const u32 uniform_address =
insn.Bits(38, 2) ? ~0U : uniform_register(insn.Bits(32, 6));
- if ((insn.words[2] & ~(0x04000000U | 0x3fU | 0xe00U | 0x2000U)) != 0x081e8100 ||
+ // desc[URn][Ra.64]: the uniform pair is a memory (cache policy) descriptor and
+ // Ra the whole address.
+ const bool descriptor_address = (insn.words[2] & ~(0x3fU | 0xe00U)) == 0x0c1e1100;
+ if ((!descriptor_address &&
+ (insn.words[2] & ~(0x04000000U | 0x3fU | 0xe00U | 0x2000U)) != 0x081e8100) ||
uniform_address == ~0U || (uniform_address & 1))
return fail(index, "native global load uniform addressing mode");
- if (address == 255) {
+ if (descriptor_address) {
+ if ((address & 1) || address >= 246)
+ return fail(index, "native global load register address pair");
+ } else if (address == 255) {
address = uniform_address;
} else {
if (insn.Bits(90, 1) && ((address & 1) || address >= 246))
@@ -1293,10 +1540,16 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
bool extended = insn.Bits(72, 1) != 0;
if (opcode == 0x986) {
const u32 uniform_address = uniform_register(insn.Bits(64, 6));
- if ((insn.words[2] & ~(0x04000000U | 0x3fU | 0xe00U)) != 0x0810a100 ||
+ // desc[URn][Ra.64]: the uniform pair is a memory (cache policy) descriptor and
+ // Ra the whole address.
+ const bool descriptor_address = (insn.words[2] & ~(0x3fU | 0xe00U)) == 0x0c101100;
+ if ((!descriptor_address && (insn.words[2] & ~(0x04000000U | 0x3fU | 0xe00U)) != 0x0810a100) ||
uniform_address == ~0U || (uniform_address & 1) || !extended)
return fail(index, "native global store uniform addressing mode");
- if (address == 255) address = uniform_address;
+ if (descriptor_address) {
+ if ((address & 1) || address >= 246)
+ return fail(index, "native global store register address pair");
+ } else if (address == 255) address = uniform_address;
else {
if (insn.Bits(90, 1) && ((address & 1) || address >= 254))
return fail(index, "native global store register address pair");
@@ -1319,14 +1572,31 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
return fail(index, "native fragment discard mode");
emit(0xe33000000000000fULL | predicate);
break;
+ case 0x311: { // F2I.FTZ.U64.TRUNC, register source, destination pair
+ if (insn.words[2] != 0x0021d800 || (insn.words[1] & ~0xffU) || insn.Bits(24, 8) ||
+ (dest & 1) || dest >= 246)
+ return fail(index, "native unsigned 64-bit float conversion mode");
+ emit(base(0x5cb0000000000000ULL) | (u64{3} << 8) | (u64{2} << 10) |
+ (u64{insn.Bits(32, 8)} << 20) | (u64{3} << 39) | (u64{1} << 44));
+ break;
+ }
+ case 0x312: { // I2F.F32.(U|S)64, register pair source; sign (74) and rounding (78-79) as Maxwell's
+ if ((insn.words[2] & ~0xc400U) != 0x00301000 || (insn.words[1] & ~0xffU) || insn.Bits(24, 8) ||
+ (insn.Bits(32, 8) & 1) || insn.Bits(32, 8) >= 254)
+ return fail(index, "native 64-bit integer-to-float mode");
+ emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{3} << 10) | (u64{insn.Bits(74, 1)} << 13) |
+ (u64{insn.Bits(32, 8)} << 20) | (u64{insn.Bits(78, 2)} << 39));
+ break;
+ }
case 0x305: case 0xb05: case 0xd05: { // F2I NTZ, register/constant-buffer/uniform source
const u32 mode = insn.words[2] & ~(opcode == 0xd05 ? 0x08000000U : 0U);
- const bool half = opcode == 0x305 && mode == 0x0010f000;
+ // F16 source: bit 72 signs the result as in the other forms, bits 60-61 pick H0/H1.
+ const bool half = opcode == 0x305 && (mode & ~0x100U) == 0x0010f000;
const bool small = mode == 0x00212800;
const bool floor = (mode & ~0x10100U) == 0x00207000;
const bool nearest = (mode & ~0x10100U) == 0x00203000;
- if ((!half && !small && !floor && !nearest && (mode & ~0x100U) != 0x0021f000) ||
- (opcode != 0xb05 && (insn.words[1] & 0xffffff00U)) ||
+ if ((!half && !small && !floor && !nearest && (mode & ~0x10100U) != 0x0020f000) ||
+ (opcode != 0xb05 && (insn.words[1] & (half ? 0xefffff00U : 0xffffff00U))) ||
(opcode == 0xd05 && uniform_register(insn.Bits(32, 8)) == ~0U)) {
return fail(index, "native float-to-integer conversion mode");
}
@@ -1334,7 +1604,7 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
u32 source = opcode == 0xb05 ? materialize(2, 0, 252, false, false)
: opcode == 0xd05 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8);
if (half) {
- extract_half({source, 2, false, false}, 0, 252);
+ extract_half({source, insn.Bits(60, 1) ? 3U : 2U, false, false}, 0, 252);
source = 252;
}
if (!is_signed) {
@@ -1363,19 +1633,42 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
}
break;
}
- case 0xb6f: { // TXQ texture dimensions, two packed scalar outputs.
- if (insn.Bits(59, 5) || insn.Bits(64, 8) != 255 || insn.Bits(72, 4) == 0 || insn.Bits(72, 4) > 3 ||
- insn.Bits(76, 14) || insn.Bits(91, 1) || insn.Bits(54, 5) != 2 ||
- insn.Bits(40, 14) > 0x1fff || (insn.Bits(72, 4) == 3 && (dest & 1)) || dest >= 254)
+ case 0xb6f: case 0x370: { // TXQ texture dimensions (0x370: TXQ.B).
+ // TXQ.B takes the handle in Ra and the level in Ra+1, as Maxwell's. Up to two outputs
+ // go to Rd, Rd+1 directly; more, or a second destination (bits 64-71) for the third and
+ // fourth, come out packed in scratch and are copied as for TEX.
+ const bool bindless = opcode == 0x370;
+ const u32 binding = insn.Bits(54, 5); // descriptor buffer, as for TLD
+ const u32 mask = insn.Bits(72, 4), components = static_cast<u32>(std::popcount(mask));
+ const u32 second_dest = insn.Bits(64, 8);
+ const bool direct = second_dest == 255;
+ if (insn.Bits(59, 5) != (bindless ? 1U : 0U) || mask == 0 || (direct && components > 2) ||
+ insn.Bits(76, 14) || insn.Bits(91, 1) || (!bindless && binding >= 18) ||
+ (bindless && (insn.Bits(32, 27) || insn.Bits(24, 8) >= 245)) ||
+ (!direct && (second_dest >= 245 || dest >= 245)) ||
+ insn.Bits(40, 14) > 0x1fff || (direct && components == 2 && (dest & 1)) || dest >= 254)
return fail(index, "native texture dimension query mode");
- result.required_texture_buffer = 2;
- emit(0xdf48000000000000ULL | predicate | dest | (u64{insn.Bits(24, 8)} << 8) |
- (u64{1} << 22) | (u64{insn.Bits(72, 4)} << 31) | (u64{insn.Bits(40, 14)} << 36) |
- (u64{insn.Bits(90, 1)} << 49));
+ if (!bindless) {
+ if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)
+ return fail(index, "native texture descriptor buffer conflict");
+ result.required_texture_buffer = binding;
+ }
+ emit((bindless ? 0xdf50000000000000ULL : 0xdf48000000000000ULL) | predicate | (direct ? dest : 250) |
+ (u64{insn.Bits(24, 8)} << 8) | (u64{1} << 22) | (u64{mask} << 31) |
+ (u64{bindless ? 0U : insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49));
+ for (u32 i = 0; !direct && i < components; ++i) {
+ const u32 target = i < 2 ? (dest == 255 ? 255U : static_cast<u32>(dest) + i) : second_dest + (i - 2);
+ if (target != 255)
+ emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{250 + i} << 20));
+ }
break;
}
- case 0xb66: { // TLD.SCR.LZ, direct texel fetch
+ case 0xb66: { // TLD.SCR, direct texel fetch at level zero (.LZ) or a given level (.LL)
const u32 mask = insn.Bits(72, 4);
+ const bool level = insn.Bits(87, 3) == 3; // .LL: the level follows the coordinates
+ // Bit 76: a texel offset after the coordinates and level. 2D only, as Maxwell TLDS
+ // 2D.LZ.AOFFI (x, y in Ra, Ra+1, offsets in Rb) or 2D.LL.AOFFI (level, offsets in Rb, Rb+1).
+ const bool offsets = insn.Bits(76, 1) != 0;
const u32 dimension = insn.Bits(61, 3);
const bool array = dimension == 5;
const bool volume = dimension == 2;
@@ -1391,8 +1684,12 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
for (u32 i = 0; i < rg_masks.size(); ++i) if (rg_masks[i] == mask) swizzle = i;
}
if (!insn.Bits(60, 1) || (dimension != 0 && dimension != 1 && !array && !volume) ||
- swizzle == ~0U || insn.Bits(76, 5) ||
- insn.Bits(81, 3) != 7 || insn.Bits(87, 3) != 1 ||
+ swizzle == ~0U || (insn.Bits(76, 5) & ~1U) ||
+ (offsets && (dimension != 1 || (source_a & 1) || source_a >= 254 || source_b == 255 ||
+ (level && ((source_b & 1) || source_b >= 254)))) ||
+ insn.Bits(81, 3) != 7 || (insn.Bits(87, 3) != 1 && !level) || (level && volume) ||
+ (level && dimension == 1 && ((source_a & 1) || source_a >= 254)) ||
+ (level && array && ((source_b & 1) || source_b >= 254)) ||
insn.Bits(59, 1) ||
insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) >= 18 ||
dest == 255 || (mask & (mask - 1) && ((dest & 1) || dest >= 254)) ||
@@ -1402,9 +1699,46 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
return fail(index, "native texel fetch mode or descriptor binding");
}
const u32 binding = insn.Bits(54, 5);
- if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)
- return fail(index, "native texture descriptor buffer conflict");
- result.required_texture_buffer = binding;
+ if (!bind_texture_buffer(binding)) {
+ if (level || offsets)
+ return fail(index, "native texture descriptor buffer conflict");
+ // A second descriptor buffer: TLD.B (LZ) with the coordinates in R250.. (array
+ // index or Z after Ra) and the handle, loaded from that buffer, in R253.
+ const auto copy = [&](u32 target, u32 source) {
+ emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{source} << 20));
+ };
+ copy(250, source_a);
+ if (array || volume) copy(251, source_a + 1);
+ if (dimension != 0) copy(array || volume ? 252 : 251, source_b);
+ emit(0x4c98000000070000ULL | 253 | (u64{15} << 39) | (u64{insn.Bits(40, 14)} << 20) |
+ (u64{binding} << 34));
+ emit(0xdd00000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{253} << 20) |
+ (u64{array ? 3U : volume ? 4U : dimension == 0 ? 0U : 2U} << 28) | (u64{mask} << 31) |
+ (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51));
+ const u32 components = static_cast<u32>(std::popcount(mask));
+ for (u32 i = 0; i < components; ++i)
+ copy(i < 2 || !split ? static_cast<u32>(dest) + i : second_dest + (i - 2), 250 + i);
+ break;
+ }
+ if (array && level) {
+ // TLDS has no array form with a level: TLD takes (layer, x, y) from Ra and the level
+ // from Rb (layer, x in Ra, Ra+1; y, level in Rb, Rb+1 here). Results come out packed,
+ // then go where the Ampere destinations (Rd, Rd+1, then the second pair) take them.
+ for (u32 i = 0; i < 2; ++i) {
+ emit(0x5c98000000000000ULL | predicate | (250 + i) | (u64{15} << 39) | (u64{source_a + i} << 20));
+ emit(0x5c98000000000000ULL | predicate | (252 + i) | (u64{15} << 39) | (u64{source_b + i} << 20));
+ }
+ emit(0xdc00000000000000ULL | predicate | 246 | (u64{250} << 8) | (u64{253} << 20) |
+ (u64{3} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) |
+ (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51) | (u64{1} << 55));
+ const u32 components = static_cast<u32>(std::popcount(mask));
+ for (u32 i = 0; i < components; ++i) {
+ const u32 target = !split || i < 2 ? static_cast<u32>(dest) + i : second_dest + (i - 2);
+ if (target != 255 && (split || i < 2))
+ emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{246 + i} << 20));
+ }
+ break;
+ }
if (array) {
emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{source_a + 1} << 20));
emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{source_b} << 20));
@@ -1413,7 +1747,42 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
emit(0xda00000000000000ULL | predicate | dest | (u64{source_a} << 8) |
(u64{source_b} << 20) | (u64{insn.Bits(64, 8)} << 28) |
(u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) |
- (u64{swizzle} << 50) | (u64{array ? 8U : volume ? 7U : dimension == 0 ? 0U : 2U} << 53));
+ (u64{swizzle} << 50) |
+ (u64{offsets ? (level ? 12U : 4U)
+ : array ? 8U : volume ? 7U : dimension == 0 ? (level ? 1U : 0U) : (level ? 5U : 2U)} << 53));
+ break;
+ }
+ case 0x367: { // TLD.B at level zero (.LZ), the handle in a register.
+ // The values are the coordinates, then the handle, split over Ra and Rb as for TEX.B
+ // (ARRAY_2D: layer, x | y, handle); Maxwell TLD.B takes the handle first in Rb.
+ constexpr std::array<u32, 8> coordinate_count{1, 2, 3, 0, 0, 3, 0, 0};
+ constexpr std::array<u32, 8> maxwell_type{0, 2, 4, 0, 0, 3, 0, 0};
+ const u32 mask = insn.Bits(72, 4), second_dest = insn.Bits(64, 8);
+ const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8);
+ const u32 dimension = insn.Bits(61, 3), coordinates = coordinate_count[dimension];
+ const u32 count = coordinates + 1, in_a = (count + 1) / 2;
+ if (coordinates == 0 || !insn.Bits(59, 1) || !insn.Bits(60, 1) || insn.Bits(40, 19) ||
+ insn.Bits(76, 5) || insn.Bits(81, 3) != 7 || insn.Bits(87, 3) != 1 || insn.Bits(91, 1) ||
+ mask == 0 || source_a == 255 || source_a + in_a > 246 || source_b == 255 ||
+ source_b + (count - in_a) > 246)
+ return fail(index, "native bindless texel fetch mode");
+ const auto value = [&](u32 i) { return i < in_a ? source_a + i : source_b + (i - in_a); };
+ const auto copy = [&](u32 target, u32 source) {
+ emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{source} << 20));
+ };
+ for (u32 i = 0; i < coordinates; ++i) copy(250 + i, value(i));
+ copy(246, value(coordinates));
+ emit(0xdd00000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{246} << 20) |
+ (u64{maxwell_type[dimension]} << 28) | (u64{mask} << 31) | (u64{insn.Bits(90, 1)} << 49) |
+ (u64{7} << 51));
+ const u32 components = static_cast<u32>(std::popcount(mask));
+ const bool split = second_dest != 255;
+ for (u32 i = 0; i < components; ++i) {
+ u32 target = 255;
+ if (!split || i < 2) target = dest == 255 ? 255U : static_cast<u32>(dest) + i;
+ else target = second_dest + (i - 2);
+ if (target != 255) copy(target, 250 + i);
+ }
break;
}
case 0x91a: // DEPBAR.LE SB5, 1: scoreboard wait, represented by IR data dependencies.
@@ -1421,22 +1790,35 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
return fail(index, "native dependency barrier mode");
}
break;
- case 0xb69: { // TMML (LOD query), 2D. Results are 8.8 fixed point, as on Maxwell.
+ case 0xb69: case 0xf69: case 0x36a: { // TMML (LOD query), 1D/2D/3D/CUBE. Results are 8.8 fixed point, as on Maxwell.
+ // 0xf69: the handle in a uniform register (bit 91); 0x36a, TMML.B: the handle in Rb and
+ // every coordinate in Ra. Both become Maxwell TMML.B with the handle in Rb.
+ const bool bindless = opcode != 0xb69;
+ const u32 handle = opcode == 0xf69 ? uniform_register(insn.Bits(40, 6))
+ : opcode == 0x36a ? insn.Bits(32, 8) : 255;
const u32 mask = insn.Bits(72, 4), second_dest = insn.Bits(64, 8);
const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8);
- const u32 binding = insn.Bits(54, 5);
- if (insn.Bits(61, 3) != 1 || insn.Bits(76, 4) || insn.Bits(80, 10) || insn.Bits(59, 1) ||
- insn.Bits(40, 14) > 0x1fff || binding >= 18 || mask == 0 || source_a >= 246 ||
- (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding))
+ const u32 binding = insn.Bits(54, 5), dimension = insn.Bits(61, 3);
+ // Coordinates: all from Ra when Rb is RZ, else split as for TEX (2D: Ra, Rb).
+ constexpr std::array<u32, 4> coordinate_count{1, 2, 3, 3};
+ const u32 coordinates = dimension < 4 ? coordinate_count[dimension] : 0;
+ const u32 in_a = source_b == 255 || opcode == 0x36a ? coordinates : (coordinates + 1) / 2;
+ if (coordinates == 0 || insn.Bits(76, 4) || insn.Bits(80, 10) || mask == 0 ||
+ insn.Bits(59, 1) != (opcode == 0x36a ? 1U : 0U) || source_a + in_a > 246 ||
+ (in_a < coordinates && source_b + (coordinates - in_a) > 246) ||
+ (opcode == 0xf69 && (handle >= 246 || insn.Bits(46, 13) || !insn.Bits(91, 1))) ||
+ (opcode == 0x36a && (handle >= 246 || insn.Bits(40, 19) || insn.Bits(91, 1))) ||
+ (!bindless && (insn.Bits(40, 14) > 0x1fff || binding >= 18 ||
+ (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding))))
return fail(index, "native texture LOD query mode");
- result.required_texture_buffer = binding;
- // Coordinates: (Ra, Ra+1) when Rb is RZ, else (Ra, Rb).
- emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{source_a} << 20));
- emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) |
- (u64{source_b == 255 ? source_a + 1 : source_b} << 20));
- emit(0xdf58000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{255} << 20) |
- (u64{2} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) |
- (u64{insn.Bits(90, 1)} << 49));
+ if (!bindless) result.required_texture_buffer = binding;
+ for (u32 i = 0; i < coordinates; ++i)
+ emit(0x5c98000000000000ULL | predicate | (250 + i) | (u64{15} << 39) |
+ (u64{i < in_a ? source_a + i : source_b + (i - in_a)} << 20));
+ if (bindless) emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{handle} << 20));
+ emit((bindless ? 0xdf60000000000000ULL : 0xdf58000000000000ULL) | predicate | 250 | (u64{250} << 8) |
+ (u64{bindless ? 246U : 255U} << 20) | (u64{dimension * 2} << 28) | (u64{mask} << 31) |
+ (u64{bindless ? 0U : insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49));
const u32 components = static_cast<u32>(std::popcount(mask));
const bool split = second_dest != 255;
for (u32 i = 0; i < components; ++i) {
@@ -1448,22 +1830,43 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
}
break;
}
- case 0xb63: { // TLD4 (gather), 2D without offsets or depth compare; component at bits 87-88.
+ case 0xb63: case 0xf63: { // TLD4 (gather), 2D without offsets or depth compare; component at bits 87-88.
+ // 0xf63: the handle in a uniform register (bit 91), Maxwell TLD4.B with the handle in Rb.
+ const bool bindless = opcode == 0xf63;
+ const u32 handle = bindless ? uniform_register(insn.Bits(40, 6)) : 255;
const u32 mask = insn.Bits(72, 4), second_dest = insn.Bits(64, 8);
const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8);
const u32 binding = insn.Bits(54, 5);
- if (insn.Bits(61, 3) != 1 || insn.Bits(76, 4) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) ||
- insn.Bits(40, 14) > 0x1fff || binding >= 18 || insn.Bits(89, 1) || mask == 0 ||
- (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding))
+ // Bit 76: a texel offset after the coordinates (x, y in Ra, Ra+1, offsets in Rb, as
+ // for TEX); Maxwell TLD4 takes it as AOFFI (bits 54-55) from Rb.
+ const bool offsets = insn.Bits(76, 1) != 0;
+ if (insn.Bits(61, 3) != 1 || insn.Bits(77, 3) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) ||
+ (offsets && (bindless || source_b == 255 || (source_a != 255 && source_a >= 245))) ||
+ insn.Bits(89, 1) || mask == 0 ||
+ (bindless ? (handle >= 246 || insn.Bits(46, 13) || !insn.Bits(91, 1))
+ : (insn.Bits(40, 14) > 0x1fff || binding >= 18 ||
+ (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding))))
return fail(index, "native texture gather mode");
- result.required_texture_buffer = binding;
- // Two coordinates: x in Ra, y in Rb (the coordinate list splits evenly across them).
+ if (!bindless) result.required_texture_buffer = binding;
+ // Two coordinates: x in Ra, y in Rb (the coordinate list splits evenly across them);
+ // with an offset, x and y in Ra, Ra+1 and the offset in Rb.
+ const u32 y = !offsets ? source_b : source_a == 255 ? 255U : source_a + 1;
emit(0x5c98000000000000ULL | predicate | 250 | (u64{15} << 39) | (u64{source_a} << 20));
- emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) |
- (u64{source_b == 255 ? 255U : source_b} << 20));
- emit(0xc800000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{255} << 20) |
- (u64{2} << 28) | (u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) |
- (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51) | (u64{insn.Bits(87, 2)} << 56));
+ emit(0x5c98000000000000ULL | predicate | 251 | (u64{15} << 39) | (u64{y} << 20));
+ if (offsets) {
+ emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{source_b} << 20));
+ }
+ if (bindless) {
+ emit(0x5c98000000000000ULL | predicate | 246 | (u64{15} << 39) | (u64{handle} << 20));
+ emit(0xdec0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{246} << 20) |
+ (u64{2} << 28) | (u64{mask} << 31) | (u64{insn.Bits(87, 2)} << 38) |
+ (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51));
+ } else {
+ emit(0xc800000000000000ULL | predicate | 250 | (u64{250} << 8) |
+ (u64{offsets ? 246U : 255U} << 20) | (u64{2} << 28) | (u64{mask} << 31) |
+ (u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) | (u64{7} << 51) |
+ (u64{offsets ? 1U : 0U} << 54) | (u64{insn.Bits(87, 2)} << 56));
+ }
const u32 components = static_cast<u32>(std::popcount(mask));
const bool split = second_dest != 255;
for (u32 i = 0; i < components; ++i) {
@@ -1475,7 +1878,15 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
}
break;
}
- case 0xb60: { // TEX, scalar 2D implicit-LOD sample
+ case 0xb60: case 0xf60: case 0x361: { // TEX; 0xf60: handle in a uniform register; 0x361: TEX.B
+ // Bindless forms become Maxwell TEX.B, the handle first among the meta registers (the
+ // texture pass tracks it back to its constant buffer words). 0x361 lists the handle
+ // right after the coordinates, then the LOD and the offsets (see general_tex).
+ const bool bindless = opcode != 0xb60;
+ const u32 handle = opcode == 0xf60 ? uniform_register(insn.Bits(40, 6)) : 255;
+ if (bindless && ((opcode == 0xf60 && (handle == ~0U || handle >= 246 || insn.Bits(46, 13))) ||
+ (opcode == 0x361 && (insn.Bits(40, 19) || insn.Bits(87, 3) > 3))))
+ return fail(index, "native bindless texture mode");
const u32 mask = insn.Bits(72, 4);
const u32 second_dest = insn.Bits(64, 8);
constexpr std::array<u32, 8> rg_masks{1, 2, 4, 8, 3, 9, 10, 12};
@@ -1485,12 +1896,12 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
const bool depth_compare = insn.Bits(78, 1);
const u32 dimension = insn.Bits(61, 3), lod = insn.Bits(87, 3);
const u32 source_a = insn.Bits(24, 8), source_b = insn.Bits(32, 8);
- if (dimension == 5 && depth_compare && lod == 0 && !split && mask == 1) {
+ if (!bindless && dimension == 5 && depth_compare && lod == 0 && !split && mask == 1) {
if (!insn.Bits(60, 1) || insn.Bits(76, 2) || insn.Bits(81, 3) != 7 ||
insn.Bits(59, 1) || insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) != 2 ||
(source_a & 1) || (source_b & 1) || source_a >= 248 || source_b >= 248 || dest >= 248)
return fail(index, "native array depth comparison sample mode");
- result.required_texture_buffer = 2;
+ if (!bind_texture_buffer(2)) return fail(index, "native texture descriptor buffer conflict");
for (u32 i = 0; i < 3; ++i) {
const u32 source = i < 2 ? source_a + i : source_b;
emit(0x5c98000000000000ULL | predicate | (250 + i) |
@@ -1510,14 +1921,23 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
constexpr std::array<u32, 8> coordinate_count{1, 2, 3, 3, 2, 3, 0, 4};
constexpr std::array<u32, 8> maxwell_type{0, 2, 4, 6, 1, 3, 0, 7};
const bool offsets = insn.Bits(76, 1) != 0;
- if (dimension == 6 || insn.Bits(77, 1) || insn.Bits(81, 3) != 7 || insn.Bits(59, 1) ||
- insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) >= 18 || (lod > 3 && lod != 6 && lod != 7) ||
- (depth_compare && mask != 1) || mask == 0)
+ // Bit 59 is .B, set in 0x361's encoding.
+ if (dimension == 6 || insn.Bits(77, 1) || insn.Bits(81, 3) != 7 ||
+ insn.Bits(59, 1) != (opcode == 0x361 ? 1U : 0U) ||
+ (!bindless && (insn.Bits(40, 14) > 0x1fff || insn.Bits(54, 5) >= 18)) ||
+ (lod > 3 && lod != 6 && lod != 7) || (depth_compare && mask != 1) || mask == 0)
return false;
const u32 coordinates = coordinate_count[dimension];
const bool has_lod = lod == 2 || lod == 3 || lod == 6 || lod == 7;
- const u32 count = coordinates + (has_lod ? 1 : 0) + (offsets ? 1 : 0) + (depth_compare ? 1 : 0);
- const u32 in_a = count <= 4 ? (count + 1) / 2 : 4;
+ // 0x361's handle is one more value after the coordinates: 2D.DC is (x, y | handle,
+ // reference), ARRAY_2D is (layer, x | y, handle); with five or more values the
+ // coordinates keep Ra (CUBE.LL is (x, y, z | handle, LOD)).
+ const u32 handle_slot = opcode == 0x361 ? 1 : 0;
+ const u32 count = coordinates + handle_slot + (has_lod ? 1 : 0) + (offsets ? 1 : 0) +
+ (depth_compare ? 1 : 0);
+ // Unpacked 0x361 (bit 60 clear): Ra holds the coordinates, Rb the handle and the rest.
+ const u32 in_a = opcode == 0x361 && !insn.Bits(60, 1) ? coordinates
+ : count <= 4 ? (count + 1) / 2 : opcode == 0x361 ? coordinates : 4;
const auto value = [&](u32 i) -> u32 {
if (i < in_a) return source_a == 255 ? 255U : source_a + i;
return source_b == 255 ? 255U : source_b + (i - in_a);
@@ -1525,25 +1945,40 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
if ((source_a != 255 && source_a + in_a > 246) ||
(count > in_a && source_b != 255 && source_b + (count - in_a) > 246))
return false;
+ const u32 texture_handle = opcode == 0x361 ? value(coordinates) : handle;
+ if (bindless && texture_handle >= 246) return false;
+ // A second descriptor buffer is read bindless: TEX.B takes the handle, loaded from
+ // that buffer, as its first meta register.
const u32 binding = insn.Bits(54, 5);
- if (result.required_texture_buffer != ~0U && result.required_texture_buffer != binding)
- return false;
- result.required_texture_buffer = binding;
+ const bool second_buffer = !bindless && !bind_texture_buffer(binding);
+ const bool tex_b = bindless || second_buffer;
const auto copy = [&](u32 target, u32 source) {
emit(0x5c98000000000000ULL | predicate | target | (u64{15} << 39) | (u64{source} << 20));
};
// Coordinates to R250.., then LOD/bias, offsets and reference to R246...
for (u32 i = 0; i < coordinates; ++i) copy(250 + i, value(i));
u32 meta = 246;
- u32 next = coordinates;
+ u32 next = coordinates + handle_slot;
+ if (bindless) copy(meta++, texture_handle);
+ else if (second_buffer)
+ emit(0x4c98000000070000ULL | meta++ | (u64{15} << 39) | (u64{insn.Bits(40, 14)} << 20) |
+ (u64{binding} << 34));
if (has_lod) copy(meta++, value(next++));
if (offsets) copy(meta++, value(next++));
if (depth_compare) copy(meta++, value(next++));
+ if (tex_b) {
+ // Maxwell TEX.B: AOFFI at 36 and the LOD mode at 37 instead of 54 and 55.
+ emit(0xde80000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{246} << 20) |
+ (u64{maxwell_type[dimension]} << 28) | (u64{mask} << 31) | (u64{offsets} << 36) |
+ (u64{lod} << 37) | (u64{insn.Bits(90, 1)} << 49) | (u64{depth_compare} << 50) |
+ (u64{7} << 51));
+ } else {
emit(0xc000000000000000ULL | predicate | 250 | (u64{250} << 8) |
(u64{meta == 246 ? 255U : 246U} << 20) | (u64{maxwell_type[dimension]} << 28) |
(u64{mask} << 31) | (u64{insn.Bits(40, 14)} << 36) |
(u64{insn.Bits(90, 1)} << 49) | (u64{depth_compare} << 50) | (u64{7} << 51) |
(u64{offsets} << 54) | (u64{lod} << 55));
+ }
const u32 components = static_cast<u32>(std::popcount(mask));
if (insn.Bits(79, 1)) {
// FP16 results: (c0, c1) packed into the first destination, (c2, c3) into
@@ -1578,7 +2013,7 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
// SM70 scalar coordinates and split RG/BA destinations match TEXS.
// Descriptor offsets are words in both encodings, but only native
// explicitly encodes the constant-buffer binding.
- if (!insn.Bits(60, 1) || (dimension != 1 && dimension != 5) ||
+ if (bindless || !insn.Bits(60, 1) || (dimension != 1 && dimension != 5) ||
swizzle == ~0U || insn.Bits(76, 2) ||
insn.Bits(81, 3) != 7 || (lod > 1 && (lod != 3 || dimension != 1 || depth_compare)) ||
(lod == 3 && ((source_a & 1) || source_a >= 249)) ||
@@ -1587,11 +2022,11 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
dest == 255 || (mask & (mask - 1) && ((dest & 1) || dest >= 254)) ||
(split && mask == 15 && ((second_dest & 1) || second_dest >= 254)) ||
(depth_compare && (dimension != 1 || split || mask != 1 || (source_a & 1) || source_a >= 249)) ||
- (dimension == 5 && ((source_a & 1) || source_a >= 249))) {
+ (dimension == 5 && ((source_a & 1) || source_a >= 249)) || bindless ||
+ !bind_texture_buffer(2)) {
if (!general_tex()) return fail(index, "native texture mode or descriptor binding");
break;
}
- result.required_texture_buffer = 2;
emit((0xd800000000000000ULL & ~(u64{insn.Bits(79, 1)} << 59)) | predicate | dest | (u64{insn.Bits(24, 8)} << 8) |
(u64{insn.Bits(32, 8)} << 20) | (u64{insn.Bits(64, 8)} << 28) |
(u64{insn.Bits(40, 14)} << 36) | (u64{insn.Bits(90, 1)} << 49) |
@@ -1631,11 +2066,12 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
}
break;
}
- case 0x307: { // FRND.F32, round to an integral floating-point value
+ case 0x307: case 0xb07: { // FRND.F32, round to an integral floating-point value (0xb07: cbuf)
if (insn.Bits(75, 2) != 2 || insn.Bits(84, 2) != 2 || insn.Bits(81, 3) ||
insn.Bits(86, 6)) return fail(index, "native floating round format");
- emit(base(0x5ca8000000000000ULL) | (u64{2} << 8) | (u64{2} << 10) |
- (u64{insn.Bits(32, 8)} << 20) | (u64{8 + insn.Bits(78, 2)} << 39) |
+ if (opcode == 0xb07 && !valid_cbuf()) return fail(index, "native constant buffer address");
+ emit(base(opcode == 0xb07 ? 0x4ca8000000000000ULL : 0x5ca8000000000000ULL) | (u64{2} << 8) | (u64{2} << 10) |
+ (opcode == 0xb07 ? cbuf() : u64{insn.Bits(32, 8)} << 20) | (u64{8 + insn.Bits(78, 2)} << 39) |
(u64{insn.Bits(80, 1)} << 44) | (u64{insn.Bits(63, 1)} << 45) |
(u64{insn.Bits(62, 1)} << 49));
break;
@@ -1666,15 +2102,22 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
result.instructions.push_back(translated);
break;
}
- case 0x387: case 0x983: { // Captured scalar local spill at an immediate byte offset.
+ case 0x387: case 0x983: { // Local spill: 32, 64 or 128 bits at [Ra + immediate byte offset].
+ // The size is bits 73-75 (4, 5, 6 = 32, 64, 128 bits); wider accesses become 32-bit ones.
+ // A load's bit 85 (.LU) is a cache hint.
const bool store = opcode == 0x387;
- const u32 data = insn.Bits(32, 8), offset = insn.Bits(40, 24);
- if (insn.Bits(24, 8) != 255 || (store && (insn.Bits(16, 8) || data > 247)) ||
- (!store && (data || dest >= 248)) || (offset & 3) || offset > 0x7fffff ||
- insn.words[2] != (store ? 0x00100800U : 0x00300800U))
+ const u32 size = insn.Bits(73, 3);
+ const u32 data = insn.Bits(32, 8), offset = insn.Bits(40, 24), address = insn.Bits(24, 8);
+ const u32 words = size == 6 ? 4 : size == 5 ? 2 : 1, value = store ? data : static_cast<u32>(dest);
+ if ((address != 255 && address >= 246) || (store && insn.Bits(16, 8)) || (!store && data) ||
+ (value != 255 && (value + words > 248 || (value & (words - 1)))) || (!store && value == 255) ||
+ (offset & 3) || offset + 4 * words > 0x800000 || size < 4 || size > 6 ||
+ (insn.words[2] & ~(store ? 0xe00U : 0x200e00U)) != 0x00100000U)
return fail(index, "native local scalar spill mode");
- emit((store ? 0xef50000000000000ULL : 0xef40000000000000ULL) | predicate |
- (store ? data : dest) | (u64{255} << 8) | (u64{offset} << 20) | (u64{4} << 48));
+ for (u32 word = 0; word < words; ++word)
+ emit((store ? 0xef50000000000000ULL : 0xef40000000000000ULL) | predicate |
+ (value == 255 ? 255U : value + word) | (u64{address} << 8) |
+ (u64{offset + 4 * word} << 20) | (u64{4} << 48));
break;
}
case 0x803: { // P2R PR, register merge source, immediate mask, byte select (76-77)
@@ -1739,17 +2182,30 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
break;
}
case 0x209: case 0x809: case 0xa09: case 0xc09: { // FMNMX, including a uniform source
- if (insn.Bits(74, 6) || insn.Bits(81, 6) || (opcode != 0xc09 && insn.Bits(91, 1)))
+ if (insn.Bits(74, 6) || insn.Bits(82, 5) || (opcode != 0xc09 && insn.Bits(91, 1)))
return fail(index, "native floating min/max mode");
const u32 a = operand(0), b = operand(1);
+ // .NAN (bit 81): an unordered pair gives the canonical NaN. R252 = ~0 when both are numbers.
+ const bool propagate_nan = insn.Bits(81, 1);
+ if (propagate_nan)
+ emit(0x5800000000000000ULL | predicate | 252 | (u64{a} << 8) | (u64{b} << 20) | (u64{7} << 39) | (u64{7} << 48));
emit(base(0x5c60000000000000ULL) | (u64{a} << 8) | (u64{b} << 20) |
(u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42) |
(u64{insn.Bits(80, 1)} << 44));
+ if (propagate_nan) {
+ emit(base(0x5c40000000000000ULL) | (dest << 8) | (u64{252} << 20) | (u64{7} << 48));
+ emit(0x0400000000000000ULL | predicate | 252 | (u64{252} << 8) | (u64{0xffffffffU} << 20) | (u64{2} << 53));
+ emit(0x0400000000000000ULL | predicate | 252 | (u64{252} << 8) | (u64{0x7fffffffU} << 20));
+ emit(base(0x5c40000000000000ULL) | (dest << 8) | (u64{252} << 20) | (u64{1} << 41) | (u64{7} << 48));
+ }
break;
}
- case 0x308: case 0xb08: { // MUFU
+ case 0x308: case 0xb08: case 0xd08: { // MUFU (register, constant or uniform register source)
const u32 operation = insn.Bits(74, 6);
- if (operation > 8 || operation == 6 || operation == 7 || insn.Bits(80, 12)) return fail(index, "native MUFU mode");
+ // Bit 91 is part of the uniform-register form's encoding.
+ if (operation > 8 || operation == 6 || operation == 7 ||
+ insn.Bits(80, 12) != (opcode == 0xd08 ? 0x800U : 0U))
+ return fail(index, "native MUFU mode");
// SM70+ MUFU.COS/SIN take the angle in turns (compilers multiply by 1/2pi first);
// Eden's Maxwell MUFU takes radians (its RRO is a plain move), so scale by 2pi.
const bool turns = operation == 0 || operation == 1;
@@ -1768,7 +2224,8 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
emit(0x5080000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{operation} << 20));
emit(base(0x5ca8000000000000ULL) | (u64{1} << 8) | (u64{2} << 10) | (u64{250} << 20));
} else {
- u32 src = operand(1);
+ u32 src = opcode == 0xd08 ? uniform_register(insn.Bits(32, 8)) : operand(1);
+ if (src == ~0U) return fail(index, "native MUFU uniform source");
if (turns) src = to_radians(src);
emit(base(0x5080000000000000ULL) | (u64{src} << 8) | (u64{operation} << 20));
}
@@ -1838,7 +2295,7 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
mov_imm(static_cast<u32>(dest) + 1, 0);
break;
}
- case 0x306: case 0xd06: { // I2F.F32.U8, low byte, round-to-nearest (0xd06: uniform source)
+ case 0x306: case 0xd06: { // I2F.F32.U8, round-to-nearest (0xd06: uniform source)
const u32 source = opcode == 0xd06 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8);
if (opcode == 0x306 && insn.words[2] == 0x00101000 && !(insn.words[1] & 0xcfffff00U)) {
// I2F.U16 Rd, Rs(.H1): Maxwell's 16-bit source format, byte selector 0 or 2.
@@ -1846,10 +2303,12 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
(u64{insn.Bits(60, 2) * 2} << 41));
break;
}
+ // I2F.U8 Rd, Rs.B0-B3: the byte selector (bits 60-61) as Maxwell's (41-42).
if (insn.words[2] != (opcode == 0xd06 ? 0x08001000U : 0x1000U) ||
- (insn.words[1] & 0xffffff00U) || source == ~0U)
+ (insn.words[1] & (opcode == 0xd06 ? 0xffffff00U : 0xcfffff00U)) || source == ~0U)
return fail(index, "native byte-to-float conversion mode");
- emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{source} << 20));
+ emit(base(0x5cb8000000000000ULL) | (u64{2} << 8) | (u64{source} << 20) |
+ (u64{insn.Bits(60, 2)} << 41));
break;
}
case 0x245: case 0xa45: case 0xc45: { // I2FP.F32.U32
@@ -1862,13 +2321,15 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
}
case 0x207: case 0x807: case 0xa07:
case 0x208: case 0x808: case 0xa08: { // Bitwise SEL / FSEL
- const u32 a = operand(0, false), b = operand(1, false);
+ // FSEL applies the floating operand modifiers (negate/absolute) before selecting.
+ const bool floating = (opcode & 0x1ff) == 0x008;
+ const u32 a = operand(0, floating), b = operand(1, floating);
emit(base(0x5ca0000000000000ULL) | (u64{a} << 8) | (u64{b} << 20) |
(u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42));
break;
}
- case 0x20a: case 0x80a: case 0xa0a: { // FSET.BF, combined with Pc (bits 87-90) by bits 74-75
- if (insn.Bits(64, 8) || insn.Bits(74, 2) == 3 || insn.Bits(81, 6) || insn.Bits(91, 1))
+ case 0x20a: case 0x80a: case 0xa0a: case 0xc0a: { // FSET.BF, combined with Pc (bits 87-90) by bits 74-75 (0xc0a: uniform b)
+ if (insn.Bits(64, 8) || insn.Bits(74, 2) == 3 || insn.Bits(81, 6) || (opcode != 0xc0a && insn.Bits(91, 1)))
return fail(index, "native floating comparison output mode");
const u32 a = operand(0), b = operand(1);
emit(base(0x5800000000000000ULL) | (u64{a} << 8) | (u64{b} << 20) |
@@ -1876,21 +2337,61 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
(u64{insn.Bits(76, 4)} << 48) | (u64{1} << 52) | (u64{insn.Bits(80, 1)} << 55));
break;
}
- case 0x434: { // HSETP2 with an immediate half pair: A swizzle 74-75 (neg 72, abs 73),
+ case 0x234: case 0x434: { // HSETP2 with a register (B swizzle 60-61, abs 62, neg 63) or an
+ // immediate half pair: A swizzle 74-75 (neg 72, abs 73),
// combine 69-70, H_AND 71, compare 76-79, FTZ 80, P/Q 81-86, input 87-90.
- mov_imm(250, insn.words[1]);
+ u64 source = u64{250} << 20;
+ if (opcode == 0x234) {
+ if ((insn.words[1] & 0x0fffff00U) || insn.Bits(64, 5))
+ return fail(index, "native half comparison mode");
+ source = (u64{insn.Bits(32, 8)} << 20) | (u64{insn.Bits(60, 2)} << 28) |
+ (u64{insn.Bits(62, 1)} << 30) | (u64{insn.Bits(63, 1)} << 31);
+ } else {
+ mov_imm(250, insn.words[1]);
+ }
emit(0x5d20000000000000ULL | predicate | insn.Bits(84, 3) | (u64{insn.Bits(81, 3)} << 3) |
- (u64{insn.Bits(80, 1)} << 6) | (u64{insn.Bits(24, 8)} << 8) | (u64{250} << 20) |
+ (u64{insn.Bits(80, 1)} << 6) | (u64{insn.Bits(24, 8)} << 8) | source |
(u64{insn.Bits(76, 4)} << 35) | (u64{insn.Bits(87, 3)} << 39) |
(u64{insn.Bits(90, 1)} << 42) | (u64{insn.Bits(72, 1)} << 43) |
(u64{insn.Bits(73, 1)} << 44) | (u64{insn.Bits(69, 2)} << 45) |
(u64{insn.Bits(74, 2)} << 47) | (u64{insn.Bits(71, 1)} << 49));
break;
}
- case 0x20b: case 0x80b: case 0xa0b: // FSETP
+ case 0x20b: case 0x80b: case 0xa0b: case 0xc0b: // FSETP
case 0x20c: case 0x80c: case 0xa0c: case 0xc0c: { // ISETP
const bool floating = (opcode & 0x1ff) == 0xb;
- if (!floating && insn.Bits(72, 1)) return fail(index, "native extended integer comparison");
+ if (!floating && insn.Bits(72, 1)) {
+ // ISETP.EX (bit 72): the high-word half of a 64-bit compare. The low words'
+ // result comes in as the predicate at bits 68-70 (negated by 71), e.g.
+ // ISETP.NE P0 = lo != 0, then ISETP.NE.EX P0 = (hi != 0) OR P0. EQ: high equal
+ // AND low; NE: high different OR low; the orderings: high strictly ordered OR
+ // (high equal AND low). One or two Maxwell ISETPs into the destination; only
+ // the plain form (no second destination, AND with PT) is handled.
+ const u32 compare = insn.Bits(76, 3);
+ const u32 destination = insn.Bits(81, 3);
+ const u32 low = insn.Bits(68, 4);
+ if (insn.Bits(84, 3) != 7 || insn.Bits(87, 4) != 7 || insn.Bits(74, 2) != 0 ||
+ compare == 0 || compare == 7 ||
+ (destination != 7 && insn.Bits(12, 3) == destination))
+ return fail(index, "native extended integer comparison mode");
+ const u32 a = operand(0, false), b = operand(1, false);
+ const auto isetp = [&](u32 op, u32 input, u32 combine) {
+ return 0x5b60000000000000ULL | predicate | 7 | (u64{destination} << 3) |
+ (u64{a} << 8) | (u64{b} << 20) | (u64{input & 7} << 39) |
+ (u64{input >> 3} << 42) | (u64{combine} << 45) |
+ (u64{insn.Bits(73, 1)} << 48) | (u64{op} << 49);
+ };
+ constexpr u32 And = 0, Or = 1, Lt = 1, Eq = 2, Gt = 4, Ne = 5;
+ if (compare == Eq) {
+ emit(isetp(Eq, low, And));
+ } else if (compare == Ne) {
+ emit(isetp(Ne, low, Or));
+ } else {
+ emit(isetp(Eq, low, And));
+ emit(isetp(compare <= 3 ? Lt : Gt, destination, Or));
+ }
+ break;
+ }
const u32 a = operand(0, floating), b = operand(1, floating);
const u32 combine = insn.Bits(74, 2);
if (combine > 2) return fail(index, "native predicate combination");
@@ -1938,15 +2439,19 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
}
case 0x217: case 0x817: case 0xa17: case 0xc17: { // IMNMX register, immediate, cbuf or uniform operand.
if ((insn.words[2] & ~(0x04000200U | (opcode == 0xc17 ? 0x08000000U : 0U))) != 0x03800000 ||
- (opcode != 0xa17 && insn.words[1] > (opcode == 0x817 ? 0x7ffffU : 255U)) ||
+ (opcode == 0x817 && insn.words[1] > 0x7ffffU && insn.words[1] < 0xfff80000U) ||
+ (opcode != 0xa17 && opcode != 0x817 && insn.words[1] > 255U) ||
(opcode == 0xa17 && (!valid_cbuf() || insn.Bits(62, 2))))
return fail(index, "native integer min/max mode");
+ // An immediate is a signed 20-bit value: 19 bits at 20, the sign in bit 56.
const u32 source = opcode == 0xc17 ? uniform_register(insn.words[1])
: opcode == 0xa17 ? operand(1, false)
+ : opcode == 0x817 ? insn.words[1] & 0x7ffffU
: insn.words[1];
if (source == ~0U) return fail(index, "native uniform min/max operand");
emit(base(opcode == 0x817 ? 0x3820000000000000ULL : 0x5c20000000000000ULL) |
(u64{insn.Bits(24, 8)} << 8) | (u64{source} << 20) |
+ (u64{opcode == 0x817 ? insn.words[1] >> 31 : 0U} << 56) |
(u64{insn.Bits(87, 4)} << 39) | (u64{insn.Bits(73, 1)} << 48));
break;
}
@@ -1976,6 +2481,20 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
break;
}
case 0xa11: { // LEA with constant base and carry predicate.
+ if ((insn.words[2] & ~(0x07800000U | 0x0000f800U)) == 0x000f06ffU && !insn.Bits(62, 2) &&
+ insn.Bits(75, 5) != 0) {
+ // LEA.HI.X.SX32, no high source: (sign-extended Ra << shift) >> 32, plus the
+ // constant high word and the carry predicate (bits 87-90).
+ const u32 b = operand(1, false);
+ emit(0x3828000000000000ULL | predicate | 250 | (u64{insn.Bits(24, 8)} << 8) |
+ (u64{32 - insn.Bits(75, 5)} << 20) | (u64{1} << 48));
+ mov_imm(252, 1);
+ emit(0x5ca0000000000000ULL | predicate | 252 | (u64{252} << 8) | (u64{255} << 20) |
+ (u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42));
+ emit(0x5c10000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{b} << 20));
+ emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{252} << 20));
+ break;
+ }
if (insn.Bits(72, 1) || insn.Bits(74, 1) || insn.Bits(80, 1) ||
insn.Bits(81, 3) == 7 || insn.Bits(62, 2))
return fail(index, "native constant LEA carry mode");
@@ -1986,35 +2505,73 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
(dest << 8) | (u64{b} << 20) | (u64{7} << 39) | (u64{1} << 49));
break;
}
- case 0x211: case 0x811: { // LEA low word, no carry result
+ case 0x211: case 0x811: case 0xc11: { // LEA low word (0xc11: uniform Rb)
if (insn.Bits(80, 1)) {
- // LEA.HI Rd, Ra, Rb (or an immediate, 0x811), RZ, s: Rb + high word of (Ra << s);
- // bit 73 sign-extends Ra.
- if (insn.Bits(72, 1) || insn.Bits(74, 1) || insn.Bits(81, 3) != 7 ||
- insn.Bits(64, 8) != 255 || (opcode == 0x211 && insn.Bits(62, 2)))
+ // LEA.HI Rd, Ra, Rb (or an immediate, 0x811), Rc, s: Rb + high word of (Rc:Ra << s);
+ // bit 73 sign-extends Ra (Rc is RZ) and .X (bit 74) adds the carry predicate in bits 87-90.
+ const bool extended = insn.Bits(74, 1) != 0;
+ const u32 high = insn.Bits(64, 8);
+ if (insn.Bits(72, 1) || insn.Bits(81, 3) != 7 ||
+ (high != 255 && insn.Bits(73, 1)) || (opcode == 0x211 && insn.Bits(62, 2)) ||
+ (opcode == 0xc11 && (insn.words[1] & ~0x3fU)))
return fail(index, "native high effective address mode");
- u32 b = insn.Bits(32, 8);
+ u32 b = opcode == 0xc11 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8);
+ if (b == ~0U) return fail(index, "native uniform LEA register");
if (opcode == 0x811) {
b = insn.words[1] ? 251U : 255U;
if (b == 251) mov_imm(251, insn.words[1]);
}
const u64 shift = (u64{32 - insn.Bits(75, 5)} << 20) | (u64{insn.Bits(73, 1)} << 48);
- if (b == 255) {
- emit(base(0x3828000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | shift);
+ // The high word is a right shift of Ra, or with Rc a left funnel shift (SHF.L).
+ const auto high_word = [&](u64 target) {
+ if (high == 255) emit(target | 0x3828000000000000ULL | (u64{insn.Bits(24, 8)} << 8) | shift);
+ else emit(target | 0x36f8000000000000ULL | (u64{insn.Bits(24, 8)} << 8) | (u64{high} << 39) |
+ (u64{insn.Bits(75, 5)} << 20));
+ };
+ if (b == 255 && !extended) {
+ high_word(base(0));
} else {
- emit(0x3828000000000000ULL | predicate | 250 | (u64{insn.Bits(24, 8)} << 8) | shift);
+ high_word(predicate | 250);
+ if (extended) {
+ mov_imm(252, 1);
+ emit(0x5ca0000000000000ULL | predicate | 252 | (u64{252} << 8) | (u64{255} << 20) |
+ (u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42));
+ emit(0x5c10000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{252} << 20));
+ }
emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{b} << 20));
}
break;
}
- if (insn.Bits(72, 1) || insn.Bits(74, 1) || insn.Bits(80, 1) || insn.Bits(81, 3) != 7 ||
- (opcode == 0x211 && insn.Bits(62, 1))) return fail(index, "native LEA carry/high mode");
+ // A carry out (bits 81-83) is the low half of a 64-bit address; IADD3.X adds it next.
+ const u32 carry_out = insn.Bits(81, 3);
+ if (insn.Bits(72, 1) || insn.Bits(74, 1) || insn.Bits(80, 1) ||
+ (carry_out != 7 && ((opcode != 0x811 && insn.Bits(63, 1)) ||
+ (rewritten_uniform && (opcode != 0x211 || carry_out > 1)))) ||
+ (opcode == 0x211 && insn.Bits(62, 1)) || (opcode == 0xc11 && (insn.words[1] & ~0x8000003fU)))
+ return fail(index, "native LEA carry/high mode");
// 0x211 bit 63 negates Rb (LEA Rd, Ra, -Rb, s), as Maxwell ISCADD's bit 48.
- u32 b = insn.Bits(32, 8);
+ u32 b = opcode == 0xc11 ? uniform_register(insn.Bits(32, 8)) : insn.Bits(32, 8);
+ if (b == ~0U) return fail(index, "native uniform LEA register");
if (opcode == 0x811) { mov_imm(251, insn.words[1]); b = 251; }
+ if (carry_out != 7) {
+ // The sum in R250, carry = sum < Rb (unsigned), as IADD3's carry out; a uniform
+ // predicate goes through P(6-n) into R239.
+ const u32 target = rewritten_uniform ? 6 - carry_out : carry_out, mask = 1U << target;
+ emit(0x5c18000000000000ULL | predicate | 250 | (u64{insn.Bits(24, 8)} << 8) |
+ (u64{b} << 20) | (u64{insn.Bits(75, 5)} << 39));
+ if (rewritten_uniform) emit(0x38e8000000070000ULL | 253 | (u64{255} << 8) | (u64{mask} << 20));
+ emit(0x5b60000000000000ULL | predicate | 7 | (u64{target} << 3) | (u64{250} << 8) |
+ (u64{b} << 20) | (u64{7} << 39) | (u64{1} << 49));
+ if (rewritten_uniform) {
+ emit(0x38e8000000070000ULL | 239 | (u64{239} << 8) | (u64{mask} << 20));
+ emit(0x38f0000000070000ULL | (u64{253} << 8) | (u64{mask} << 20));
+ }
+ emit(base(0x5c98000000000000ULL) | (u64{15} << 39) | (u64{250} << 20));
+ break;
+ }
emit(base(0x5c18000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) |
(u64{b} << 20) | (u64{insn.Bits(75, 5)} << 39) |
- (u64{opcode == 0x211 ? insn.Bits(63, 1) : 0U} << 48));
+ (u64{opcode != 0x811 ? insn.Bits(63, 1) : 0U} << 48));
break;
}
case 0x210: case 0x810: case 0xa10: case 0xc10: { // IADD3 with zero third source and no carry outputs
@@ -2022,7 +2579,7 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
// a carry-in predicate (bits 87-89, negate 90). Uniform forms keep UPn in R239 bit 6-n.
if ((opcode == 0x210 || opcode == 0x810) && insn.Bits(64, 8) == 255 && !insn.Bits(73, 1) &&
!insn.Bits(75, 2) && insn.Bits(84, 3) == 7 && insn.Bits(77, 4) == 15 && !insn.Bits(72, 1) &&
- (insn.Bits(81, 3) != 7 || insn.Bits(74, 1))) {
+ (insn.Bits(81, 3) != 7 || insn.Bits(74, 1)) && (opcode == 0x810 || !insn.Bits(63, 1))) {
const bool carry_in = insn.Bits(74, 1) != 0;
const u32 carry_out = insn.Bits(81, 3);
const u32 a = insn.Bits(24, 8);
@@ -2065,9 +2622,65 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
emit(base(0x5c10000000000000ULL) | (u64{250} << 8) | (u64{251} << 20));
break;
}
- if (opcode == 0xa10 && insn.Bits(81, 3) != 7) {
+ // Three sources, negations or two carries: the sum builds up in R253 (.CC) with the carry
+ // count in R254 (.X). Pu/Pv (bits 81/84) are set when the count reaches 1/2, and IADD3.X
+ // adds both carry-ins (bits 87, 77). A negated source is ~x, plus 1 unless extended.
+ const bool extended = insn.Bits(74, 1) != 0;
+ const bool negate_a = insn.Bits(72, 1), negate_b = opcode != 0x810 && insn.Bits(63, 1), negate_c = insn.Bits(75, 1);
+ if ((extended || insn.Bits(81, 6) != 0x3f) &&
+ (insn.Bits(64, 8) != 255 || insn.Bits(84, 3) != 7 || negate_a || negate_b || negate_c ||
+ (extended && insn.Bits(77, 4) != 15))) {
+ const u32 first_output = insn.Bits(81, 3), second_output = insn.Bits(84, 3);
+ if (rewritten_uniform || guarded_uniform || insn.Bits(73, 1) || insn.Bits(76, 1) ||
+ (first_output != 7 && first_output == second_output) ||
+ (!extended && (insn.Bits(77, 4) != 15 || insn.Bits(87, 4) != 15)) ||
+ (opcode == 0x210 && (insn.words[1] & ~0x800000ffU)) || (opcode == 0xa10 && insn.Bits(62, 1)) ||
+ (opcode == 0xc10 && (insn.words[1] & ~0x8000003fU)))
+ return fail(index, "native three-input carry mode");
+ u32 a = insn.Bits(24, 8), b = operand(1, false), c = insn.Bits(64, 8);
+ const auto invert = [&](u32& source, u32 target) {
+ emit(0x0400000000000000ULL | predicate | target | (u64{source} << 8) | (u64{0xffffffffU} << 20) | (u64{2} << 53));
+ source = target;
+ };
+ if (negate_a) invert(a, 250);
+ if (negate_b) invert(b, 251);
+ if (negate_c) invert(c, 252);
+ const auto accumulate = [&](u32 source) {
+ emit(0x5c10000000000000ULL | predicate | 253 | (u64{253} << 8) | (u64{source} << 20) | (u64{1} << 47));
+ emit(0x5c10000000000000ULL | predicate | 254 | (u64{254} << 8) | (u64{255} << 20) | (u64{1} << 43));
+ };
+ emit(0x5c10000000000000ULL | predicate | 253 | (u64{a} << 8) | (u64{b} << 20) | (u64{1} << 47));
+ emit(0x5c10000000000000ULL | predicate | 254 | (u64{255} << 8) | (u64{255} << 20) | (u64{1} << 43));
+ accumulate(c);
+ if (extended) {
+ for (const u32 carry : {insn.Bits(87, 4), insn.Bits(77, 4)}) {
+ if (carry == 15) continue;
+ mov_imm(250, 1);
+ if (carry != 7)
+ emit(0x5ca0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{255} << 20) |
+ (u64{carry & 7} << 39) | (u64{carry >> 3} << 42));
+ accumulate(250);
+ }
+ } else if (const u32 ones = u32{negate_a} + u32{negate_b} + u32{negate_c}) {
+ mov_imm(250, ones);
+ accumulate(250);
+ }
+ if (dest != 255) emit(base(0x5c98000000000000ULL) | (u64{253} << 20) | (u64{15} << 39));
+ // An output that is also the guard predicate is written last.
+ const bool guard_first = (native_predicate & 7) != 7 && first_output == (native_predicate & 7);
+ for (const u32 output : {guard_first ? 1U : 0U, guard_first ? 0U : 1U}) {
+ const u32 target = output ? second_output : first_output;
+ if (target == 7) continue;
+ mov_imm(250, output + 1);
+ emit(0x5b60000000000000ULL | predicate | 7 | (u64{target} << 3) | (u64{254} << 8) |
+ (u64{250} << 20) | (u64{7} << 39) | (u64{6} << 49));
+ }
+ break;
+ }
+ if ((opcode == 0xa10 || opcode == 0xc10) && insn.Bits(81, 3) != 7) { // cbuf or uniform b
if (insn.Bits(64, 8) != 255 || insn.Bits(73, 4) || insn.Bits(84, 7) != 0x7f ||
- insn.Bits(77, 4) != 15 || insn.Bits(63, 1) || insn.Bits(72, 1))
+ insn.Bits(77, 4) != 15 || insn.Bits(63, 1) || insn.Bits(72, 1) ||
+ (opcode == 0xc10 && (insn.words[1] & ~0x3fU)))
return fail(index, "native constant add carry mode");
const u32 b = operand(1, false);
emit(base(0x5c10000000000000ULL) | (u64{insn.Bits(24, 8)} << 8) | (u64{b} << 20));
@@ -2075,13 +2688,15 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
(dest << 8) | (u64{b} << 20) | (u64{7} << 39) | (u64{1} << 49));
break;
}
- if (opcode == 0xa10 && insn.Bits(74, 1)) {
- if (insn.Bits(24, 8) != 255 || (insn.words[2] & ~0x07800000U) != 0x007fe4ffU)
+ if ((opcode == 0xa10 || opcode == 0xc10) && insn.Bits(74, 1)) { // IADD3.X Ra + cbuf or UR + carry predicate
+ if ((insn.words[2] & ~(0x07800000U | (opcode == 0xc10 ? 0x08000000U : 0U))) != 0x007fe4ffU ||
+ (opcode == 0xc10 && (insn.words[1] & ~0x3fU)))
return fail(index, "native extended constant add mode");
- const u32 b = operand(1, false);
+ const u32 a = insn.Bits(24, 8), b = operand(1, false);
mov_imm(250, 1);
emit(0x5ca0000000000000ULL | predicate | 250 | (u64{250} << 8) | (u64{255} << 20) |
(u64{insn.Bits(87, 3)} << 39) | (u64{insn.Bits(90, 1)} << 42));
+ if (a != 255) emit(0x5c10000000000000ULL | predicate | 250 | (u64{a} << 8) | (u64{250} << 20));
emit(base(0x5c10000000000000ULL) | (u64{b} << 8) | (u64{250} << 20));
break;
}
@@ -2214,6 +2829,24 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
return finish(index); // the closing self-branch
break;
}
+ case 0x923: case 0x927: { // ISBERD / ISBEWR: task and mesh programs' stage buffer.
+ // Ampere: Ra 24-31, offset 40-63, ISBERD dest 16-23, ISBEWR data 32-39; 75 = .32 (else
+ // U8), 76-77 = 3 .ATTR (0 the map region), 78 = .SKEW, 79 = .O (the output buffer).
+ // Kept as an nx2 pseudo instruction (Maxwell 0xefc0, free there) that the frontend
+ // reads for the task or mesh stage: data/dest 0-7, Ra 8-15, offset 20-43, .32 44,
+ // .ATTR 45, .SKEW 46, .O 47, write 48. Addresses are linear bytes in the region.
+ const bool write = opcode == 0x927;
+ const u32 region = insn.Bits(76, 2);
+ const bool output = insn.Bits(79, 1) != 0;
+ if ((insn.words[2] & ~0xf800U) != 0 || (region != 0 && region != 3) || write != output ||
+ (!write && region != 3) || (!write && dest == 255))
+ return fail(index, "native stage buffer access mode");
+ emit(0xefc0000000000000ULL | predicate | (write ? u64{insn.Bits(32, 8)} : dest) |
+ (u64{insn.Bits(24, 8)} << 8) | (u64{insn.Bits(40, 24)} << 20) |
+ (u64{insn.Bits(75, 1)} << 44) | (u64{region == 3} << 45) |
+ (u64{insn.Bits(78, 1)} << 46) | (u64{output} << 47) | (u64{write} << 48));
+ break;
+ }
case 0x94d: { // EXIT
if (insn.Bits(84, 2)) {
return fail(index, "native conditional/special EXIT");
@@ -2255,6 +2888,21 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
return fail(index, "unsupported native opcode");
}
if (!operand_error.empty()) return fail(index, operand_error);
+ if (f16_components != 0) {
+ // Each pair as F2FP.PACK_AB: the second value in the high half.
+ const u64 pred = u64{insn.Bits(12, 4)} << 16;
+ const auto pack = [&](u32 target, u32 low, u32 high) {
+ result.instructions.push_back(0x5ca8000000000000ULL | pred | 250 | (u64{1} << 8) |
+ (u64{2} << 10) | (u64{high} << 20));
+ result.instructions.push_back(0x5ca8000000000000ULL | pred | 251 | (u64{1} << 8) |
+ (u64{2} << 10) | (u64{low} << 20));
+ result.instructions.push_back(0x5c18000000000000ULL | pred | target | (u64{250} << 8) |
+ (u64{251} << 20) | (u64{16} << 39));
+ };
+ if (f16_dest != 255) pack(f16_dest, 248, f16_components > 1 ? 249U : 255U);
+ if (f16_second != 255 && f16_components > 2)
+ pack(f16_second, 246, f16_components > 3 ? 247U : 255U);
+ }
if (output_mask) {
emit(0x38e8000000070000ULL | 239 | (u64{239} << 8) | (u64{output_mask} << 20));
emit(0x38f0000000070000ULL | (u64{254} << 8) | (u64{output_mask} << 20));
@@ -2264,4 +2912,14 @@ inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
return fail(static_cast<u32>(code.size()), main_exited ? "native program lacks captured library return" : "native program has no unconditional EXIT");
}
+inline LoweredProgram LowerToMaxwell(std::span<const Instruction> code) {
+ // Programs using uniform registers above UR27 are lowered again with mirrors for them.
+ u32 high_uniforms = 0;
+ for (;;) {
+ LoweredProgram lowered = LowerToMaxwellWith(code, high_uniforms);
+ if (lowered || lowered.high_uniform_registers <= high_uniforms) return lowered;
+ high_uniforms = lowered.high_uniform_registers;
+ }
+}
+
} // namespace Shader::Ampere
diff --git a/src/shader_recompiler/frontend/ir/ir_emitter.cpp b/src/shader_recompiler/frontend/ir/ir_emitter.cpp
index 95815d7d51..23d1db5bf4 100644
--- a/src/shader_recompiler/frontend/ir/ir_emitter.cpp
+++ b/src/shader_recompiler/frontend/ir/ir_emitter.cpp
@@ -329,6 +329,22 @@ void IREmitter::SetPatch(Patch patch, const F32& value) {
Inst(Opcode::SetPatch, patch, value);
}
+U32 IREmitter::IsbeRead(const U32& address) {
+ return Inst<U32>(Opcode::IsbeRead, address);
+}
+
+void IREmitter::IsbeWriteAttribute(const U32& address, const U32& value) {
+ Inst(Opcode::IsbeWriteAttribute, address, value);
+}
+
+void IREmitter::IsbeWriteMapU8(const U32& address, const U32& value) {
+ Inst(Opcode::IsbeWriteMapU8, address, value);
+}
+
+void IREmitter::IsbeWriteMapU32(const U32& address, const U32& value) {
+ Inst(Opcode::IsbeWriteMapU32, address, value);
+}
+
void IREmitter::SetFragColor(u32 index, u32 component, const F32& value) {
Inst(Opcode::SetFragColor, Imm32(index), Imm32(component), value);
}
diff --git a/src/shader_recompiler/frontend/ir/ir_emitter.h b/src/shader_recompiler/frontend/ir/ir_emitter.h
index f22f0f75e2..d0cd88ca3c 100644
--- a/src/shader_recompiler/frontend/ir/ir_emitter.h
+++ b/src/shader_recompiler/frontend/ir/ir_emitter.h
@@ -88,6 +88,11 @@ public:
[[nodiscard]] F32 GetPatch(Patch patch);
void SetPatch(Patch patch, const F32& value);
+ [[nodiscard]] U32 IsbeRead(const U32& address);
+ void IsbeWriteAttribute(const U32& address, const U32& value);
+ void IsbeWriteMapU8(const U32& address, const U32& value);
+ void IsbeWriteMapU32(const U32& address, const U32& value);
+
void SetFragColor(u32 index, u32 component, const F32& value);
void SetSampleMask(const U32& value);
void SetFragDepth(const F32& value);
diff --git a/src/shader_recompiler/frontend/ir/microinstruction.cpp b/src/shader_recompiler/frontend/ir/microinstruction.cpp
index 84417980bc..a579ed1057 100644
--- a/src/shader_recompiler/frontend/ir/microinstruction.cpp
+++ b/src/shader_recompiler/frontend/ir/microinstruction.cpp
@@ -82,6 +82,9 @@ bool Inst::MayHaveSideEffects() const noexcept {
case Opcode::SetAttribute:
case Opcode::SetAttributeIndexed:
case Opcode::SetPatch:
+ case Opcode::IsbeWriteAttribute:
+ case Opcode::IsbeWriteMapU8:
+ case Opcode::IsbeWriteMapU32:
case Opcode::SetFragColor:
case Opcode::SetSampleMask:
case Opcode::SetFragDepth:
diff --git a/src/shader_recompiler/frontend/ir/opcodes.cpp b/src/shader_recompiler/frontend/ir/opcodes.cpp
index 94ba0f6ae0..1999f877fe 100644
--- a/src/shader_recompiler/frontend/ir/opcodes.cpp
+++ b/src/shader_recompiler/frontend/ir/opcodes.cpp
@@ -10,7 +10,7 @@ namespace Shader::IR {
namespace Detail {
-OpcodeMeta META_TABLE[534] = {
+OpcodeMeta META_TABLE[538] = {
#define OPCODE(name_token, type_token, ...) \
{ \
.name{#name_token}, \
@@ -21,7 +21,7 @@ OpcodeMeta META_TABLE[534] = {
#undef OPCODE
};
-u8 NUM_ARGS[534] = {
+u8 NUM_ARGS[538] = {
#define OPCODE(name_token, type_token, ...) u8(CalculateNumArgsOf(Opcode::name_token)),
#include "opcodes.inc"
#undef OPCODE
diff --git a/src/shader_recompiler/frontend/ir/opcodes.h b/src/shader_recompiler/frontend/ir/opcodes.h
index 50f9d7b155..c59f50adfe 100644
--- a/src/shader_recompiler/frontend/ir/opcodes.h
+++ b/src/shader_recompiler/frontend/ir/opcodes.h
@@ -57,12 +57,12 @@ static constexpr Type F64x2{Type::F64x2};
static constexpr Type F64x3{Type::F64x3};
static constexpr Type F64x4{Type::F64x4};
-extern OpcodeMeta META_TABLE[534];
+extern OpcodeMeta META_TABLE[538];
constexpr size_t CalculateNumArgsOf(Opcode op) noexcept {
const auto& arg_types = META_TABLE[size_t(op)].arg_types;
return size_t(std::distance(arg_types.begin(), std::ranges::find(arg_types, Type::Void)));
}
-extern u8 NUM_ARGS[534];
+extern u8 NUM_ARGS[538];
} // namespace Detail
/// Get return type of an opcode
diff --git a/src/shader_recompiler/frontend/ir/opcodes.inc b/src/shader_recompiler/frontend/ir/opcodes.inc
index fee0afaf33..552ec4fbb6 100644
--- a/src/shader_recompiler/frontend/ir/opcodes.inc
+++ b/src/shader_recompiler/frontend/ir/opcodes.inc
@@ -48,6 +48,10 @@ OPCODE(GetAttributeIndexed, F32, U32,
OPCODE(SetAttributeIndexed, Void, U32, F32, U32, )
OPCODE(GetPatch, F32, Patch, )
OPCODE(SetPatch, Void, Patch, F32, )
+OPCODE(IsbeRead, U32, U32, )
+OPCODE(IsbeWriteAttribute, Void, U32, U32, )
+OPCODE(IsbeWriteMapU8, Void, U32, U32, )
+OPCODE(IsbeWriteMapU32, Void, U32, U32, )
OPCODE(SetFragColor, Void, U32, U32, F32, )
OPCODE(SetSampleMask, Void, U32, )
OPCODE(SetFragDepth, Void, F32, )
diff --git a/src/shader_recompiler/frontend/ir/program.h b/src/shader_recompiler/frontend/ir/program.h
index 6b4a05c598..fee5cee721 100644
--- a/src/shader_recompiler/frontend/ir/program.h
+++ b/src/shader_recompiler/frontend/ir/program.h
@@ -27,6 +27,7 @@ struct Program {
u32 local_memory_size{};
u32 shared_memory_size{};
bool is_geometry_passthrough{};
+ MeshInfo mesh{};
};
[[nodiscard]] std::string DumpProgram(const Program& program);
diff --git a/src/shader_recompiler/frontend/maxwell/maxwell.inc b/src/shader_recompiler/frontend/maxwell/maxwell.inc
index e5dc9c8005..5facfb4803 100644
--- a/src/shader_recompiler/frontend/maxwell/maxwell.inc
+++ b/src/shader_recompiler/frontend/maxwell/maxwell.inc
@@ -55,6 +55,7 @@ INST(IMNMX_reg, "IMNMX (reg)", "0101 1100 0010 0---")
INST(IMUL_cbuf, "IMUL (cbuf)", "0100 1100 0011 1---")
INST(IMUL_reg, "IMUL (reg)", "0101 1100 0011 1---")
INST(ISBERD, "ISBERD", "1110 1111 1101 0---")
+INST(ISBE_nx2, "ISBE (nx2)", "1110 1111 1100 0---")
INST(ISCADD_cbuf, "ISCADD (cbuf)", "0100 1100 0001 1---")
INST(ISCADD_reg, "ISCADD (reg)", "0101 1100 0001 1---")
INST(LDC, "LDC", "1110 1111 1001 0---")
diff --git a/src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp b/src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp
index 329d44443e..50cff52f88 100644
--- a/src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp
+++ b/src/shader_recompiler/frontend/maxwell/structured_control_flow.cpp
@@ -314,7 +314,8 @@ bool NeedsLift(Node goto_stmt, Node label_stmt) noexcept {
class GotoPass {
public:
- explicit GotoPass(Flow::CFG& cfg, ObjectPool<Statement>& stmt_pool) : pool{stmt_pool} {
+ explicit GotoPass(Flow::CFG& cfg, ObjectPool<Statement>& stmt_pool, bool converge_exits_)
+ : pool{stmt_pool}, converge_exits{converge_exits_} {
std::vector gotos{BuildTree(cfg)};
const auto end{gotos.rend()};
for (auto goto_stmt = gotos.rbegin(); goto_stmt != end; ++goto_stmt) {
@@ -381,7 +382,17 @@ private:
u32 label_id{0};
std::vector<Node> gotos;
Flow::Function& first_function{cfg.Functions().front()};
- BuildTree(cfg, first_function, label_id, gotos, root_stmt.children.end(), std::nullopt);
+ Node insert_point{root_stmt.children.end()};
+ if (converge_exits) {
+ // Every EXIT jumps to one return at the end, so all invocations reach the same
+ // epilogue (task and mesh programs emit their outputs there, after a barrier).
+ Tree& root{root_stmt.children};
+ exit_label = root.insert(root.end(), *pool.Create(Label{}, label_id, &root_stmt));
+ ++label_id;
+ root.insert(root.end(), *pool.Create(Return{}, &root_stmt));
+ insert_point = *exit_label;
+ }
+ BuildTree(cfg, first_function, label_id, gotos, insert_point, std::nullopt);
return gotos;
}
@@ -454,7 +465,14 @@ private:
break;
}
case Flow::EndClass::Exit:
- root.insert(ip, *pool.Create(Return{}, &root_stmt));
+ if (exit_label) {
+ Statement* const always_cond{
+ pool.Create(Identity{}, IR::Condition{true}, &root_stmt)};
+ gotos.push_back(root.insert(
+ ip, *pool.Create(Goto{}, always_cond, *exit_label, &root_stmt)));
+ } else {
+ root.insert(ip, *pool.Create(Return{}, &root_stmt));
+ }
break;
case Flow::EndClass::Return: {
Statement* const always_cond{pool.Create(Identity{}, block.cond, &root_stmt)};
@@ -618,6 +636,8 @@ private:
ObjectPool<Statement>& pool;
Statement root_stmt{FunctionTag{}};
+ bool converge_exits{};
+ std::optional<Node> exit_label;
};
[[nodiscard]] Statement* TryFindForwardBlock(Statement& stmt) {
@@ -987,7 +1007,8 @@ IR::AbstractSyntaxList BuildASL(ObjectPool<IR::Inst>& inst_pool, ObjectPool<IR::
Environment& env, Flow::CFG& cfg,
const HostTranslateInfo& host_info) {
ObjectPool<Statement> stmt_pool{64};
- GotoPass goto_pass{cfg, stmt_pool};
+ const Stage stage{env.ShaderStage()};
+ GotoPass goto_pass{cfg, stmt_pool, stage == Stage::Task || stage == Stage::Mesh};
Statement& root{goto_pass.RootStatement()};
IR::AbstractSyntaxList syntax_list;
TranslatePass{inst_pool, block_pool, stmt_pool, env, root, syntax_list, host_info};
diff --git a/src/shader_recompiler/frontend/maxwell/translate/impl/internal_stage_buffer_entry_read.cpp b/src/shader_recompiler/frontend/maxwell/translate/impl/internal_stage_buffer_entry_read.cpp
index 46660563a1..27de2a0171 100644
--- a/src/shader_recompiler/frontend/maxwell/translate/impl/internal_stage_buffer_entry_read.cpp
+++ b/src/shader_recompiler/frontend/maxwell/translate/impl/internal_stage_buffer_entry_read.cpp
@@ -125,4 +125,40 @@ void TranslatorVisitor::ISBERD(u64 insn) {
X(isberd.dest_reg.Value(), X(isberd.src_reg.Value()));
}
+// nx2 pseudo instruction for the ISBERD/ISBEWR of Switch 2 task and mesh programs (see
+// frontend/ampere/lower_to_maxwell.h). Addresses are linear bytes in the stage buffer: reads come
+// from the input attribute region, writes go to the output attribute or map region.
+void TranslatorVisitor::ISBE_nx2(u64 insn) {
+ union {
+ u64 raw;
+ BitField<0, 8, IR::Reg> reg;
+ BitField<8, 8, IR::Reg> address_reg;
+ BitField<20, 24, u64> offset;
+ BitField<44, 1, u64> is_32;
+ BitField<45, 1, u64> attribute;
+ BitField<48, 1, u64> write;
+ } const isbe{insn};
+
+ const Stage stage{env.ShaderStage()};
+ if (stage != Stage::Task && stage != Stage::Mesh) {
+ throw NotImplementedException("Stage buffer access outside task and mesh programs");
+ }
+ const IR::U32 address{ir.IAdd(X(isbe.address_reg), ir.Imm32(static_cast<u32>(isbe.offset)))};
+ if (isbe.write == 0) {
+ X(isbe.reg, ir.IsbeRead(address));
+ return;
+ }
+ const IR::U32 value{X(isbe.reg)};
+ if (isbe.attribute != 0) {
+ if (isbe.is_32 == 0) {
+ throw NotImplementedException("U8 stage buffer attribute write");
+ }
+ ir.IsbeWriteAttribute(address, value);
+ } else if (isbe.is_32 != 0) {
+ ir.IsbeWriteMapU32(address, value);
+ } else {
+ ir.IsbeWriteMapU8(address, value);
+ }
+}
+
} // namespace Shader::Maxwell
diff --git a/src/shader_recompiler/frontend/maxwell/translate_program.cpp b/src/shader_recompiler/frontend/maxwell/translate_program.cpp
index cd4678cef6..fe6d8d8913 100644
--- a/src/shader_recompiler/frontend/maxwell/translate_program.cpp
+++ b/src/shader_recompiler/frontend/maxwell/translate_program.cpp
@@ -101,10 +101,12 @@ void AddNVNStorageBuffers(IR::Program& program) {
switch (program.stage) {
case Stage::VertexA:
case Stage::VertexB:
+ case Stage::Task: // bound in the vertex slot
return 0x110u;
case Stage::TessellationControl:
return 0x210u;
case Stage::TessellationEval:
+ case Stage::Mesh: // bound in the tessellation slot
return 0x310u;
case Stage::Geometry:
return 0x410u;
@@ -304,6 +306,30 @@ void LowerGeometryPassthrough(const IR::Program& program, const HostTranslateInf
}
}
+// The lines of a 32-element group in a mesh program's stage buffer: one per attribute an output
+// map enables, in attribute order (system values, then generics).
+void AddMeshLines(const ProgramHeader& sph, boost::container::static_vector<u32, 136>& lines) {
+ const auto& c{sph.vtg.omap_systemc};
+ if (c.clip_distances != 0 || c.point_sprite_s != 0 || c.point_sprite_t != 0 ||
+ c.fog_coordinate != 0 || c.tessellation_eval_point_u != 0 ||
+ c.tessellation_eval_point_v != 0 || c.instance_id != 0 || c.vertex_id != 0) {
+ throw NotImplementedException("Mesh outputs past the generic attributes");
+ }
+ for (u32 bit = 0; bit < 8; ++bit) {
+ if (((sph.vtg.omap_systemb.raw >> bit) & 1) != 0) {
+ lines.push_back(static_cast<u32>(IR::Attribute::PrimitiveId) + bit);
+ }
+ }
+ for (u32 generic = 0; generic < 32; ++generic) {
+ const std::array<bool, 4> mask{sph.vtg.OutputGeneric(generic)};
+ for (u32 element = 0; element < 4; ++element) {
+ if (mask[element]) {
+ lines.push_back(static_cast<u32>(IR::Attribute::Generic0X) + generic * 4 + element);
+ }
+ }
+ }
+}
+
void TightenOutputVertices(IR::Program& program) {
if (program.stage != Stage::Geometry || program.is_geometry_passthrough) {
return;
@@ -368,6 +394,30 @@ IR::Program TranslateProgram(ObjectPool<IR::Inst>& inst_pool, ObjectPool<IR::Blo
program.workgroup_size = env.WorkgroupSize();
program.shared_memory_size = env.SharedMemorySize();
break;
+ case Stage::Task:
+ case Stage::Mesh: {
+ // SET_MESH_SHADER_A: topology 3:0, max vertex 12:4, max primitive 22:13;
+ // SET_MESH_SHADER_B: mesh threads 25:20; SET_MESH_INIT_SHADER: task threads 5:0, local
+ // buffer lines 20:12, lines passed to the mesh stage 30:22 (lines are 128 bytes).
+ const auto& registers{env.MeshRegisters()};
+ const u32 shader_a{registers[0]}, shader_b{registers[1]}, init{registers[2]};
+ const u32 lines{(std::max)((init >> 12) & 0x1ff, (init >> 22) & 0x1ff)};
+ program.mesh.payload_words = (std::max)(lines, 1U) * 32;
+ program.mesh.topology = shader_a & 0xf;
+ program.mesh.max_vertices = (shader_a >> 4) & 0x1ff;
+ program.mesh.max_primitives = (shader_a >> 13) & 0x3ff;
+ const u32 threads{program.stage == Stage::Task ? init & 0x3f : (shader_b >> 20) & 0x3f};
+ program.workgroup_size = {(std::max)(threads, 1U), 1, 1};
+ if (program.stage == Stage::Mesh) {
+ if (program.mesh.topology != 4) {
+ throw NotImplementedException("Mesh output topology {} (SET_MESH_SHADER_A {:#010x})",
+ program.mesh.topology, shader_a);
+ }
+ AddMeshLines(env.SPH(), program.mesh.vertex_lines);
+ AddMeshLines(env.MeshPrimitiveHeader(), program.mesh.primitive_lines);
+ }
+ break;
+ }
default:
break;
}
@@ -406,6 +456,18 @@ IR::Program TranslateProgram(ObjectPool<IR::Inst>& inst_pool, ObjectPool<IR::Blo
Optimization::VerificationPass(program);
}
Optimization::CollectShaderInfoPass(env, program);
+ program.info.is_task = program.stage == Stage::Task;
+ program.info.is_mesh = program.stage == Stage::Mesh;
+ if (program.stage == Stage::Mesh) {
+ // Its outputs come from the stage buffer lines, not attribute stores; the next stage
+ // reads what they name.
+ for (const u32 line : program.mesh.vertex_lines) {
+ program.info.stores.Set(static_cast<IR::Attribute>(line), true);
+ }
+ for (const u32 line : program.mesh.primitive_lines) {
+ program.info.stores.Set(static_cast<IR::Attribute>(line), true);
+ }
+ }
Optimization::LayerPass(program, normalized_host_info);
Optimization::VendorWorkaroundPass(program);
diff --git a/src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp b/src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp
index 9010c2bf68..c212a25d37 100644
--- a/src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp
+++ b/src/shader_recompiler/ir_opt/collect_shader_info_pass.cpp
@@ -963,10 +963,12 @@ void CollectShaderInfoPass(Environment& env, IR::Program& program) {
switch (program.stage) {
case Stage::VertexA:
case Stage::VertexB:
+ case Stage::Task: // bound in the vertex slot
return 0x110u;
case Stage::TessellationControl:
return 0x210u;
case Stage::TessellationEval:
+ case Stage::Mesh: // bound in the tessellation slot
return 0x310u;
case Stage::Geometry:
return 0x410u;
@@ -978,6 +980,11 @@ void CollectShaderInfoPass(Environment& env, IR::Program& program) {
throw InvalidArgument("Invalid stage {}", program.stage);
}()};
info.nvn_buffer_base = base;
+ if (program.stage == Stage::Task || program.stage == Stage::Mesh) {
+ // The stage buffer reads the workgroup index; the epilogue splits work by invocation.
+ info.uses_workgroup_id = true;
+ info.uses_local_invocation_id = true;
+ }
for (IR::Block* const block : program.post_order_blocks) {
for (IR::Inst& inst : block->Instructions()) {
diff --git a/src/shader_recompiler/ir_opt/position_pass.cpp b/src/shader_recompiler/ir_opt/position_pass.cpp
index 4890346944..600a596bf3 100644
--- a/src/shader_recompiler/ir_opt/position_pass.cpp
+++ b/src/shader_recompiler/ir_opt/position_pass.cpp
@@ -83,6 +83,37 @@ void BlitConstantsPass(Environment& env, IR::Program& program) {
if (env.ShaderStage() != Stage::VertexB && env.ShaderStage() != Stage::Fragment) {
return;
}
+ // Bank 24 is root table 0, which games may load themselves (3D methods 0x504/0x508; the buffer
+ // cache binds it as bank 17): Fire Emblem: Fortune's Weave keeps its material push constants
+ // there. A shader reading bank 17 outside the blit block reads such a table and keeps its reads,
+ // as does one reading only words the blit block leaves zero (an unbound bank reads zero anyway;
+ // the game's enemy health gauges read only 0x40 of their table).
+ const auto in_blit_block = [](u32 offset) {
+ return offset < 0x20 || (offset >= 0x30 && offset < 0x4c) || (offset >= 0x50 && offset < 0x60);
+ };
+ const auto is_nonzero_word = [](u32 offset) {
+ return offset < 0x20 || offset == 0x38 || offset == 0x3c || offset == 0x58 || offset == 0x5c;
+ };
+ bool reads_nonzero_word{};
+ for (IR::Block* const block : program.post_order_blocks) {
+ for (IR::Inst& inst : block->Instructions()) {
+ const IR::Opcode opcode{inst.GetOpcode()};
+ if ((opcode != IR::Opcode::GetCbufU32 && opcode != IR::Opcode::GetCbufF32 &&
+ opcode != IR::Opcode::GetCbufU32x2) ||
+ !inst.Arg(0).IsImmediate() || inst.Arg(0).U32() != 17 || !inst.Arg(1).IsImmediate()) {
+ continue;
+ }
+ const u32 offset{inst.Arg(1).U32()};
+ const bool pair{opcode == IR::Opcode::GetCbufU32x2};
+ if (!in_blit_block(offset) || (pair && !in_blit_block(offset + 4))) {
+ return;
+ }
+ reads_nonzero_word |= is_nonzero_word(offset) || (pair && is_nonzero_word(offset + 4));
+ }
+ }
+ if (!reads_nonzero_word) {
+ return;
+ }
bool used{};
for (IR::Block* const block : program.post_order_blocks) {
for (IR::Inst& inst : block->Instructions()) {
diff --git a/src/shader_recompiler/ir_opt/texture_pass.cpp b/src/shader_recompiler/ir_opt/texture_pass.cpp
index fae69ab91b..b6aca0a118 100644
--- a/src/shader_recompiler/ir_opt/texture_pass.cpp
+++ b/src/shader_recompiler/ir_opt/texture_pass.cpp
@@ -5,6 +5,10 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
+#include <queue>
+#include <unordered_map>
+#include <unordered_set>
+#include <vector>
#include <bit>
#include <optional>
#include "common/container/unordered_map.h"
@@ -12,6 +16,7 @@
#include <limits>
#include <boost/container/small_vector.hpp>
+#include "common/scope_exit.h"
#include "common/settings.h"
#include "shader_recompiler/environment.h"
#include "shader_recompiler/frontend/ir/basic_block.h"
@@ -340,15 +345,21 @@ static inline u32 GetTextureHandleCached(Environment& env, const ConstBufferAddr
}
// Cached variants of existing helpers
+// A handle computed per draw has no descriptor to read at translation time: assume an ordinary
+// floating-point 2D texture, the instruction's own type deciding the rest (see the TexturePass
+// ImageQueryDimensions case, which has none).
static inline TextureType ReadTextureTypeCached(Environment& env, const ConstBufferAddr& cbuf) {
+ if (cbuf.handle_program != ~0U) return TextureType::Color2D;
return env.ReadTextureType(GetTextureHandleCached(env, cbuf));
}
static inline TexturePixelFormat ReadTexturePixelFormatCached(Environment& env,
const ConstBufferAddr& cbuf) {
+ if (cbuf.handle_program != ~0U) return TexturePixelFormat::A8B8G8R8_UNORM;
return env.ReadTexturePixelFormat(GetTextureHandleCached(env, cbuf));
}
static inline bool IsTexturePixelFormatIntegerCached(Environment& env,
const ConstBufferAddr& cbuf) {
+ if (cbuf.handle_program != ~0U) return false;
return env.IsTexturePixelFormatInteger(GetTextureHandleCached(env, cbuf));
}
@@ -366,12 +377,400 @@ static inline std::optional<ConstBufferAddr> TrackCached(const IR::Value& v, Env
std::optional<ConstBufferAddr> TryGetConstBuffer(const IR::Inst* inst, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block);
+bool IsMemoryLoad(IR::Opcode opcode) {
+ switch (opcode) {
+ case IR::Opcode::LoadGlobalU8: case IR::Opcode::LoadGlobalS8: case IR::Opcode::LoadGlobalU16:
+ case IR::Opcode::LoadGlobalS16: case IR::Opcode::LoadGlobal32: case IR::Opcode::LoadGlobal64:
+ case IR::Opcode::LoadGlobal128: case IR::Opcode::LoadStorageU8: case IR::Opcode::LoadStorageS8:
+ case IR::Opcode::LoadStorageU16: case IR::Opcode::LoadStorageS16: case IR::Opcode::LoadStorage32:
+ case IR::Opcode::LoadStorage64: case IR::Opcode::LoadStorage128: case IR::Opcode::LoadLocal:
+ case IR::Opcode::LoadSharedU8: case IR::Opcode::LoadSharedS8: case IR::Opcode::LoadSharedU16:
+ case IR::Opcode::LoadSharedS16: case IR::Opcode::LoadSharedU32: case IR::Opcode::LoadSharedU64:
+ case IR::Opcode::LoadSharedU128:
+ return true;
+ default:
+ return false;
+ }
+}
+
std::optional<ConstBufferAddr> Track(const IR::Value& value, Environment& env, const HostTranslateInfo& host_info, const IR::Block* block) {
- return IR::BreadthFirstSearch(value, [&env, &host_info, block](const IR::Inst* inst) {
- return TryGetConstBuffer(inst, env, host_info, block);
- }, [&](IR::Value operand) { return ResolveConditionalPhi(operand, block); });
+ // A value loaded from memory is not a constant buffer word, whatever its address is built
+ // from: the search does not look into a load's operands (a handle read from a storage
+ // buffer would otherwise be taken from the buffer's address in the constant buffer).
+ const IR::Value start{ResolveConditionalPhi(value, block)};
+ if (start.IsImmediate()) {
+ return std::nullopt;
+ }
+ std::unordered_set<const IR::Inst*> visited;
+ std::queue<const IR::Inst*> queue;
+ queue.push(start.InstRecursive());
+ visited.insert(start.InstRecursive());
+ while (!queue.empty()) {
+ const IR::Inst* const inst{queue.front()};
+ queue.pop();
+ if (const std::optional result = TryGetConstBuffer(inst, env, host_info, block)) {
+ return result;
+ }
+ if (IsMemoryLoad(inst->GetOpcode())) {
+ continue;
+ }
+ for (size_t arg = inst->NumArgs(); arg--;) {
+ const IR::Value arg_value{ResolveConditionalPhi(inst->Arg(arg), block)};
+ if (arg_value.IsImmediate()) {
+ continue;
+ }
+ const IR::Inst* const arg_inst{arg_value.InstRecursive()};
+ if (visited.insert(arg_inst).second) {
+ queue.push(arg_inst);
+ }
+ }
+ }
+ return std::nullopt;
+}
+
+// Builds a handle program (shader_info.h) for a value computed from constant buffer and storage
+// buffer words; false when the value depends on anything else.
+thread_local IR::Opcode g_handle_failure{IR::Opcode::Void}; // the first operation not supported
+
+bool IsUndefined(IR::Opcode opcode) {
+ return opcode == IR::Opcode::UndefU1 || opcode == IR::Opcode::UndefU8 ||
+ opcode == IR::Opcode::UndefU16 || opcode == IR::Opcode::UndefU32 ||
+ opcode == IR::Opcode::UndefU64;
+}
+
+// Values already emitted by the program being built, by the slot that keeps them, how many slots
+// are in use, and how deep in nested BuildHandleProgram calls the build is.
+thread_local std::unordered_map<const IR::Inst*, u32> g_handle_slots;
+thread_local u32 g_handle_slot_count{};
+thread_local int g_handle_depth{};
+
+bool BuildHandleNode(const IR::Value& value, std::vector<u32>& code, int& budget,
+ const IR::Block* block);
+
+bool BuildHandleProgram(const IR::Value& value, std::vector<u32>& code, int& budget,
+ const IR::Block* block) {
+ using Op = HandleOp;
+ if (value.IsImmediate()) {
+ return BuildHandleNode(value, code, budget, block);
+ }
+ const bool outermost = g_handle_depth++ == 0;
+ if (outermost) {
+ g_handle_slots.clear();
+ g_handle_slot_count = 0;
+ }
+ SCOPE_EXIT {
+ --g_handle_depth;
+ if (outermost) {
+ g_handle_slots.clear();
+ g_handle_slot_count = 0;
+ }
+ };
+ const IR::Inst* const inst{value.InstRecursive()};
+ bool result;
+ if (const auto it = g_handle_slots.find(inst); it != g_handle_slots.end()) {
+ if (it->second == ~0U) {
+ // Reached again while building it: a loop carries the value, which no single
+ // evaluation can give.
+ g_handle_failure = IR::Opcode::Phi;
+ result = false;
+ } else {
+ code.push_back(static_cast<u32>(Op::Load));
+ code.push_back(it->second);
+ result = true;
+ }
+ } else {
+ g_handle_slots.emplace(inst, ~0U);
+ const size_t start = code.size();
+ const u32 first_slot = g_handle_slot_count;
+ result = BuildHandleNode(value, code, budget, block);
+ g_handle_slots.erase(inst);
+ if (!result) {
+ // The Tee of every value the dropped code kept goes with it: a later use of one of
+ // them (another Phi input) must build it again, not load a slot nothing wrote.
+ code.resize(start);
+ std::erase_if(g_handle_slots, [first_slot](const auto& entry) {
+ return entry.second != ~0U && entry.second >= first_slot;
+ });
+ g_handle_slot_count = first_slot;
+ }
+ if (result && inst->UseCount() > 1 && g_handle_slots.size() < 256) {
+ const u32 slot = g_handle_slot_count++;
+ g_handle_slots.emplace(inst, slot);
+ code.push_back(static_cast<u32>(Op::Tee));
+ code.push_back(slot);
+ }
+ }
+ return result;
}
+bool BuildHandleNode(const IR::Value& value, std::vector<u32>& code, int& budget,
+ const IR::Block* block) {
+ if (--budget < 0) {
+ return false;
+ }
+ using Op = HandleOp;
+ const auto emit = [&](Op op) { code.push_back(static_cast<u32>(op)); };
+ if (value.IsImmediate()) {
+ emit(Op::Immediate);
+ code.push_back(value.Type() == IR::Type::U1 ? (value.U1() ? 1U : 0U) : value.U32());
+ return true;
+ }
+ const IR::Inst* const inst{value.InstRecursive()};
+ const auto build = [&](size_t arg) { return BuildHandleProgram(inst->Arg(arg), code, budget, block); };
+ const auto binary = [&](Op op) {
+ if (!build(0) || !build(1)) return false;
+ emit(op);
+ return true;
+ };
+ const auto unary = [&](Op op) {
+ if (!build(0)) return false;
+ emit(op);
+ return true;
+ };
+ // A word of a constant buffer read or of a storage buffer load, at byte offset +extra.
+ const auto load_word = [&](const IR::Inst* source, u32 extra) {
+ const auto add_extra = [&] {
+ if (extra != 0) {
+ emit(Op::Immediate);
+ code.push_back(extra);
+ emit(Op::Add);
+ }
+ };
+ switch (source->GetOpcode()) {
+ case IR::Opcode::GetCbufU32:
+ case IR::Opcode::GetCbufU32x2: {
+ const IR::Value index{source->Arg(0)};
+ const IR::Value offset{source->Arg(1)};
+ if (!index.IsImmediate()) return false;
+ if (offset.IsImmediate()) {
+ emit(Op::Cbuf);
+ code.push_back(index.U32());
+ code.push_back(offset.U32() + extra);
+ return true;
+ }
+ if (!BuildHandleProgram(offset, code, budget, block)) return false;
+ add_extra();
+ emit(Op::CbufDynamic);
+ code.push_back(index.U32());
+ return true;
+ }
+ case IR::Opcode::LoadStorage32:
+ case IR::Opcode::LoadStorage64:
+ case IR::Opcode::LoadStorage128: {
+ const IR::Value binding{source->Arg(0)};
+ if (!binding.IsImmediate()) return false;
+ if (!BuildHandleProgram(source->Arg(1), code, budget, block)) return false;
+ add_extra();
+ emit(Op::Storage);
+ code.push_back(binding.U32());
+ return true;
+ }
+ default:
+ return false;
+ }
+ };
+ switch (inst->GetOpcode()) {
+ case IR::Opcode::Identity:
+ case IR::Opcode::ConditionRef:
+ case IR::Opcode::ShuffleIndex: // a value made uniform (R2UR): the same in every lane
+ case IR::Opcode::VoteAny: // of a condition on per-draw words only, the same in every lane
+ case IR::Opcode::VoteAll:
+ return build(0);
+ case IR::Opcode::GetCbufU32:
+ case IR::Opcode::LoadStorage32:
+ return load_word(inst, 0);
+ case IR::Opcode::CompositeExtractU32x2: {
+ const IR::Value element{inst->Arg(1)};
+ if (!element.IsImmediate() || inst->Arg(0).IsImmediate()) return false;
+ return load_word(inst->Arg(0).InstRecursive(), element.U32() * 4);
+ }
+ case IR::Opcode::Phi: {
+ // Keep the incoming value that is defined; of two, the one made uniform (the
+ // register-to-uniform move a conditional block makes for the handle).
+ const IR::Value resolved{ResolveConditionalPhi(value, block)};
+ if (resolved.IsImmediate() || resolved.InstRecursive() != inst) {
+ return BuildHandleProgram(resolved, code, budget, block);
+ }
+ std::optional<size_t> chosen;
+ for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
+ const IR::Value incoming{inst->Arg(arg)};
+ if (!incoming.IsImmediate() && IsUndefined(incoming.InstRecursive()->GetOpcode())) {
+ continue;
+ }
+ IR::Value probe{incoming};
+ bool shuffled = false;
+ for (int depth = 0; depth < 8 && !probe.IsImmediate(); ++depth) {
+ const IR::Inst* const probe_inst{probe.InstRecursive()};
+ if (probe_inst->GetOpcode() == IR::Opcode::ShuffleIndex) {
+ shuffled = true;
+ break;
+ }
+ if (probe_inst->GetOpcode() != IR::Opcode::Identity) break;
+ probe = probe_inst->Arg(0);
+ }
+ if (!chosen || shuffled) chosen = arg;
+ }
+ if (chosen && build(*chosen)) {
+ return true;
+ }
+ for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
+ const IR::Value incoming{inst->Arg(arg)};
+ if (arg == chosen || (!incoming.IsImmediate() && IsUndefined(incoming.InstRecursive()->GetOpcode()))) {
+ continue;
+ }
+ if (build(arg)) {
+ return true;
+ }
+ }
+ return false;
+ }
+ case IR::Opcode::IAdd32: return binary(Op::Add);
+ case IR::Opcode::ISub32: return binary(Op::Sub);
+ case IR::Opcode::IMul32: return binary(Op::Mul);
+ case IR::Opcode::BitwiseAnd32: return binary(Op::And);
+ case IR::Opcode::BitwiseOr32: return binary(Op::Or);
+ case IR::Opcode::BitwiseXor32: return binary(Op::Xor);
+ case IR::Opcode::ShiftLeftLogical32: return binary(Op::Shl);
+ case IR::Opcode::ShiftRightLogical32: return binary(Op::Shr);
+ case IR::Opcode::ShiftRightArithmetic32: return binary(Op::Sar);
+ case IR::Opcode::INeg32: return unary(Op::Neg);
+ case IR::Opcode::BitwiseNot32: return unary(Op::Not);
+ case IR::Opcode::UMin32: return binary(Op::UMin);
+ case IR::Opcode::UMax32: return binary(Op::UMax);
+ case IR::Opcode::SMin32: return binary(Op::SMin);
+ case IR::Opcode::SMax32: return binary(Op::SMax);
+ case IR::Opcode::SLessThan: return binary(Op::SLt);
+ case IR::Opcode::SLessThanEqual: return binary(Op::SLe);
+ case IR::Opcode::SGreaterThan: return binary(Op::SGt);
+ case IR::Opcode::SGreaterThanEqual: return binary(Op::SGe);
+ case IR::Opcode::ULessThan: return binary(Op::ULt);
+ case IR::Opcode::ULessThanEqual: return binary(Op::ULe);
+ case IR::Opcode::UGreaterThan: return binary(Op::UGt);
+ case IR::Opcode::UGreaterThanEqual: return binary(Op::UGe);
+ case IR::Opcode::IEqual: return binary(Op::Eq);
+ case IR::Opcode::INotEqual: return binary(Op::Ne);
+ case IR::Opcode::LogicalAnd: return binary(Op::LAnd);
+ case IR::Opcode::LogicalOr: return binary(Op::LOr);
+ case IR::Opcode::LogicalXor: return binary(Op::LXor);
+ case IR::Opcode::LogicalNot: return unary(Op::LNot);
+ case IR::Opcode::GetZeroFromOp: // the flag a producing operation sets on its result
+ if (!build(0)) return false;
+ emit(Op::Immediate);
+ code.push_back(0);
+ emit(Op::Eq);
+ return true;
+ case IR::Opcode::GetSignFromOp:
+ if (!build(0)) return false;
+ emit(Op::Immediate);
+ code.push_back(0);
+ emit(Op::SLt);
+ return true;
+ case IR::Opcode::GetCarryFromOp: {
+ // The carry of a 32-bit addition: the sum below an operand.
+ const IR::Value op{inst->Arg(0)};
+ if (op.IsImmediate() || op.InstRecursive()->GetOpcode() != IR::Opcode::IAdd32) {
+ g_handle_failure = inst->GetOpcode();
+ return false;
+ }
+ const IR::Inst* const add{op.InstRecursive()};
+ if (!BuildHandleProgram(op, code, budget, block) ||
+ !BuildHandleProgram(add->Arg(0), code, budget, block)) {
+ return false;
+ }
+ emit(Op::ULt);
+ return true;
+ }
+ case IR::Opcode::BitCastU32F32:
+ case IR::Opcode::BitCastF32U32:
+ return build(0);
+ case IR::Opcode::UndefU1:
+ case IR::Opcode::UndefU32:
+ // Read before any write on some path: no value to compute, take zero.
+ emit(Op::Immediate);
+ code.push_back(0);
+ return true;
+ case IR::Opcode::BitFieldUExtract: {
+ // (base >> offset) & ((1 << count) - 1), the shift by 32 giving the all-ones mask.
+ const auto mask = [&] {
+ emit(Op::Immediate);
+ code.push_back(1);
+ if (!build(2)) return false;
+ emit(Op::Shl);
+ emit(Op::Immediate);
+ code.push_back(1);
+ emit(Op::Sub);
+ return true;
+ };
+ if (!build(0) || !build(1)) return false;
+ emit(Op::Shr);
+ if (!mask()) return false;
+ emit(Op::And);
+ return true;
+ }
+ case IR::Opcode::BitFieldSExtract: {
+ // (base << (32 - offset - count)) >> (32 - count), arithmetic.
+ if (!build(0)) return false;
+ emit(Op::Immediate);
+ code.push_back(32);
+ if (!build(1)) return false;
+ emit(Op::Sub);
+ if (!build(2)) return false;
+ emit(Op::Sub);
+ emit(Op::Shl);
+ emit(Op::Immediate);
+ code.push_back(32);
+ if (!build(2)) return false;
+ emit(Op::Sub);
+ emit(Op::Sar);
+ return true;
+ }
+ case IR::Opcode::BitFieldInsert: {
+ // (base & ~mask) | ((insert << offset) & mask), mask = ((1 << count) - 1) << offset.
+ const auto mask = [&] {
+ emit(Op::Immediate);
+ code.push_back(1);
+ if (!build(3)) return false;
+ emit(Op::Shl);
+ emit(Op::Immediate);
+ code.push_back(1);
+ emit(Op::Sub);
+ if (!build(2)) return false;
+ emit(Op::Shl);
+ return true;
+ };
+ if (!build(0) || !mask()) return false;
+ emit(Op::Not);
+ emit(Op::And);
+ if (!build(1) || !build(2)) return false;
+ emit(Op::Shl);
+ if (!mask()) return false;
+ emit(Op::And);
+ emit(Op::Or);
+ return true;
+ }
+ case IR::Opcode::UClamp32:
+ case IR::Opcode::SClamp32: {
+ const bool is_signed = inst->GetOpcode() == IR::Opcode::SClamp32;
+ if (!build(0) || !build(1)) return false;
+ emit(is_signed ? Op::SMax : Op::UMax);
+ if (!build(2)) return false;
+ emit(is_signed ? Op::SMin : Op::UMin);
+ return true;
+ }
+ case IR::Opcode::SelectU1:
+ case IR::Opcode::SelectU32:
+ if (!build(0) || !build(1) || !build(2)) return false;
+ emit(Op::Select);
+ return true;
+ default:
+ g_handle_failure = inst->GetOpcode();
+ return false;
+ }
+}
+
+// Programs built while making the texture instructions of the pass running on this thread.
+thread_local std::vector<std::vector<u32>>* g_handle_programs{};
+
std::optional<u32> TryGetConstant(IR::Value& value, Environment& env) {
const IR::Inst* inst = value.InstRecursive();
if (inst->GetOpcode() != IR::Opcode::GetCbufU32) {
@@ -525,10 +924,44 @@ TextureInst MakeInst(Environment& env, IR::Block* block, IR::Inst& inst, const H
if (IsBindless(inst)) {
const std::optional<ConstBufferAddr> track_addr{TrackCached(inst.Arg(0), env, host_info, block)};
- if (!track_addr) {
- throw NotImplementedException("Failed to track bindless texture constant buffer");
- } else {
+ if (track_addr) {
addr = *track_addr;
+ } else {
+ // Read from a storage buffer (a handle table): computed for each draw.
+ std::vector<u32> code;
+ int budget = 4096;
+ g_handle_failure = IR::Opcode::Void;
+ if (!g_handle_programs || !BuildHandleProgram(inst.Arg(0), code, budget, block)) {
+ // Not computable for a whole draw (a handle that changes in a loop): keep the
+ // earlier search through loads, which at least gives the shader a texture.
+ const std::optional<ConstBufferAddr> legacy{IR::BreadthFirstSearch(inst.Arg(0),
+ [&env, &host_info, block](const IR::Inst* candidate) {
+ return TryGetConstBuffer(candidate, env, host_info, block);
+ }, [&](IR::Value operand) { return ResolveConditionalPhi(operand, block); })};
+ if (!legacy) {
+ throw NotImplementedException("Failed to track bindless texture constant buffer (stopped at {}, budget {})",
+ g_handle_failure, budget);
+ }
+ return TextureInst{.cbuf = *legacy, .inst = &inst, .block = block};
+ }
+ auto& programs{*g_handle_programs};
+ auto it{std::ranges::find(programs, code)};
+ if (it == programs.end()) {
+ programs.push_back(std::move(code));
+ it = programs.end() - 1;
+ }
+ addr = ConstBufferAddr{
+ .index = 0,
+ .offset = 0,
+ .shift_left = 0,
+ .secondary_index = 0,
+ .secondary_offset = 0,
+ .secondary_shift_left = 0,
+ .dynamic_offset = {},
+ .count = 1,
+ .has_secondary = false,
+ .handle_program = static_cast<u32>(it - programs.begin()),
+ };
}
} else {
addr = ConstBufferAddr{
@@ -590,7 +1023,8 @@ public:
desc.secondary_cbuf_offset == existing.secondary_cbuf_offset &&
desc.secondary_shift_left == existing.secondary_shift_left &&
desc.count == existing.count && desc.size_shift == existing.size_shift &&
- desc.has_secondary == existing.has_secondary;
+ desc.has_secondary == existing.has_secondary &&
+ desc.handle_program == existing.handle_program;
});
}
@@ -598,7 +1032,8 @@ public:
const u32 index{Add(image_buffer_descriptors, desc, [&desc](const auto& existing) {
return desc.format == existing.format && desc.cbuf_index == existing.cbuf_index &&
desc.cbuf_offset == existing.cbuf_offset && desc.count == existing.count &&
- desc.size_shift == existing.size_shift;
+ desc.size_shift == existing.size_shift &&
+ desc.handle_program == existing.handle_program;
})};
image_buffer_descriptors[index].is_written |= desc.is_written;
image_buffer_descriptors[index].is_read |= desc.is_read;
@@ -616,7 +1051,8 @@ public:
desc.secondary_cbuf_index == existing.secondary_cbuf_index &&
desc.secondary_cbuf_offset == existing.secondary_cbuf_offset &&
desc.secondary_shift_left == existing.secondary_shift_left &&
- desc.count == existing.count && desc.size_shift == existing.size_shift;
+ desc.count == existing.count && desc.size_shift == existing.size_shift &&
+ desc.handle_program == existing.handle_program;
})};
// TODO: Read this from TIC
texture_descriptors[index].is_multisample |= desc.is_multisample;
@@ -629,7 +1065,8 @@ public:
return desc.type == existing.type && desc.format == existing.format &&
desc.cbuf_index == existing.cbuf_index &&
desc.cbuf_offset == existing.cbuf_offset && desc.count == existing.count &&
- desc.size_shift == existing.size_shift;
+ desc.size_shift == existing.size_shift &&
+ desc.handle_program == existing.handle_program;
})};
image_descriptors[index].is_written |= desc.is_written;
image_descriptors[index].is_read |= desc.is_read;
@@ -724,6 +1161,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
env.handle_cache.clear();
env.track_cache.clear();
TextureInstVector to_replace;
+ g_handle_programs = &program.info.handle_programs;
for (IR::Block* const block : program.post_order_blocks) {
env.track_cache.clear();
for (IR::Inst& inst : block->Instructions()) {
@@ -750,6 +1188,10 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
u32 last_cbuf_offset{};
bool last_is_integer{false};
const auto is_texture_pixel_format_integer{[&](const ConstBufferAddr& cbuf_addr) {
+ if (cbuf_addr.handle_program != ~0U) {
+ // Every handle program is at cbuf 0 offset 0: the cache below can't tell them apart.
+ return IsTexturePixelFormatIntegerCached(env, cbuf_addr);
+ }
if (has_last_is_integer && last_cbuf_index == cbuf_addr.index &&
last_cbuf_offset == cbuf_addr.offset) {
return last_is_integer;
@@ -760,6 +1202,18 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
has_last_is_integer = true;
return last_is_integer;
}};
+ // A handle program has no descriptor to read a type from, and ImageQueryDimensions carries
+ // none: it takes the type of another instruction using the same handle.
+ std::unordered_map<u32, TextureType> handle_program_types;
+ for (const TextureInst& texture_inst : to_replace) {
+ const IR::Opcode opcode{IndexedInstruction(*texture_inst.inst)};
+ if (texture_inst.cbuf.handle_program != ~0U && opcode != IR::Opcode::ImageQueryDimensions &&
+ !IsStorageImageOpcode(opcode)) {
+ handle_program_types.try_emplace(
+ texture_inst.cbuf.handle_program,
+ texture_inst.inst->Flags<IR::TextureInstInfo>().type.Value());
+ }
+ }
for (TextureInst& texture_inst : to_replace) {
// TODO: Handle arrays
IR::Inst* const inst{texture_inst.inst};
@@ -770,7 +1224,12 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
bool is_multisample{false};
switch (inst->GetOpcode()) {
case IR::Opcode::ImageQueryDimensions:
- flags.type.Assign(ReadTextureTypeCached(env, cbuf));
+ if (const auto it{handle_program_types.find(cbuf.handle_program)};
+ it != handle_program_types.end()) {
+ flags.type.Assign(it->second);
+ } else {
+ flags.type.Assign(ReadTextureTypeCached(env, cbuf));
+ }
inst->SetFlags(flags);
break;
case IR::Opcode::ImageSampleImplicitLod:
@@ -835,6 +1294,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.cbuf_offset = cbuf.offset,
.count = count,
.size_shift = size_shift,
+ .handle_program = cbuf.handle_program,
});
} else {
index = descriptors.Add(ImageDescriptor{
@@ -847,6 +1307,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.cbuf_offset = cbuf.offset,
.count = count,
.size_shift = size_shift,
+ .handle_program = cbuf.handle_program,
});
}
break;
@@ -863,6 +1324,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.secondary_shift_left = cbuf.secondary_shift_left,
.count = count,
.size_shift = size_shift,
+ .handle_program = cbuf.handle_program,
});
} else {
count = std::min(count, sampled_dynamic_cap);
@@ -881,6 +1343,7 @@ void TexturePass(Environment& env, IR::Program& program, const HostTranslateInfo
.secondary_shift_left = cbuf.secondary_shift_left,
.count = count,
.size_shift = size_shift,
+ .handle_program = cbuf.handle_program,
});
flags.is_integer.Assign(is_integer ? 1 : 0);
}
diff --git a/src/shader_recompiler/runtime_info.h b/src/shader_recompiler/runtime_info.h
index e6e1284762..560afb3648 100644
--- a/src/shader_recompiler/runtime_info.h
+++ b/src/shader_recompiler/runtime_info.h
@@ -7,6 +7,7 @@
#pragma once
#include <array>
+#include <bitset>
#include <map>
#include <optional>
#include <vector>
@@ -85,6 +86,8 @@ struct TransformFeedbackVarying {
struct RuntimeInfo {
std::array<AttributeType, 32> generic_input_types{};
VaryingState previous_stage_stores;
+ /// Fragment stage after a mesh program: generic inputs it writes per primitive.
+ std::bitset<32> per_primitive_generics{};
std::map<IR::Attribute, IR::Attribute> previous_stage_legacy_stores_mapping;
bool convert_depth_mode{};
diff --git a/src/shader_recompiler/shader_info.h b/src/shader_recompiler/shader_info.h
index 71309e040e..e2b341b0c3 100644
--- a/src/shader_recompiler/shader_info.h
+++ b/src/shader_recompiler/shader_info.h
@@ -7,6 +7,9 @@
#pragma once
#include <array>
+#include <optional>
+#include <span>
+#include <vector>
#include <bitset>
#include <map>
@@ -186,6 +189,9 @@ struct TextureBufferDescriptor {
u32 secondary_shift_left;
u32 count;
u32 size_shift;
+ /// Index in Info::handle_programs when the handle is computed at draw time (read from a
+ /// storage buffer), ~0 when it is the constant buffer word.
+ u32 handle_program{~0U};
auto operator<=>(const TextureBufferDescriptor&) const = default;
};
@@ -200,6 +206,9 @@ struct ImageBufferDescriptor {
u32 cbuf_offset;
u32 count;
u32 size_shift;
+ /// Index in Info::handle_programs when the handle is computed at draw time (read from a
+ /// storage buffer), ~0 when it is the constant buffer word.
+ u32 handle_program{~0U};
auto operator<=>(const ImageBufferDescriptor&) const = default;
};
@@ -219,6 +228,9 @@ struct TextureDescriptor {
u32 secondary_shift_left;
u32 count;
u32 size_shift;
+ /// Index in Info::handle_programs when the handle is computed at draw time (read from a
+ /// storage buffer), ~0 when it is the constant buffer word above.
+ u32 handle_program{~0U};
auto operator<=>(const TextureDescriptor&) const = default;
};
@@ -234,16 +246,34 @@ struct ImageDescriptor {
u32 cbuf_offset;
u32 count;
u32 size_shift;
+ /// Index in Info::handle_programs when the handle is computed at draw time (read from a
+ /// storage buffer), ~0 when it is the constant buffer word.
+ u32 handle_program{~0U};
auto operator<=>(const ImageDescriptor&) const = default;
};
using ImageDescriptors = boost::container::small_vector<ImageDescriptor, 4>;
+/// Switch 2 (NVN) task and mesh programs: the sizes their mesh registers set and, for mesh
+/// programs, the attribute (IR::Attribute index) held by each 128-byte line of a 32-element group
+/// in the emulated stage buffer: per-vertex lines from the program's output map, per-primitive
+/// lines from the pass-through geometry header bound with it, in attribute order.
+struct MeshInfo {
+ u32 payload_words{}; ///< Task output region / mesh input region, in 32-bit words
+ u32 max_vertices{};
+ u32 max_primitives{};
+ u32 topology{}; ///< SET_MESH_SHADER_A output topology (4 = triangles)
+ boost::container::static_vector<u32, 136> vertex_lines;
+ boost::container::static_vector<u32, 136> primitive_lines;
+};
+
struct Info {
static constexpr size_t MAX_INDIRECT_CBUFS{14};
static constexpr size_t MAX_CBUFS{18};
static constexpr size_t MAX_SSBOS{32};
+ bool is_task{}; ///< Switch 2 task program (Stage::Task)
+ bool is_mesh{}; ///< Switch 2 mesh program (Stage::Mesh)
bool uses_workgroup_id{};
bool uses_local_invocation_id{};
bool uses_invocation_id{};
@@ -337,6 +367,9 @@ struct Info {
TextureBufferDescriptors texture_buffer_descriptors;
ImageBufferDescriptors image_buffer_descriptors;
TextureDescriptors texture_descriptors;
+ /// Texture handles the shader reads from storage buffers, as programs over constant buffer
+ /// and storage buffer words (see EvaluateHandleProgram), evaluated for each draw.
+ std::vector<std::vector<u32>> handle_programs;
ImageDescriptors image_descriptors;
};
@@ -349,4 +382,111 @@ u32 NumDescriptors(const Descriptors& descriptors) {
return num;
}
+
+/// Operations of a handle program, a stack machine over 32-bit words. Operands follow the
+/// operation code in the program; values are popped right operand first.
+enum class HandleOp : u32 {
+ Immediate, ///< value
+ Cbuf, ///< index, offset: the constant buffer word
+ CbufDynamic, ///< index: the word at the popped byte offset
+ Storage, ///< binding: the storage buffer word at the popped byte offset
+ Add, Sub, Mul, And, Or, Xor, Shl, Shr, Sar, Neg, Not,
+ UMin, UMax, SMin, SMax,
+ SLt, SLe, SGt, SGe, ULt, ULe, UGt, UGe, Eq, Ne,
+ LAnd, LOr, LXor, LNot,
+ Select, ///< condition, true value, false value
+ Tee, ///< slot: keeps the top value in the slot (shared subexpressions)
+ Load, ///< slot: the value kept there
+};
+
+/// Runs a handle program; read_cbuf(index, offset) and read_storage(binding, offset) return the
+/// words it reads. Returns nothing for a malformed program.
+template <typename ReadCbuf, typename ReadStorage>
+std::optional<u32> EvaluateHandleProgram(std::span<const u32> code, ReadCbuf&& read_cbuf,
+ ReadStorage&& read_storage) {
+ std::array<u32, 64> stack{};
+ std::array<u32, 256> slots{};
+ size_t size = 0;
+ const auto pop = [&]() -> u32 { return size > 0 ? stack[--size] : 0; };
+ const auto push = [&](u32 value) {
+ if (size < stack.size()) stack[size++] = value;
+ };
+ for (size_t pc = 0; pc < code.size();) {
+ const HandleOp op = static_cast<HandleOp>(code[pc++]);
+ const auto operand = [&]() -> u32 { return pc < code.size() ? code[pc++] : 0; };
+ if (op == HandleOp::Immediate) {
+ push(operand());
+ continue;
+ }
+ if (op == HandleOp::Cbuf) {
+ const u32 index = operand();
+ push(read_cbuf(index, operand()));
+ continue;
+ }
+ if (op == HandleOp::CbufDynamic) {
+ const u32 index = operand();
+ push(read_cbuf(index, pop()));
+ continue;
+ }
+ if (op == HandleOp::Storage) {
+ const u32 binding = operand();
+ push(read_storage(binding, pop()));
+ continue;
+ }
+ if (op == HandleOp::Neg || op == HandleOp::Not || op == HandleOp::LNot) {
+ const u32 a = pop();
+ push(op == HandleOp::Neg ? 0U - a : op == HandleOp::Not ? ~a : (a == 0 ? 1U : 0U));
+ continue;
+ }
+ if (op == HandleOp::Tee) {
+ const u32 slot = operand();
+ if (slot < slots.size() && size > 0) slots[slot] = stack[size - 1];
+ continue;
+ }
+ if (op == HandleOp::Load) {
+ const u32 slot = operand();
+ push(slot < slots.size() ? slots[slot] : 0U);
+ continue;
+ }
+ if (op == HandleOp::Select) {
+ const u32 false_value = pop(), true_value = pop(), condition = pop();
+ push(condition != 0 ? true_value : false_value);
+ continue;
+ }
+ const u32 b = pop(), a = pop();
+ const s32 sa = static_cast<s32>(a), sb = static_cast<s32>(b);
+ switch (op) {
+ case HandleOp::Add: push(a + b); break;
+ case HandleOp::Sub: push(a - b); break;
+ case HandleOp::Mul: push(a * b); break;
+ case HandleOp::And: push(a & b); break;
+ case HandleOp::Or: push(a | b); break;
+ case HandleOp::Xor: push(a ^ b); break;
+ case HandleOp::Shl: push(b >= 32 ? 0U : a << b); break;
+ case HandleOp::Shr: push(b >= 32 ? 0U : a >> b); break;
+ case HandleOp::Sar: push(static_cast<u32>(sa >> (b >= 32 ? 31 : b))); break;
+ case HandleOp::UMin: push(a < b ? a : b); break;
+ case HandleOp::UMax: push(a > b ? a : b); break;
+ case HandleOp::SMin: push(static_cast<u32>(sa < sb ? sa : sb)); break;
+ case HandleOp::SMax: push(static_cast<u32>(sa > sb ? sa : sb)); break;
+ case HandleOp::SLt: push(sa < sb); break;
+ case HandleOp::SLe: push(sa <= sb); break;
+ case HandleOp::SGt: push(sa > sb); break;
+ case HandleOp::SGe: push(sa >= sb); break;
+ case HandleOp::ULt: push(a < b); break;
+ case HandleOp::ULe: push(a <= b); break;
+ case HandleOp::UGt: push(a > b); break;
+ case HandleOp::UGe: push(a >= b); break;
+ case HandleOp::Eq: push(a == b); break;
+ case HandleOp::Ne: push(a != b); break;
+ case HandleOp::LAnd: push(a != 0 && b != 0); break;
+ case HandleOp::LOr: push(a != 0 || b != 0); break;
+ case HandleOp::LXor: push((a != 0) != (b != 0)); break;
+ default: return std::nullopt;
+ }
+ }
+ if (size != 1) return std::nullopt;
+ return stack[0];
+}
+
} // namespace Shader
diff --git a/src/shader_recompiler/stage.h b/src/shader_recompiler/stage.h
index 9af0a1b720..34172cfee2 100644
--- a/src/shader_recompiler/stage.h
+++ b/src/shader_recompiler/stage.h
@@ -17,6 +17,11 @@ enum class Stage : u32 {
Compute,
VertexA,
+
+ // Switch 2 (NVN) task and mesh programs, bound in the vertex and tessellation slots while
+ // SET_MESH_CONTROL is enabled.
+ Task,
+ Mesh,
};
constexpr u32 MaxStageTypes = 6;
diff --git a/src/tests/CMakeLists.txt b/src/tests/CMakeLists.txt
index dd6f5d97b5..1eb1040e34 100644
--- a/src/tests/CMakeLists.txt
+++ b/src/tests/CMakeLists.txt
@@ -9,17 +9,26 @@ add_executable(tests
common/cityhash.cpp
common/container_hash.cpp
common/fibers.cpp
+ common/game_presets.cpp
common/host_memory.cpp
common/param_package.cpp
common/range_map.cpp
common/ring_buffer.cpp
common/scratch_buffer.cpp
+ common/slot_vector.cpp
common/undefined_fix.cpp
common/unique_function.cpp
core/core_timing.cpp
+ core/nup_writer.cpp
+ core/priority_queue.cpp
core/internal_network/network.cpp
video_core/memory_tracker.cpp
+ video_core/vertex_repack.cpp
video_core/ampere_returns.cpp
+ video_core/native_macro_fifo.cpp
+ video_core/uniform_stream_reuse.cpp
+ video_core/shader_invalidation.cpp
+ video_core/descriptor_table.cpp
input_common/calibration_configuration_job.cpp
)
diff --git a/src/tests/common/game_presets.cpp b/src/tests/common/game_presets.cpp
new file mode 100644
index 0000000000..481f5c5e1d
--- /dev/null
+++ b/src/tests/common/game_presets.cpp
@@ -0,0 +1,111 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <memory>
+#include <catch2/catch_test_macros.hpp>
+#include "common/game_presets.h"
+
+namespace {
+void SetConflictingPreferences(Settings::Values& settings, bool per_game) {
+ const auto set = [per_game](auto& setting, auto value) {
+ setting.SetGlobal(!per_game);
+ setting.SetValue(value);
+ };
+ set(settings.renderer_backend, Settings::RendererBackend::Null);
+ set(settings.resolution_setup, Settings::ResolutionSetup::Res2X);
+ set(settings.gpu_accuracy, Settings::GpuAccuracy::High);
+ set(settings.use_asynchronous_shaders, false);
+ set(settings.astc_recompression, Settings::AstcRecompression::Uncompressed);
+ set(settings.vram_usage_mode, Settings::VramUsageMode::Aggressive);
+ set(settings.use_reactive_flushing, true);
+ set(settings.vsync_mode, Settings::VSyncMode::Immediate);
+ set(settings.scaling_filter, Settings::ScalingFilter::NearestNeighbor);
+ set(settings.anti_aliasing, Settings::AntiAliasing::Smaa);
+}
+
+void CheckPreset(const Settings::Values& settings) {
+ CHECK(settings.renderer_backend.GetValue() == Settings::RendererBackend::Vulkan);
+ CHECK(settings.resolution_setup.GetValue() == Settings::ResolutionSetup::Res1X);
+ CHECK(settings.gpu_accuracy.GetValue() == Settings::GpuAccuracy::Low);
+ CHECK(settings.use_asynchronous_shaders.GetValue());
+ CHECK(settings.astc_recompression.GetValue() == Settings::AstcRecompression::Bc3);
+ CHECK(settings.vram_usage_mode.GetValue() == Settings::VramUsageMode::Conservative);
+ CHECK_FALSE(settings.use_reactive_flushing.GetValue());
+ CHECK(settings.vsync_mode.GetValue() == Settings::VSyncMode::FifoRelaxed);
+ CHECK(settings.use_docked_mode.GetValue() == Settings::ConsoleMode::Handheld);
+ CHECK(settings.scaling_filter.GetValue() == Settings::ScalingFilter::ScaleForce);
+ CHECK(settings.anti_aliasing.GetValue() == Settings::AntiAliasing::Fxaa);
+}
+
+void CheckPreferences(const Settings::Values& settings, bool global = false) {
+ CHECK(settings.renderer_backend.GetValue(global) == Settings::RendererBackend::Null);
+ CHECK(settings.resolution_setup.GetValue(global) == Settings::ResolutionSetup::Res2X);
+ CHECK(settings.gpu_accuracy.GetValue(global) == Settings::GpuAccuracy::High);
+ CHECK_FALSE(settings.use_asynchronous_shaders.GetValue(global));
+ CHECK(settings.astc_recompression.GetValue(global) ==
+ Settings::AstcRecompression::Uncompressed);
+ CHECK(settings.vram_usage_mode.GetValue(global) == Settings::VramUsageMode::Aggressive);
+ CHECK(settings.use_reactive_flushing.GetValue(global));
+ CHECK(settings.vsync_mode.GetValue(global) == Settings::VSyncMode::Immediate);
+ CHECK(settings.scaling_filter.GetValue(global) == Settings::ScalingFilter::NearestNeighbor);
+ CHECK(settings.anti_aliasing.GetValue(global) == Settings::AntiAliasing::Smaa);
+}
+} // namespace
+
+TEST_CASE("Display filter defaults use ScaleForce and FXAA", "[game_presets]") {
+ auto settings = std::make_unique<Settings::Values>();
+ CHECK(settings->scaling_filter.GetDefault() == Settings::ScalingFilter::ScaleForce);
+ CHECK(settings->anti_aliasing.GetDefault() == Settings::AntiAliasing::Fxaa);
+}
+
+TEST_CASE("Accepted MKW settings replace visible global preferences", "[game_presets]") {
+ auto settings = std::make_unique<Settings::Values>();
+ SetConflictingPreferences(*settings, false);
+ Settings::SetMarioKartWorldRecommendedSettings(*settings, true);
+ CheckPreset(*settings);
+ CHECK(settings->renderer_backend.UsingGlobal());
+ CHECK(settings->gpu_accuracy.UsingGlobal());
+ CHECK(settings->renderer_backend.GetValue(true) == Settings::RendererBackend::Vulkan);
+ CHECK(settings->gpu_accuracy.GetValue(true) == Settings::GpuAccuracy::Low);
+}
+
+TEST_CASE("MKW preset overrides saved settings for the session", "[game_presets]") {
+ auto settings = std::make_unique<Settings::Values>();
+ SetConflictingPreferences(*settings, false);
+ SetConflictingPreferences(*settings, true);
+ REQUIRE(
+ Settings::ApplyMarioKartWorldPreset(*settings, Settings::MarioKartWorldProgramId, true));
+ CheckPreset(*settings);
+ CheckPreferences(*settings, true);
+ for (const auto& restore : settings->linkage.restore_functions) {
+ restore();
+ }
+ CheckPreferences(*settings);
+ CHECK(settings->renderer_backend.UsingGlobal());
+ CHECK(settings->resolution_setup.UsingGlobal());
+ REQUIRE(
+ Settings::ApplyMarioKartWorldPreset(*settings, Settings::MarioKartWorldProgramId, true));
+ CheckPreset(*settings);
+}
+
+TEST_CASE("MKW preset does not change unrelated games", "[game_presets]") {
+ auto settings = std::make_unique<Settings::Values>();
+ SetConflictingPreferences(*settings, false);
+ CHECK_FALSE(Settings::ApplyMarioKartWorldPreset(*settings, 0, true));
+ CHECK_FALSE(Settings::ApplyMarioKartWorldPreset(*settings,
+ Settings::MarioKartWorldProgramId + 1, true));
+ CheckPreferences(*settings);
+ CHECK(settings->renderer_backend.UsingGlobal());
+ CHECK(settings->resolution_setup.UsingGlobal());
+}
+
+TEST_CASE("Declining the MKW preset preserves global and per-game preferences", "[game_presets]") {
+ auto settings = std::make_unique<Settings::Values>();
+ for (bool per_game : {false, true}) {
+ SetConflictingPreferences(*settings, per_game);
+ CHECK_FALSE(Settings::ApplyMarioKartWorldPreset(*settings,
+ Settings::MarioKartWorldProgramId, false));
+ CheckPreferences(*settings);
+ CHECK(settings->renderer_backend.UsingGlobal() == !per_game);
+ CHECK(settings->resolution_setup.UsingGlobal() == !per_game);
+ }
+}
diff --git a/src/tests/common/slot_vector.cpp b/src/tests/common/slot_vector.cpp
new file mode 100644
index 0000000000..d8f242085c
--- /dev/null
+++ b/src/tests/common/slot_vector.cpp
@@ -0,0 +1,54 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <map>
+#include <random>
+#include <vector>
+#include <catch2/catch_test_macros.hpp>
+#include "common/slot_vector.h"
+
+TEST_CASE("SlotVector visits live entries across empty bitmap words", "[common]") {
+ Common::SlotVector<u32> slots;
+ std::vector<Common::SlotId> ids;
+ for (u32 i = 0; i < 513; ++i) {
+ ids.push_back(slots.insert(i));
+ }
+ for (u32 i = 0; i < 512; ++i) {
+ slots.erase(ids[i]);
+ }
+ auto it = slots.begin();
+ REQUIRE(it != slots.end());
+ REQUIRE((*it).first == ids[512]);
+ REQUIRE(*(*it).second == 512);
+ REQUIRE(++it == slots.end());
+ slots.erase(ids[512]);
+ REQUIRE(slots.begin() == slots.end());
+}
+
+TEST_CASE("SlotVector sparse iteration matches a live-entry model", "[common]") {
+ Common::SlotVector<u32> slots;
+ std::map<u32, u32> live;
+ std::mt19937 random{9813};
+ for (u32 batch = 0; batch < 200; ++batch) {
+ for (u32 i = 0; i < 100; ++i) {
+ if (!live.empty() && (random() % 3) == 0) {
+ auto selected = live.begin();
+ std::advance(selected, random() % live.size());
+ slots.erase(Common::SlotId{selected->first});
+ live.erase(selected);
+ } else {
+ const u32 value = random();
+ const auto id = slots.insert(value);
+ live.emplace(id.Value(), value);
+ }
+ }
+ auto expected = live.begin();
+ for (const auto [id, value] : slots) {
+ REQUIRE(expected != live.end());
+ REQUIRE(id.Value() == expected->first);
+ REQUIRE(*value == expected->second);
+ ++expected;
+ }
+ REQUIRE(expected == live.end());
+ }
+}
diff --git a/src/tests/core/core_timing.cpp b/src/tests/core/core_timing.cpp
index 81898a1d38..6b424cc52c 100644
--- a/src/tests/core/core_timing.cpp
+++ b/src/tests/core/core_timing.cpp
@@ -33,6 +33,9 @@ std::optional<std::chrono::nanoseconds> HostCallbackTemplate(s64 time,
struct ScopeInit final {
ScopeInit() {
+ callbacks_ran_flags.reset();
+ delays.fill(0);
+ expected_callback = 0;
core_timing.SetMulticore(true);
core_timing.Initialize([]() {});
}
diff --git a/src/tests/core/nup_writer.cpp b/src/tests/core/nup_writer.cpp
new file mode 100644
index 0000000000..1ce9661218
--- /dev/null
+++ b/src/tests/core/nup_writer.cpp
@@ -0,0 +1,562 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <catch2/catch_test_macros.hpp>
+
+#include <array>
+#include <atomic>
+#include <chrono>
+#include <filesystem>
+#include <fstream>
+#include <iterator>
+#include <map>
+#include <string>
+#include <vector>
+
+#include <fmt/format.h>
+#include <openssl/evp.h>
+
+#include "core/file_sys/nup_container.h"
+#include "core/file_sys/nup_writer.h"
+#include "core/file_sys/vfs/vfs_real.h"
+#include "core/file_sys/vfs/vfs_vector.h"
+
+namespace {
+
+namespace fs = std::filesystem;
+using FileSys::NupContainer;
+
+struct TempDir {
+ fs::path path = fs::temp_directory_path() /
+ ("nup-test-" +
+ std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()));
+ TempDir() {
+ fs::create_directories(path);
+ }
+ ~TempDir() {
+ std::error_code ec;
+ fs::remove_all(path, ec);
+ }
+};
+
+void Put(const fs::path& p, std::string_view value) {
+ fs::create_directories(p.parent_path());
+ std::ofstream(p, std::ios::binary) << value;
+}
+
+std::string ReadAll(const fs::path& p) {
+ std::ifstream in(p, std::ios::binary);
+ return {std::istreambuf_iterator<char>(in), {}};
+}
+
+std::string Read(const FileSys::VirtualFile& f) {
+ std::string out(f->GetSize(), '\0');
+ f->Read(reinterpret_cast<u8*>(out.data()), out.size(), 0);
+ return out;
+}
+
+// Enough of an NPDM for the packer: the program ID at ACI0 + 0x10.
+std::string Npdm(u64 program_id) {
+ std::string npdm(0x100, '\0');
+ npdm.replace(0x80, 4, "ACI0");
+ for (int i = 0; i < 8; ++i) {
+ npdm[0x90 + i] = static_cast<char>(program_id >> (8 * i));
+ }
+ return npdm;
+}
+
+std::array<u8, 32> Sha256(std::string_view data) {
+ std::array<u8, 32> out{};
+ unsigned int length{};
+ EVP_Digest(data.data(), data.size(), out.data(), &length, EVP_sha256(), nullptr);
+ return out;
+}
+
+u64 Le(const std::string& buf, size_t at, size_t bytes) {
+ u64 value = 0;
+ for (size_t i = 0; i < bytes; ++i) {
+ value |= static_cast<u64>(static_cast<u8>(buf[at + i])) << (8 * i);
+ }
+ return value;
+}
+
+struct RawFile {
+ std::string name;
+ u64 size;
+ std::array<u8, 32> sha256;
+ u64 extent_offset;
+ u64 extent_size;
+ u8 codec;
+};
+
+// Reads the file records straight from the TOC, which NupContainer does not expose.
+std::vector<RawFile> RawFiles(const std::string& nup) {
+ const u64 toc = Le(nup, 0x08, 8);
+ const u64 packages = Le(nup, toc + 0x04, 4);
+ const u64 dirs = Le(nup, toc + 0x08, 8);
+ const u64 files = Le(nup, toc + 0x10, 8);
+ const u64 pool = toc + Le(nup, toc + 0x18, 8);
+ std::vector<RawFile> out;
+ u64 pos = toc + 0x30 + packages * 0x40 + dirs * 0x18;
+ for (u64 i = 0; i < files; ++i, pos += 0x50) {
+ RawFile file{nup.substr(pool + Le(nup, pos, 4), Le(nup, pos + 4, 4)),
+ Le(nup, pos + 0x08, 8),
+ {},
+ Le(nup, pos + 0x10, 8),
+ Le(nup, pos + 0x18, 8),
+ static_cast<u8>(nup[pos + 0x40])};
+ std::copy_n(nup.begin() + static_cast<std::ptrdiff_t>(pos + 0x20), 32,
+ file.sha256.begin());
+ out.push_back(file);
+ }
+ return out;
+}
+
+// Every file under `dir` with its contents, keyed by its path relative to `dir`.
+std::map<std::string, std::string> SourceTree(const fs::path& dir) {
+ std::map<std::string, std::string> out;
+ for (const auto& entry : fs::recursive_directory_iterator(dir)) {
+ if (entry.is_regular_file() && entry.path().filename() != ".listing") {
+ out[fs::relative(entry.path(), dir).generic_string()] = ReadAll(entry.path());
+ }
+ }
+ return out;
+}
+
+void PackedTree(const FileSys::VirtualDir& dir, const std::string& prefix,
+ std::map<std::string, std::string>& out) {
+ if (dir == nullptr) {
+ return;
+ }
+ for (const auto& file : dir->GetFiles()) {
+ out[prefix + file->GetName()] = Read(file);
+ }
+ for (const auto& sub : dir->GetSubdirectories()) {
+ PackedTree(sub, prefix + sub->GetName() + "/", out);
+ }
+}
+
+std::map<std::string, std::string> PackedTree(const FileSys::VirtualDir& dir) {
+ std::map<std::string, std::string> out;
+ PackedTree(dir, "", out);
+ return out;
+}
+
+constexpr u64 TitleId = 0x0400ABCD00010000ULL;
+
+void MakeDump(const fs::path& root) {
+ Put(root / "Base/ExeFS/main", "base main");
+ Put(root / "Base/ExeFS/main.npdm", Npdm(TitleId));
+ Put(root / "Base/RomFS/shared.bin", std::string(5000, 's'));
+ Put(root / "Base/RomFS/copy/shared.bin", std::string(5000, 's'));
+ Put(root / "Base/RomFS/empty", "");
+ Put(root / "Base/RomFS/.listing", "listing");
+ Put(root / "Base/RomFS/System/Resource/ResourceSizeTable.Product.100.rsizetable", "rst");
+ Put(root / "Base/RomFS/big.bin", std::string(64 * 1024, 'b'));
+ Put(root / "Base/RomFS/random.bin", [] {
+ std::string data(3000, '\0');
+ u32 x = 12345;
+ for (auto& c : data) {
+ x = x * 1664525 + 1013904223;
+ c = static_cast<char>(x >> 24);
+ }
+ return data;
+ }());
+ Put(root / "updates/65536/exefs/main", "update main");
+ Put(root / "updates/65536/exefs/main.npdm", Npdm(TitleId));
+ Put(root / "updates/65536/romfs/shared.bin", std::string(5000, 's'));
+ Put(root / "updates/65536/romfs/System/Resource/ResourceSizeTable.Product.110.rsizetable",
+ "rst");
+ Put(root / "updates/65536/romfs/Zeta/a", "a");
+ Put(root / "updates/65536/romfs/alpha", "alpha");
+ Put(root / "updates/131072/exefs/main", "update 2 main");
+ Put(root / "updates/131072/exefs/main.npdm", Npdm(TitleId));
+ Put(root / "dlc/0400ABCD00011001/0/romfs/addon.bin", "addon data");
+}
+
+void SetLe(std::string& buf, size_t at, u64 value, size_t bytes) {
+ for (size_t i = 0; i < bytes; ++i) {
+ buf[at + i] = static_cast<char>(value >> (8 * i));
+ }
+}
+
+// Applies `patch` to a valid archive, fixes up the TOC hash and opens the result.
+template <typename Patch>
+Loader::ResultStatus StatusAfterPatch(std::string nup, Patch&& patch) {
+ const u64 toc = Le(nup, 0x08, 8);
+ patch(nup, toc);
+ const auto hash = Sha256(std::string_view{nup}.substr(toc, Le(nup, 0x10, 8)));
+ std::copy(hash.begin(), hash.end(), nup.begin() + 0x18);
+ const auto data = std::vector<u8>(nup.begin(), nup.end());
+ return NupContainer{std::make_shared<FileSys::VectorVfsFile>(data, "bad.nup")}.GetStatus();
+}
+
+} // namespace
+
+TEST_CASE("NUP packs base, updates and add-ons and reads back through NupContainer", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ MakeDump(root);
+ const auto archive = t.path / "Game.nup";
+ FileSys::Nup::PackOptions options{.raw_threshold = 16 * 1024};
+ std::string error;
+ REQUIRE(FileSys::Nup::PackDump(root, archive, options, {}, nullptr, &error) ==
+ FileSys::Nup::PackResult::Success);
+ INFO(error);
+ REQUIRE_FALSE(fs::exists(archive.string() + ".part"));
+
+ FileSys::RealVfsFilesystem real;
+ NupContainer nup{real.OpenFile(archive.string(), FileSys::OpenMode::Read)};
+ REQUIRE(nup.GetStatus() == Loader::ResultStatus::Success);
+ REQUIRE(nup.VerifyIntegrity({}) == Loader::ResultStatus::Success);
+
+ const auto& packages = nup.GetPackages();
+ REQUIRE(packages.size() == 4);
+ REQUIRE(packages[0].kind == NupContainer::PackageKind::Base);
+ REQUIRE(packages[0].title_id == TitleId);
+ REQUIRE(packages[0].version == 0);
+ REQUIRE(packages[0].display_version == "1.0.0");
+ REQUIRE(packages[0].npdm.size() == 0x100);
+ REQUIRE(packages[1].kind == NupContainer::PackageKind::Update);
+ REQUIRE(packages[1].version == 65536);
+ REQUIRE(packages[1].display_version == "1.1.0");
+ REQUIRE(packages[1].title_id == TitleId);
+ REQUIRE_FALSE(packages[1].standalone_update);
+ REQUIRE(packages[2].kind == NupContainer::PackageKind::Update);
+ REQUIRE(packages[2].version == 131072);
+ REQUIRE(packages[2].romfs_dir == 0xFFFFFFFF);
+ REQUIRE(packages[3].kind == NupContainer::PackageKind::Addon);
+ REQUIRE(packages[3].title_id == 0x0400ABCD00011001ULL);
+ REQUIRE(packages[3].exefs_dir == 0xFFFFFFFF);
+ REQUIRE(nup.FindBestUpdatePackage() == std::size_t{2});
+
+ REQUIRE(PackedTree(nup.GetExeFSDir(0)) == SourceTree(root / "Base/ExeFS"));
+ REQUIRE(PackedTree(nup.GetRomFSDir(0)) == SourceTree(root / "Base/RomFS"));
+ REQUIRE(PackedTree(nup.GetExeFSDir(1)) == SourceTree(root / "updates/65536/exefs"));
+ REQUIRE(PackedTree(nup.GetRomFSDir(1)) == SourceTree(root / "updates/65536/romfs"));
+ REQUIRE(PackedTree(nup.GetExeFSDir(2)) == SourceTree(root / "updates/131072/exefs"));
+ REQUIRE(PackedTree(nup.GetRomFSDir(3)) == SourceTree(root / "dlc/0400ABCD00011001/0/romfs"));
+ REQUIRE_FALSE(nup.GetRomFSDir(0)->GetFile(".listing"));
+
+ const auto bytes = ReadAll(archive);
+ const auto files = RawFiles(bytes);
+ std::map<std::string, std::vector<RawFile>> by_name;
+ for (const auto& file : files) {
+ by_name[file.name].push_back(file);
+ if (file.size != 0) {
+ REQUIRE(file.extent_offset % 16 == 0);
+ }
+ }
+ const auto shared = by_name.at("shared.bin");
+ REQUIRE(shared.size() == 3);
+ for (const auto& file : shared) {
+ REQUIRE(file.sha256 == Sha256(std::string(5000, 's')));
+ REQUIRE(file.codec == 1);
+ REQUIRE(file.extent_offset == shared[0].extent_offset);
+ REQUIRE(file.extent_size == shared[0].extent_size);
+ }
+ const auto& empty = by_name.at("empty").at(0);
+ REQUIRE(empty.size == 0);
+ REQUIRE(empty.extent_offset == 0);
+ REQUIRE(empty.extent_size == 0);
+ REQUIRE(empty.codec == 0);
+ REQUIRE(empty.sha256 == Sha256(""));
+ // Highly compressible, but above the raw threshold.
+ const auto& big = by_name.at("big.bin").at(0);
+ REQUIRE(big.codec == 0);
+ REQUIRE(big.extent_size == 64 * 1024);
+ REQUIRE(big.sha256 == Sha256(std::string(64 * 1024, 'b')));
+ // Incompressible, so stored raw.
+ const auto& random = by_name.at("random.bin").at(0);
+ REQUIRE(random.codec == 0);
+ REQUIRE(random.extent_size == 3000);
+ REQUIRE_FALSE(by_name.contains(".listing"));
+}
+
+TEST_CASE("NUP reads zstd members above the memory cache through a temporary file", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ MakeDump(root);
+ std::string large(300 * 1024, '\0');
+ u32 x = 7;
+ for (auto& c : large) {
+ x = x * 1664525 + 1013904223;
+ c = static_cast<char>('a' + (x >> 28));
+ }
+ Put(root / "Base/RomFS/large.bin", large);
+ const auto archive = t.path / "Game.nup";
+ FileSys::Nup::PackOptions options{.raw_threshold = 1024 * 1024};
+ REQUIRE(FileSys::Nup::PackDump(root, archive, options) == FileSys::Nup::PackResult::Success);
+ for (const auto& file : RawFiles(ReadAll(archive))) {
+ if (file.name == "large.bin") {
+ REQUIRE(file.codec == 1);
+ }
+ }
+
+ const auto spill_dir = t.path / "spill";
+ FileSys::RealVfsFilesystem real;
+ {
+ // Members above 32 KiB do not fit the 64 KiB cache budget and are spilled to disk.
+ NupContainer nup{real.OpenFile(archive.string(), FileSys::OpenMode::Read), 64 * 1024,
+ spill_dir};
+ REQUIRE(nup.GetStatus() == Loader::ResultStatus::Success);
+ const auto file = nup.GetRomFSDir(0)->GetFile("large.bin");
+ REQUIRE(file != nullptr);
+ REQUIRE(file->GetSize() == large.size());
+
+ std::string chunk(4096, '\0');
+ for (size_t offset = 0; offset < large.size(); offset += chunk.size()) {
+ const size_t count =
+ file->Read(reinterpret_cast<u8*>(chunk.data()), chunk.size(), offset);
+ REQUIRE(count == std::min(chunk.size(), large.size() - offset));
+ REQUIRE(chunk.compare(0, count, large, offset, count) == 0);
+ }
+ for (const size_t offset : {size_t{0}, size_t{123456}, size_t{77}, large.size() - 5}) {
+ const size_t count = file->Read(reinterpret_cast<u8*>(chunk.data()), 100, offset);
+ REQUIRE(count == std::min<size_t>(100, large.size() - offset));
+ REQUIRE(chunk.compare(0, count, large, offset, count) == 0);
+ }
+ REQUIRE(file->Read(reinterpret_cast<u8*>(chunk.data()), 10, large.size()) == 0);
+
+ REQUIRE(std::distance(fs::directory_iterator(spill_dir), fs::directory_iterator{}) > 0);
+ REQUIRE(PackedTree(nup.GetRomFSDir(0)) == SourceTree(root / "Base/RomFS"));
+ REQUIRE(nup.VerifyIntegrity({}) == Loader::ResultStatus::Success);
+ }
+ REQUIRE(std::distance(fs::directory_iterator(spill_dir), fs::directory_iterator{}) == 0);
+}
+
+TEST_CASE("NUP rejects a TOC whose counts or offsets wrap", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ MakeDump(root);
+ const auto archive = t.path / "Game.nup";
+ REQUIRE(FileSys::Nup::PackDump(root, archive) == FileSys::Nup::PackResult::Success);
+ const auto nup = ReadAll(archive);
+ const auto no_patch = [](std::string&, u64) {};
+ REQUIRE(StatusAfterPatch(nup, no_patch) == Loader::ResultStatus::Success);
+
+ // 2^61 directories wrap dir_count * 0x18 back to the real record size.
+ REQUIRE(StatusAfterPatch(nup, [](std::string& buf, u64 toc) {
+ SetLe(buf, toc + 0x08, Le(buf, toc + 0x08, 8) + (1ULL << 61), 8);
+ }) == Loader::ResultStatus::ErrorNUPBadFormat);
+
+ const auto first_dir = [](const std::string& buf, u64 toc) {
+ return toc + 0x30 + Le(buf, toc + 0x04, 4) * 0x40;
+ };
+ // first_child + child_count wraps to a small value.
+ REQUIRE(StatusAfterPatch(nup, [&](std::string& buf, u64 toc) {
+ SetLe(buf, first_dir(buf, toc) + 0x08, 0xFFFFFFFF, 4);
+ SetLe(buf, first_dir(buf, toc) + 0x0C, 2, 4);
+ }) == Loader::ResultStatus::ErrorNUPBadFormat);
+}
+
+TEST_CASE("NUP rejects a directory that contains its own ancestor", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ MakeDump(root);
+ const auto archive = t.path / "Game.nup";
+ REQUIRE(FileSys::Nup::PackDump(root, archive) == FileSys::Nup::PackResult::Success);
+
+ // Give the base RomFS directory the same children as the package root, which lists it.
+ REQUIRE(StatusAfterPatch(ReadAll(archive), [](std::string& buf, u64 toc) {
+ const u64 dirs = toc + 0x30 + Le(buf, toc + 0x04, 4) * 0x40;
+ const u64 root_dir = Le(buf, toc + 0x30 + 0x08, 4);
+ const u64 romfs_dir = Le(buf, toc + 0x30 + 0x10, 4);
+ const u64 root_record = dirs + root_dir * 0x18;
+ const u64 romfs_record = dirs + romfs_dir * 0x18;
+ SetLe(buf, romfs_record + 0x08, Le(buf, root_record + 0x08, 4), 4);
+ SetLe(buf, romfs_record + 0x0C, Le(buf, root_record + 0x0C, 4), 4);
+ }) == Loader::ResultStatus::ErrorNUPBadFormat);
+}
+
+TEST_CASE("NUP marks updates standalone when the dump has no base", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ Put(root / "updates/65536/exefs/main", "main");
+ Put(root / "updates/65536/exefs/main.npdm", Npdm(TitleId));
+ Put(root / "updates/65536/romfs/file", "rom");
+ const auto archive = t.path / "Game.nup";
+ REQUIRE(FileSys::Nup::PackDump(root, archive) == FileSys::Nup::PackResult::Success);
+
+ FileSys::RealVfsFilesystem real;
+ NupContainer nup{real.OpenFile(archive.string(), FileSys::OpenMode::Read)};
+ REQUIRE(nup.GetStatus() == Loader::ResultStatus::Success);
+ REQUIRE(nup.GetPackages().size() == 1);
+ REQUIRE(nup.GetPackages()[0].kind == NupContainer::PackageKind::Update);
+ REQUIRE(nup.GetPackages()[0].standalone_update);
+ REQUIRE(nup.GetPackages()[0].title_id == TitleId);
+ REQUIRE(Read(nup.GetRomFSDir(0)->GetFile("file")) == "rom");
+}
+
+TEST_CASE("NUP packs a bare exefs/romfs dump as one base", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ Put(root / "ExeFS/main", "main");
+ Put(root / "ExeFS/main.npdm", Npdm(TitleId));
+ Put(root / "RomFS/file", "rom");
+ Put(root / "notes.txt", "extra");
+ const auto archive = t.path / "Game.nup";
+ REQUIRE(FileSys::Nup::PackDump(root, archive) == FileSys::Nup::PackResult::Success);
+
+ FileSys::RealVfsFilesystem real;
+ NupContainer nup{real.OpenFile(archive.string(), FileSys::OpenMode::Read)};
+ REQUIRE(nup.GetStatus() == Loader::ResultStatus::Success);
+ REQUIRE(nup.GetPackages().size() == 1);
+ REQUIRE(nup.GetPackages()[0].kind == NupContainer::PackageKind::Base);
+ REQUIRE_FALSE(nup.GetPackages()[0].standalone_update);
+ REQUIRE(Read(nup.GetExeFSDir(0)->GetFile("main")) == "main");
+ REQUIRE(Read(nup.GetRomFSDir(0)->GetFile("file")) == "rom");
+ const auto files = RawFiles(ReadAll(archive));
+ REQUIRE(std::ranges::count(files, std::string{"notes.txt"}, &RawFile::name) == 1);
+}
+
+TEST_CASE("NUP low storage resumes and deletes the packed sources", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ MakeDump(root);
+ const auto copy = t.path / "copy" / "Game";
+ fs::create_directories(copy);
+ fs::copy(root, copy, fs::copy_options::recursive);
+ const auto archive = t.path / "Game.nup";
+
+ std::atomic_bool cancel = false;
+ bool cancelled = false;
+ FileSys::Nup::PackOptions options{.delete_sources = true, .chunk_bytes = 1};
+ auto result = FileSys::Nup::PackDump(
+ root, archive, options,
+ [&](const auto& p) {
+ if (p.files_done >= 4 && !cancelled) {
+ cancel = true;
+ cancelled = true;
+ }
+ },
+ &cancel);
+ REQUIRE(result == FileSys::Nup::PackResult::Cancelled);
+ REQUIRE(FileSys::Nup::HasResumableJournal(archive));
+ REQUIRE_FALSE(fs::exists(root / "Base/ExeFS/main"));
+ REQUIRE_FALSE(fs::exists(root / "Base/ExeFS/main.npdm"));
+
+ cancel = false;
+ std::string error;
+ REQUIRE(FileSys::Nup::PackDump(root, archive, options, {}, &cancel, &error) ==
+ FileSys::Nup::PackResult::Success);
+ INFO(error);
+ REQUIRE_FALSE(fs::exists(archive.string() + ".journal"));
+ REQUIRE_FALSE(fs::exists(archive.string() + ".part"));
+ REQUIRE_FALSE(fs::exists(root / "Base/ExeFS"));
+ REQUIRE_FALSE(fs::exists(root / "updates"));
+ REQUIRE_FALSE(fs::exists(root / "dlc"));
+ // The listing is not packed, so it is left behind with the folders holding it.
+ REQUIRE(ReadAll(root / "Base/RomFS/.listing") == "listing");
+ REQUIRE_FALSE(fs::exists(root / "Base/RomFS/shared.bin"));
+
+ FileSys::RealVfsFilesystem real;
+ NupContainer nup{real.OpenFile(archive.string(), FileSys::OpenMode::Read)};
+ REQUIRE(nup.GetStatus() == Loader::ResultStatus::Success);
+ REQUIRE(nup.VerifyIntegrity({}) == Loader::ResultStatus::Success);
+ REQUIRE(nup.GetPackages().size() == 4);
+ const auto npdm = Npdm(TitleId);
+ REQUIRE(nup.GetPackages()[0].npdm == std::vector<u8>(npdm.begin(), npdm.end()));
+ REQUIRE(PackedTree(nup.GetRomFSDir(0)) == SourceTree(copy / "Base/RomFS"));
+ REQUIRE(PackedTree(nup.GetRomFSDir(1)) == SourceTree(copy / "updates/65536/romfs"));
+
+ // The resumed archive matches one packed in a single pass.
+ const auto again = t.path / "again.nup";
+ REQUIRE(FileSys::Nup::PackDump(copy, again) == FileSys::Nup::PackResult::Success);
+ REQUIRE(ReadAll(archive) == ReadAll(again));
+}
+
+TEST_CASE("NUP rejects overwrite and a dump without NPDM", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ Put(root / "exefs/main", "main");
+ Put(root / "romfs/a", "rom");
+ const auto archive = t.path / "out.nup";
+ Put(archive, "existing");
+ REQUIRE(FileSys::Nup::PackDump(root, archive) == FileSys::Nup::PackResult::IoError);
+ REQUIRE(ReadAll(archive) == "existing");
+ fs::remove(archive);
+ REQUIRE(FileSys::Nup::PackDump(root, archive) == FileSys::Nup::PackResult::InvalidSource);
+ REQUIRE_FALSE(fs::exists(archive.string() + ".part"));
+}
+
+// Archives are meant to be byte-identical on every platform. A change to this hash means
+// packing a dump no longer reproduces archives made by earlier builds (for example after a zstd
+// update), so make it deliberately.
+TEST_CASE("NUP output is reproducible", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ MakeDump(root);
+ // Large and compressible, so it covers what used to be zstd's multithreaded path.
+ Put(root / "Base/RomFS/large.bin", [] {
+ std::string data(20 * 1024 * 1024, '\0');
+ u32 x = 1;
+ for (auto& c : data) {
+ x = x * 1664525 + 1013904223;
+ c = static_cast<char>('a' + (x >> 28));
+ }
+ return data;
+ }());
+ const auto archive = t.path / "Game.nup";
+ REQUIRE(FileSys::Nup::PackDump(root, archive) == FileSys::Nup::PackResult::Success);
+
+ std::string hex;
+ for (const u8 b : Sha256(ReadAll(archive))) {
+ hex += fmt::format("{:02x}", b);
+ }
+ // The mixed-case Base/RomFS directory supplies base version 1.0.0 on every platform.
+ REQUIRE(hex == "718231e0974ca04c7be7bd6ed4334015b777c6e5f1ed51de23f899f81ee004ab");
+}
+
+TEST_CASE("NUP writes metadata overrides to the base and the newest update", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ MakeDump(root);
+ const auto probed = FileSys::Nup::ProbeMetadata(root);
+ REQUIRE(probed.title_name.empty());
+ REQUIRE(probed.icon.empty());
+ // The version comes from the RomFS resource size table of the newest update that has one.
+ REQUIRE(probed.display_version == "1.1.0");
+ REQUIRE(probed.version_source == "update 65536");
+
+ FileSys::Nup::PackOptions options;
+ options.metadata.title_name = "Custom Name";
+ options.metadata.icon = {0xFF, 0xD8, 0xFF, 0xD9};
+ options.metadata.display_version = "9.8.7";
+ const auto archive = t.path / "Game.nup";
+ REQUIRE(FileSys::Nup::PackDump(root, archive, options) == FileSys::Nup::PackResult::Success);
+
+ FileSys::RealVfsFilesystem real;
+ NupContainer nup{real.OpenFile(archive.string(), FileSys::OpenMode::Read)};
+ REQUIRE(nup.GetStatus() == Loader::ResultStatus::Success);
+ const auto& packages = nup.GetPackages();
+ REQUIRE(packages.size() == 4);
+ REQUIRE(packages[0].title_name == "Custom Name");
+ REQUIRE(packages[0].icon == options.metadata.icon);
+ REQUIRE(packages[0].display_version == "9.8.7");
+ REQUIRE(packages[2].version == 131072);
+ REQUIRE(packages[2].display_version == "9.8.7");
+ REQUIRE(packages[1].display_version == "1.1.0");
+}
+
+TEST_CASE("NUP probes the version from the game data list tag", "[nup]") {
+ TempDir t;
+ const auto root = t.path / "Game";
+ Put(root / "base/exefs/main", "main");
+ Put(root / "base/exefs/main.npdm", Npdm(TitleId));
+ Put(root / "base/romfs/GameData/GameDataList.Product.100.byml.zs", "x");
+ Put(root / "base/romfs/System/Resource/ResourceSizeTable.Product.rsizetable.zs", "x");
+ Put(root / "updates/458752/exefs/main", "main");
+ Put(root / "updates/458752/exefs/main.npdm", Npdm(TitleId));
+ Put(root / "updates/458752/romfs/GameData/GameDataList.Product.320.byml.zs", "x");
+ Put(root / "updates/458752/romfs/GameData/GameDataList.Product.320.revision.txt", "1");
+
+ auto probed = FileSys::Nup::ProbeMetadata(root);
+ REQUIRE(probed.display_version == "3.2.0");
+ REQUIRE(probed.version_source == "update 458752");
+
+ fs::remove_all(root / "updates");
+ probed = FileSys::Nup::ProbeMetadata(root);
+ REQUIRE(probed.display_version == "1.0.0");
+ REQUIRE(probed.version_source == "base");
+}
diff --git a/src/tests/core/priority_queue.cpp b/src/tests/core/priority_queue.cpp
new file mode 100644
index 0000000000..0d7174f793
--- /dev/null
+++ b/src/tests/core/priority_queue.cpp
@@ -0,0 +1,111 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <algorithm>
+#include <array>
+#include <random>
+#include <vector>
+#include <catch2/catch_test_macros.hpp>
+#include "core/hle/kernel/k_priority_queue.h"
+
+namespace {
+struct Affinity {
+ u64 GetAffinityMask() const {
+ return 3;
+ }
+ void SetAffinityMask(u64) {}
+ bool GetAffinity(s32) const {
+ return true;
+ }
+ void SetAffinity(s32, bool) {}
+ void SetAll() {}
+};
+
+struct Member {
+ struct QueueEntry {
+ Member* prev{};
+ Member* next{};
+ void Initialize() {
+ prev = next = nullptr;
+ }
+ void SetPrev(Member* value) {
+ prev = value;
+ }
+ void SetNext(Member* value) {
+ next = value;
+ }
+ Member* GetNext() const {
+ return next;
+ }
+ Member* GetPrev() const {
+ return prev;
+ }
+ };
+ std::array<QueueEntry, 2> entries{};
+ s32 priority{};
+ QueueEntry& GetPriorityQueueEntry(s32 core) {
+ return entries[core];
+ }
+ const QueueEntry& GetPriorityQueueEntry(s32 core) const {
+ return entries[core];
+ }
+ Affinity GetAffinityMask() const {
+ return {};
+ }
+ s32 GetActiveCore() const {
+ return 0;
+ }
+ s32 GetPriority() const {
+ return priority;
+ }
+ bool IsDummyThread() const {
+ return false;
+ }
+};
+
+template <size_t Priorities>
+void CheckQueue() {
+ using Queue = Kernel::KPriorityQueue<Member, 2, Priorities - 1, 0>;
+ typename Queue::KPriorityQueueImpl queue;
+ std::array<Member, 160> members{};
+ std::array<std::array<bool, 2>, 160> present{};
+ std::array<std::array<std::vector<Member*>, Priorities>, 2> model{};
+ std::mt19937 random{6152};
+ for (size_t i = 0; i < members.size(); ++i) {
+ members[i].priority = static_cast<s32>(i % Priorities);
+ }
+ REQUIRE(queue.GetFront(0) == nullptr);
+ for (u32 operation = 0; operation < 3000; ++operation) {
+ const size_t index = random() % members.size();
+ const s32 core = random() % 2;
+ auto& member = members[index];
+ auto& expected = model[core][member.priority];
+ if (present[index][core]) {
+ queue.Remove(member.priority, core, &member);
+ std::erase(expected, &member);
+ } else if ((random() & 1) != 0) {
+ queue.PushFront(member.priority, core, &member);
+ expected.insert(expected.begin(), &member);
+ } else {
+ queue.PushBack(member.priority, core, &member);
+ expected.push_back(&member);
+ }
+ present[index][core] = !present[index][core];
+ for (s32 checked_core = 0; checked_core < 2; ++checked_core) {
+ Member* actual = queue.GetFront(checked_core);
+ for (const auto& priority : model[checked_core]) {
+ for (Member* wanted : priority) {
+ REQUIRE(actual == wanted);
+ actual = queue.GetNext(checked_core, actual);
+ }
+ }
+ REQUIRE(actual == nullptr);
+ }
+ }
+}
+} // namespace
+
+TEST_CASE("Priority queue bitmap traversal preserves thread order", "[core]") {
+ CheckQueue<64>();
+ CheckQueue<128>();
+}
diff --git a/src/tests/input_common/calibration_configuration_job.cpp b/src/tests/input_common/calibration_configuration_job.cpp
index 6ded0dfc86..c45ef157a5 100644
--- a/src/tests/input_common/calibration_configuration_job.cpp
+++ b/src/tests/input_common/calibration_configuration_job.cpp
@@ -5,6 +5,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <array>
+#include <memory>
#include <string>
#include <thread>
#include <utility>
@@ -19,7 +20,7 @@
class FakeCemuhookServer {
public:
FakeCemuhookServer()
- : socket(io_context, boost::asio::ip::udp::endpoint(boost::asio::ip::udp::v4(), 0)) {}
+ : socket(io_context, boost::asio::ip::udp::endpoint(boost::asio::ip::address_v4::loopback(), 0)) {}
~FakeCemuhookServer() {
boost::system::error_code error_code;
@@ -132,3 +133,20 @@ TEST_CASE("CalibrationConfigurationJob completed", "[input_common]") {
REQUIRE(max_x == 200);
REQUIRE(max_y == 200);
}
+
+TEST_CASE("CalibrationConfigurationJob cancellation joins its worker", "[input_common]") {
+ FakeCemuhookServer server;
+ for (unsigned i = 0; i < 32; ++i) {
+ auto lifetime = std::make_shared<int>(0);
+ std::weak_ptr<int> observer = lifetime;
+ {
+ InputCommon::CemuhookUDP::CalibrationConfigurationJob job(
+ server.GetHost(), server.GetPort(),
+ [lifetime](InputCommon::CemuhookUDP::CalibrationConfigurationJob::Status) {},
+ [](u16, u16, u16, u16) {});
+ lifetime.reset();
+ job.Stop();
+ }
+ REQUIRE(observer.expired());
+ }
+}
diff --git a/src/tests/video_core/ampere_returns.cpp b/src/tests/video_core/ampere_returns.cpp
index 56b8b374da..163758e07e 100644
--- a/src/tests/video_core/ampere_returns.cpp
+++ b/src/tests/video_core/ampere_returns.cpp
@@ -48,4 +48,31 @@ TEST_CASE("Ampere callees still reject writes to the link", "[video_core][ampere
REQUIRE(lowered.error == "native callee overwrites link register");
REQUIRE(lowered.error_offset == 0x30);
}
+
+TEST_CASE("Ampere callees can compare into predicates with an R0 link", "[video_core][ampere]") {
+ // ISETP's zero destination field is not a write to R0.
+ const std::array<Instruction, 5> code{{
+ {{0x00007802, 0x20, 0x00000f00, 0}},
+ {{0x00007944, 0x10, 0x03c00000, 0}},
+ {{0x0000794d, 0, 0x03800000, 0}},
+ {{0x0100720c, 0x00000002, 0x03f06070, 0}},
+ {{0x00007950, 0xffffffb0, 0x03c3ffff, 0}},
+ }};
+ REQUIRE(static_cast<bool>(LowerToMaxwell(code)));
+}
+
+TEST_CASE("Ampere callees reject a wide multiply over the link", "[video_core][ampere]") {
+ // IMAD.WIDE.U32 R54:R55 writes the R55 link.
+ const std::array<Instruction, 5> code{{
+ {{0x00377802, 0x20, 0x00000f00, 0}},
+ {{0x00007944, 0x10, 0x03c00000, 0}},
+ {{0x0000794d, 0, 0x03800000, 0}},
+ {{0x01367225, 0x00000002, 0x078e00ff, 0}},
+ {{0x37007950, 0xffffffb0, 0x03c3ffff, 0}},
+ }};
+ const auto lowered = LowerToMaxwell(code);
+ REQUIRE(!static_cast<bool>(lowered));
+ REQUIRE(lowered.error == "native callee overwrites link register");
+ REQUIRE(lowered.error_offset == 0x30);
+}
} // namespace
diff --git a/src/tests/video_core/descriptor_table.cpp b/src/tests/video_core/descriptor_table.cpp
new file mode 100644
index 0000000000..8b20564949
--- /dev/null
+++ b/src/tests/video_core/descriptor_table.cpp
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <array>
+#include <chrono>
+#include <cstring>
+#include <iostream>
+#include <limits>
+#include <random>
+#include <catch2/catch_test_macros.hpp>
+#include "video_core/texture_cache/descriptor_table.h"
+
+namespace {
+struct DescriptorMemory {
+ std::array<std::array<u64, 128>, 2> banks{};
+
+ void ReadBlockUnsafe(GPUVAddr address, void* output, size_t size) const {
+ const auto bank = (address - 0x1000) / 0x1000;
+ const auto offset = (address - 0x1000) % 0x1000;
+ std::memcpy(output, reinterpret_cast<const u8*>(banks[bank].data()) + offset, size);
+ }
+};
+} // namespace
+
+TEST_CASE("Descriptor invalidation matches eager refresh across mutations", "[descriptor_table]") {
+ DescriptorMemory memory;
+ VideoCommon::DescriptorTable<u64> table;
+ REQUIRE(table.Synchronize(0x1000, 127));
+ std::array<bool, 128> seen{};
+ std::array<u64, 128> previous{};
+ std::mt19937 random{0x5080};
+ unsigned bank{};
+ u32 limit = 127;
+ for (unsigned i = 0; i < 40000; ++i) {
+ switch (random() % 5) {
+ case 0:
+ table.Invalidate();
+ seen.fill(false);
+ break;
+ case 1:
+ memory.banks[random() % 2][random() % 128] = random();
+ break;
+ case 2: {
+ const auto next_bank = random() % 2;
+ const u32 next_limit = random() % 2 ? 63 : 127;
+ const bool changed = next_bank != bank || next_limit != limit;
+ REQUIRE(table.Synchronize(0x1000 + next_bank * 0x1000, next_limit) == changed);
+ if (changed) {
+ seen.fill(false);
+ }
+ bank = next_bank;
+ limit = next_limit;
+ break;
+ }
+ default: {
+ const u32 index = random() % (limit + 1);
+ const u64 expected = memory.banks[bank][index];
+ const bool changed = !seen[index] || previous[index] != expected;
+ const auto actual = table.Read(memory, index);
+ REQUIRE(actual.first == expected);
+ REQUIRE(actual.second == changed);
+ seen[index] = true;
+ previous[index] = expected;
+ }
+ }
+ }
+}
+
+TEST_CASE("Repeated image deletion cannot reuse stale descriptor view IDs", "[descriptor_table]") {
+ DescriptorMemory memory;
+ std::array<VideoCommon::DescriptorTable<u64>, 5> channels;
+ std::array<std::array<unsigned, 128>, 5> view_ids{};
+ unsigned live_generation = 1;
+ const auto visit = [&](size_t channel, u32 index) {
+ if (channels[channel].Read(memory, index).second) {
+ view_ids[channel][index] = live_generation;
+ }
+ return view_ids[channel][index];
+ };
+ for (size_t channel = 0; channel < channels.size(); ++channel) {
+ REQUIRE(channels[channel].Synchronize(0x1000, 127));
+ for (u32 index = 0; index < 128; ++index) {
+ REQUIRE(visit(channel, index) == live_generation);
+ }
+ }
+ for (unsigned generation = 0; generation < 32; ++generation) {
+ ++live_generation;
+ for (unsigned deletion = 0; deletion < 50; ++deletion) {
+ for (auto& table : channels) {
+ table.Invalidate();
+ }
+ }
+ for (size_t channel = 0; channel < channels.size(); ++channel) {
+ for (u32 index = 0; index < 128; ++index) {
+ REQUIRE(visit(channel, index) == live_generation);
+ REQUIRE_FALSE(channels[channel].Read(memory, index).second);
+ }
+ }
+ }
+}
+
+TEST_CASE("Descriptor limits clear exposed slots after shrink and growth", "[descriptor_table]") {
+ struct AddressMemory {
+ void ReadBlockUnsafe(GPUVAddr address, void* output, size_t size) const {
+ std::memcpy(output, &address, size);
+ }
+ } memory;
+ constexpr std::array<u32, 9> limits{0, 63, 64, 127, 128, 0x7ffff, 0x80000, 0x80001, 0x100000};
+ for (const u32 limit : limits) {
+ VideoCommon::DescriptorTable<u64> table;
+ REQUIRE(table.Synchronize(0x1000, limit));
+ CHECK(table.Read(memory, limit).second);
+ CHECK_FALSE(table.Read(memory, limit).second);
+ table.Refresh(0x1000, 0);
+ table.Invalidate();
+ CHECK(table.Read(memory, 0).second);
+ CHECK_FALSE(table.Read(memory, 0).second);
+ table.Refresh(0x1000, limit);
+ CHECK(table.Read(memory, limit).second);
+ CHECK_FALSE(table.Read(memory, limit).second);
+ table.Invalidate();
+ table.Invalidate();
+ CHECK(table.Read(memory, limit).second);
+ CHECK_FALSE(table.Read(memory, limit).second);
+ }
+ for (const u32 limit :
+ {std::numeric_limits<u32>::max(), std::numeric_limits<u32>::max() - 0x1000}) {
+ VideoCommon::DescriptorTable<u64> table;
+ table.Refresh(0x1000, limit);
+ CHECK(table.Read(memory, 0).second);
+ table.Invalidate();
+ CHECK(table.Read(memory, 0).second);
+ CHECK_FALSE(table.Read(memory, 0).second);
+ }
+}
+
+TEST_CASE("Descriptor invalidation CPU benchmark", "[.][cache_bench]") {
+ DescriptorMemory memory;
+ const auto measure = [&](bool deferred) {
+ std::array<VideoCommon::DescriptorTable<u64>, 5> tables;
+ for (auto& table : tables) {
+ REQUIRE(table.Synchronize(0x1000, 127));
+ }
+ void (*volatile invalidate)(VideoCommon::DescriptorTable<u64>&) =
+ deferred ? +[](VideoCommon::DescriptorTable<u64>& table) { table.Invalidate(); }
+ : +[](VideoCommon::DescriptorTable<u64>& table) {
+ std::ranges::fill(table.read_descriptors, 0);
+ };
+ size_t changed{};
+ const auto start = std::chrono::steady_clock::now();
+ for (unsigned batch = 0; batch < 2000; ++batch) {
+ for (unsigned deletion = 0; deletion < 50; ++deletion) {
+ for (auto& table : tables) {
+ invalidate(table);
+ }
+ }
+ for (auto& table : tables) {
+ changed += table.Read(memory, 0).second;
+ }
+ }
+ const auto ms =
+ std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start)
+ .count();
+ REQUIRE(changed == 10000);
+ return ms;
+ };
+ const double eager = measure(false);
+ const double deferred = measure(true);
+ std::cout << "Descriptor bitmap invalidation: eager=" << eager << " ms deferred=" << deferred
+ << " ms (CPU only, not gameplay FPS)\n";
+}
diff --git a/src/tests/video_core/memory_tracker.cpp b/src/tests/video_core/memory_tracker.cpp
index c2236848b5..3ac4cee840 100644
--- a/src/tests/video_core/memory_tracker.cpp
+++ b/src/tests/video_core/memory_tracker.cpp
@@ -30,6 +30,10 @@ class RasterizerInterface {
public:
void UpdatePagesCachedCount(DAddr addr, size_t size, s32 delta) {
++update_calls;
+ ApplyPagesCachedCount(addr, size, delta);
+ }
+
+ void ApplyPagesCachedCount(DAddr addr, size_t size, s32 delta) {
calls.emplace_back(addr, size, delta);
const u64 page_start{addr >> Core::DEVICE_PAGEBITS};
const u64 page_end{(addr + size + Core::DEVICE_PAGESIZE - 1) >> Core::DEVICE_PAGEBITS};
@@ -45,7 +49,13 @@ public:
}
void UpdatePagesCachedBatch(std::span<const std::pair<DAddr, size_t>> ranges, s32 delta) {
- // TODO: for now assume fine?
+ if (ranges.empty()) {
+ return;
+ }
+ ++update_calls;
+ for (const auto& [addr, size] : ranges) {
+ ApplyPagesCachedCount(addr, size, delta);
+ }
}
[[nodiscard]] size_t UpdateCalls() const noexcept { return update_calls; }
@@ -557,20 +567,32 @@ TEST_CASE("MemoryTracker: Cached write downloads") {
REQUIRE(rasterizer.Count() == 0);
}
-TEST_CASE("MemoryTracker: FlushCachedWrites batching") {
+TEST_CASE("MemoryTracker: cached writes preserve tracking counts") {
RasterizerInterface rasterizer;
std::optional<MemoryTracker> memory_track(rasterizer);
memory_track->UnmarkRegionAsCpuModified(c, WORD * 2);
+ REQUIRE(rasterizer.UpdateCalls() == 1);
+ REQUIRE(rasterizer.UpdateCallsList().size() == 1);
+ REQUIRE(rasterizer.UpdateCallsList().front() == std::tuple{c, WORD * 2, 1});
+
memory_track->CachedCpuWrite(c + PAGE, PAGE);
memory_track->CachedCpuWrite(c + PAGE * 2, PAGE);
memory_track->CachedCpuWrite(c + PAGE * 4, PAGE);
- REQUIRE(rasterizer.UpdateCalls() == 0);
+ REQUIRE(rasterizer.Count() == 125);
+ REQUIRE(rasterizer.Count(c + PAGE) == 0);
+ REQUIRE(rasterizer.Count(c + PAGE * 2) == 0);
+ REQUIRE(rasterizer.Count(c + PAGE * 3) == 1);
+ REQUIRE(rasterizer.Count(c + PAGE * 4) == 0);
+ const auto update_calls = rasterizer.UpdateCalls();
memory_track->FlushCachedWrites();
- // Now we expect a single batch call (coalesced ranges) to the device memory manager
- REQUIRE(rasterizer.UpdateCalls() == 1);
- const auto& calls = rasterizer.UpdateCallsList();
- REQUIRE(std::get<0>(calls[0]) == c + PAGE);
- REQUIRE(std::get<1>(calls[0]) == PAGE * 3);
+ REQUIRE(rasterizer.UpdateCalls() == update_calls);
+ REQUIRE(rasterizer.Count() == 125);
+ REQUIRE(memory_track->IsRegionCpuModified(c + PAGE, PAGE));
+ REQUIRE(memory_track->IsRegionCpuModified(c + PAGE * 2, PAGE));
+ REQUIRE_FALSE(memory_track->IsRegionCpuModified(c + PAGE * 3, PAGE));
+ REQUIRE(memory_track->IsRegionCpuModified(c + PAGE * 4, PAGE));
+ memory_track->MarkRegionAsCpuModified(c, WORD * 2);
+ REQUIRE(rasterizer.Count() == 0);
}
TEST_CASE("DeviceMemoryManager: UpdatePagesCachedBatch basic") {
diff --git a/src/tests/video_core/native_macro_fifo.cpp b/src/tests/video_core/native_macro_fifo.cpp
new file mode 100644
index 0000000000..e19b146192
--- /dev/null
+++ b/src/tests/video_core/native_macro_fifo.cpp
@@ -0,0 +1,74 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <array>
+#include <vector>
+#include <catch2/catch_test_macros.hpp>
+
+#ifdef ARCHITECTURE_x86_64
+#include "video_core/macro_turing_jit.h"
+
+namespace {
+std::array<u32, 3> FifoInstruction(u32 method, bool end) {
+ std::array<u32, 3> bits{};
+ const auto field = [&](u32 offset, u32 count, u32 value) {
+ for (u32 bit = 0; bit < count; ++bit) {
+ bits[(offset + bit) / 32] |= ((value >> bit) & 1) << ((offset + bit) % 32);
+ }
+ };
+ field(0, 1, end);
+ for (u32 lane = 0; lane < 2; ++lane) {
+ field(15 + lane * 36, 5, 24);
+ field(20 + lane * 36, 5, 24);
+ field(25 + lane * 36, 5, 24);
+ }
+ field(30, 16, method);
+ field(82, 3, 5);
+ field(85, 4, 3);
+ return {bits[2], bits[1], bits[0]};
+}
+} // namespace
+
+TEST_CASE("Owned native macro FIFOs preserve loads across DMA appends and repeated calls",
+ "[video_core][native_macro]") {
+ std::vector<u32> code;
+ for (const auto instruction : {FifoInstruction(0x158, false), FifoInstruction(0x200, true),
+ FifoInstruction(0x201, false)}) {
+ code.insert(code.end(), instruction.begin(), instruction.end());
+ }
+ Tegra::TuringMacroJit jit;
+ jit.Prepare(code, 1);
+ std::array<u32, 0xc00> ram{};
+ std::vector<u32> parameters;
+ std::vector<u32> reference;
+ for (u32 invocation = 0; invocation < 1000; ++invocation) {
+ const u32 count = 2 + invocation % 127;
+ parameters.push_back(count);
+ reference.assign(parameters.begin(), parameters.end());
+ const auto run = [&](std::vector<u32>& input) {
+ std::vector<std::pair<u32, u32>> writes;
+ const auto result = jit.Execute(
+ 0, input, ram, [](u32) { return 0U; },
+ [&](u32 method, u32 value) {
+ writes.emplace_back(method, value);
+ if (method == 0x158) {
+ for (u32 word = 0; word < value; ++word) {
+ input.push_back(invocation * 256 + word);
+ }
+ }
+ return true;
+ });
+ CHECK(result.success);
+ CHECK(result.consumed == 3);
+ return writes;
+ };
+ const auto expected = run(reference);
+ const auto actual = run(parameters);
+ REQUIRE(actual.size() == 3);
+ CHECK(actual == expected);
+ CHECK(parameters == reference);
+ CHECK(actual[1] == std::pair<u32, u32>{0x200, invocation * 256});
+ CHECK(actual[2] == std::pair<u32, u32>{0x201, invocation * 256 + 1});
+ parameters.clear();
+ }
+}
+#endif
diff --git a/src/tests/video_core/shader_invalidation.cpp b/src/tests/video_core/shader_invalidation.cpp
new file mode 100644
index 0000000000..806fb5c58d
--- /dev/null
+++ b/src/tests/video_core/shader_invalidation.cpp
@@ -0,0 +1,120 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <array>
+#include <chrono>
+#include <iostream>
+#include <memory>
+#include <random>
+#include <vector>
+#include <catch2/catch_test_macros.hpp>
+#include "core/device_memory.h"
+#include "video_core/shader_cache.h"
+
+namespace VideoCommon {
+struct ShaderCacheTestAccess {
+ static std::unique_ptr<ShaderCache> Create(Tegra::MaxwellDeviceMemoryManager& memory) {
+ return std::unique_ptr<ShaderCache>{new ShaderCache{memory}};
+ }
+ static void Register(ShaderCache& cache, VAddr address, size_t size, u64 hash) {
+ auto data = std::make_unique<ShaderInfo>();
+ data->unique_hash = hash;
+ cache.Register(std::move(data), address, size);
+ }
+ static const ShaderInfo* Find(const ShaderCache& cache, VAddr address) {
+ return cache.TryGet(address);
+ }
+};
+} // namespace VideoCommon
+
+using VideoCommon::ShaderCacheTestAccess;
+
+TEST_CASE("Shader batch invalidation matches sequential cache contents", "[shader_invalidation]") {
+ Core::DeviceMemory memory;
+ Tegra::MaxwellDeviceMemoryManager device_memory{memory};
+ auto batched = ShaderCacheTestAccess::Create(device_memory);
+ auto sequential = ShaderCacheTestAccess::Create(device_memory);
+ std::mt19937 random{0x16'67};
+ constexpr VAddr base = 0x100000;
+ for (unsigned iteration = 0; iteration < 600; ++iteration) {
+ for (unsigned slot = 0; slot < 128; ++slot) {
+ const auto address = base + slot * 4096;
+ if (!ShaderCacheTestAccess::Find(*batched, address)) {
+ const auto size = 1 + random() % 65536;
+ ShaderCacheTestAccess::Register(*batched, address, size, iteration + slot);
+ ShaderCacheTestAccess::Register(*sequential, address, size, iteration + slot);
+ }
+ }
+ std::vector<std::pair<DAddr, size_t>> writes;
+ for (unsigned i = 0, count = random() % 24; i < count; ++i) {
+ writes.emplace_back(base + random() % (128 * 4096), random() % 32768);
+ if (i % 4 == 0) {
+ writes.push_back(writes.back());
+ }
+ }
+ for (const auto& [address, size] : writes) {
+ sequential->InvalidateRegion(address, size);
+ }
+ batched->InvalidateRegions(writes);
+ for (unsigned slot = 0; slot < 128; ++slot) {
+ const auto address = base + slot * 4096;
+ const auto actual = ShaderCacheTestAccess::Find(*batched, address);
+ const auto expected = ShaderCacheTestAccess::Find(*sequential, address);
+ REQUIRE(bool(actual) == bool(expected));
+ if (actual) {
+ REQUIRE(actual->unique_hash == expected->unique_hash);
+ }
+ }
+ }
+}
+
+TEST_CASE("Shader empty batches preserve pending invalidations", "[shader_invalidation]") {
+ Core::DeviceMemory memory;
+ Tegra::MaxwellDeviceMemoryManager device_memory{memory};
+ auto cache = ShaderCacheTestAccess::Create(device_memory);
+ constexpr VAddr address = 0x100000;
+ ShaderCacheTestAccess::Register(*cache, address, 32768, 123);
+ cache->OnCacheInvalidation(address, 1);
+ cache->InvalidateRegions({});
+ REQUIRE(ShaderCacheTestAccess::Find(*cache, address));
+ const std::array unrelated{std::pair<DAddr, size_t>{address + 0x100000, 4096}};
+ cache->InvalidateRegions(unrelated);
+ REQUIRE_FALSE(ShaderCacheTestAccess::Find(*cache, address));
+}
+
+TEST_CASE("Shader invalidation CPU benchmark", "[.][cache_bench]") {
+ Core::DeviceMemory memory;
+ Tegra::MaxwellDeviceMemoryManager device_memory{memory};
+ auto batched = ShaderCacheTestAccess::Create(device_memory);
+ auto sequential = ShaderCacheTestAccess::Create(device_memory);
+ std::vector<std::pair<DAddr, size_t>> writes;
+ constexpr VAddr base = 0x100000;
+ for (unsigned slot = 0; slot < 768; ++slot) {
+ writes.emplace_back(base + slot * 65536, 1);
+ }
+ double sequential_ms{}, batched_ms{};
+ for (unsigned repeat = 0; repeat < 20; ++repeat) {
+ for (unsigned slot = 0; slot < 1024; ++slot) {
+ const auto address = base + slot * 65536;
+ if (!ShaderCacheTestAccess::Find(*batched, address)) {
+ ShaderCacheTestAccess::Register(*batched, address, 64, slot);
+ ShaderCacheTestAccess::Register(*sequential, address, 64, slot);
+ }
+ }
+ auto start = std::chrono::steady_clock::now();
+ for (const auto& [address, size] : writes) {
+ sequential->InvalidateRegion(address, size);
+ }
+ sequential_ms +=
+ std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start)
+ .count();
+ start = std::chrono::steady_clock::now();
+ batched->InvalidateRegions(writes);
+ batched_ms +=
+ std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - start)
+ .count();
+ REQUIRE_FALSE(ShaderCacheTestAccess::Find(*batched, base));
+ REQUIRE(ShaderCacheTestAccess::Find(*batched, base + 1023 * 65536));
+ }
+ std::cout << "Shader invalidation: sequential=" << sequential_ms << " ms batched=" << batched_ms
+ << " ms (CPU only, not gameplay FPS)\n";
+}
diff --git a/src/tests/video_core/uniform_stream_reuse.cpp b/src/tests/video_core/uniform_stream_reuse.cpp
new file mode 100644
index 0000000000..6059b70eeb
--- /dev/null
+++ b/src/tests/video_core/uniform_stream_reuse.cpp
@@ -0,0 +1,116 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <algorithm>
+#include <array>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <span>
+#include <vector>
+#include <catch2/catch_test_macros.hpp>
+
+#include "video_core/buffer_cache/uniform_stream_reuse.h"
+
+namespace {
+void Check(bool condition, const char* message) {
+ INFO(message);
+ REQUIRE(condition);
+}
+
+struct Memory {
+ std::array<u8, 8192> bytes{};
+ void ReadBlockUnsafe(u64 address, void* destination, u32 size) {
+ Check(address + size <= bytes.size(), "read outside guest memory");
+ std::memcpy(destination, bytes.data() + address, size);
+ }
+};
+
+struct Runtime {
+ struct StreamUniformRef {
+ size_t allocation;
+ };
+ std::vector<std::vector<u8>> allocations;
+ StreamUniformRef last{}, bound{};
+ u64 tick{1};
+ u64 CurrentTick() const {
+ return tick;
+ }
+ StreamUniformRef LastStreamUniformRef() const {
+ return last;
+ }
+ std::span<u8> BindMappedUniformBuffer(size_t, u32, u32 size) {
+ allocations.emplace_back(size);
+ last = bound = StreamUniformRef{allocations.size() - 1};
+ return allocations.back();
+ }
+ void RebindStreamUniform(StreamUniformRef ref, u32 size) {
+ Check(ref.allocation < allocations.size(), "reused a recycled allocation");
+ Check(allocations[ref.allocation].size() == size, "reused wrong allocation size");
+ bound = ref;
+ }
+ void NextTick() {
+ ++tick;
+ allocations.clear();
+ }
+};
+
+void CheckBound(const Runtime& runtime, const Memory& memory, u64 address, u32 size) {
+ const auto& bytes = runtime.allocations.at(runtime.bound.allocation);
+ Check(bytes.size() == size &&
+ std::memcmp(bytes.data(), memory.bytes.data() + address, size) == 0,
+ "bound uniform data differs from guest memory");
+}
+} // namespace
+
+TEST_CASE("Uniform stream reuse preserves data through mutation and session resets",
+ "[video_core]") {
+ using VideoCommon::BindSmallUniformReusing;
+ Memory memory;
+ Runtime runtime;
+ VideoCommon::ResetUniformStreamReuse<Runtime>();
+ auto bind = [&](size_t stage, u32 slot, u64 address, u32 size) {
+ Check(BindSmallUniformReusing(runtime, memory, stage, slot, slot, address, size),
+ "valid small uniform rejected");
+ CheckBound(runtime, memory, address, size);
+ };
+ bind(0, 0, 0, 64);
+ bind(0, 0, 0, 64);
+ Check(runtime.allocations.size() == 1, "identical bytes were copied twice");
+ memory.bytes[7] = 23;
+ bind(0, 0, 0, 64);
+ Check(runtime.allocations.size() == 2, "changed bytes reused stale data");
+ bind(0, 1, 0, 64);
+ bind(1, 0, 0, 64);
+ Check(runtime.allocations.size() == 4, "stage or slot shared the wrong allocation");
+ runtime.NextTick();
+ bind(0, 0, 0, 64);
+ Check(runtime.allocations.size() == 1, "a new command buffer reused an old allocation");
+ VideoCommon::ResetUniformStreamReuse<Runtime>();
+ bind(0, 0, 0, 64);
+ Check(runtime.allocations.size() == 2, "session reset retained an old allocation");
+ bind(4, 17, 0, 4096);
+ const auto before = runtime.allocations.size();
+ for (auto [stage, slot, size] :
+ {std::array<u32, 3>{5, 0, 64}, {0, 18, 64}, {0, 0, 0}, {0, 0, 4097}}) {
+ Check(!BindSmallUniformReusing(runtime, memory, stage, slot, slot, 0, size),
+ "invalid uniform range did not use the fallback");
+ }
+ Check(runtime.allocations.size() == before, "fallback allocated stream memory");
+
+ u32 random = 17;
+ for (u32 index = 0; index < 10000; ++index) {
+ random = random * 1664525U + 1013904223U;
+ if (index % 37 == 0)
+ runtime.NextTick();
+ if (index % 503 == 0)
+ VideoCommon::ResetUniformStreamReuse<Runtime>();
+ const u32 size = 1 + random % 4096;
+ const u64 address = (random >> 12) % (memory.bytes.size() - 4096);
+ if (index % 3 == 0)
+ memory.bytes[address + random % size] ^= u8{0x5a};
+ const size_t stage = (random >> 20) % 5;
+ const u32 slot = (random >> 24) % 18;
+ bind(stage, slot, address, size);
+ bind(stage, slot, address, size);
+ }
+}
diff --git a/src/tests/video_core/vertex_repack.cpp b/src/tests/video_core/vertex_repack.cpp
new file mode 100644
index 0000000000..5e21e3714c
--- /dev/null
+++ b/src/tests/video_core/vertex_repack.cpp
@@ -0,0 +1,126 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include <catch2/catch_test_macros.hpp>
+#include "video_core/renderer_vulkan/vertex_repack.h"
+
+namespace {
+using Regs = Tegra::Engines::Maxwell3D::Regs;
+using Size = Regs::VertexAttribute::Size;
+
+Regs MakeLayout() {
+ Regs regs{};
+ for (auto& attr : regs.vertex_attrib_format) {
+ attr.constant.Assign(1);
+ }
+ regs.vertex_streams[0].stride.Assign(69);
+ auto& position = regs.vertex_attrib_format[0];
+ position.constant.Assign(0);
+ position.size.Assign(Size::Size_R32_G32_B32);
+ auto& packed = regs.vertex_attrib_format[1];
+ packed.constant.Assign(0);
+ packed.offset.Assign(65);
+ packed.size.Assign(Size::Size_A2_B10_G10_R10);
+ return regs;
+}
+}
+
+TEST_CASE("Vertex repacking preserves aligned fields and relocates packed tail", "[vertex_repack]") {
+ const auto regs = MakeLayout();
+ const auto mask = Vulkan::VertexRepackMask(regs);
+ REQUIRE(mask == 1);
+ REQUIRE(Vulkan::RepackedVertexOffset(mask, 0, 0) == 0);
+ REQUIRE(Vulkan::RepackedVertexOffset(mask, 0, 64) == 64);
+ REQUIRE(Vulkan::RepackedVertexOffset(mask, 0, 65) == 68);
+ REQUIRE(Vulkan::RepackedVertexStride(mask, 0, 69) == 72);
+ REQUIRE(Vulkan::RepackedVertexStride(mask, 1, 16) == 16);
+}
+
+TEST_CASE("Vertex repacking excludes overlapping and oversized attributes", "[vertex_repack]") {
+ auto regs = MakeLayout();
+ auto& attr = regs.vertex_attrib_format[2];
+ attr.constant.Assign(0);
+ attr.offset.Assign(64);
+ attr.size.Assign(Size::Size_R32);
+ REQUIRE(Vulkan::VertexRepackMask(regs) == 0);
+ attr.offset.Assign(68);
+ REQUIRE(Vulkan::VertexRepackMask(regs) == 0);
+ attr.constant.Assign(1);
+ REQUIRE(Vulkan::VertexRepackMask(regs) == 1);
+}
+
+TEST_CASE("Vertex repacking ignores constant invalid formats", "[vertex_repack]") {
+ auto regs = MakeLayout();
+ REQUIRE(Vulkan::VertexRepackMask(regs) == 1);
+ regs.vertex_attrib_format[2].constant.Assign(0);
+ REQUIRE(Vulkan::VertexRepackMask(regs) == 0);
+}
+
+TEST_CASE("Vertex repacking selects bindings independently", "[vertex_repack]") {
+ auto regs = MakeLayout();
+ regs.vertex_streams[31].stride.Assign(69);
+ auto& attr = regs.vertex_attrib_format[2];
+ attr = regs.vertex_attrib_format[1];
+ attr.buffer.Assign(31);
+ REQUIRE(Vulkan::VertexRepackMask(regs) == 0x80000001U);
+ regs.vertex_streams[0].stride.Assign(72);
+ REQUIRE(Vulkan::VertexRepackMask(regs) == 0x80000000U);
+ REQUIRE(Vulkan::RepackedVertexOffset(0x80000000U, 0, 65) == 65);
+}
+
+TEST_CASE("Vertex repacking requires the captured tail and stride", "[vertex_repack]") {
+ auto regs = MakeLayout();
+ regs.vertex_streams[0].stride.Assign(68);
+ REQUIRE(Vulkan::VertexRepackMask(regs) == 0);
+ regs.vertex_streams[0].stride.Assign(69);
+ regs.vertex_attrib_format[1].offset.Assign(64);
+ REQUIRE(Vulkan::VertexRepackMask(regs) == 0);
+}
+
+TEST_CASE("Static vertex repacking follows the device stride alignment", "[vertex_repack]") {
+ REQUIRE(Vulkan::VertexRepackInputSupported(true, false));
+ REQUIRE_FALSE(Vulkan::VertexRepackInputSupported(false, false));
+ REQUIRE(Vulkan::VertexRepackInputSupported(false, true));
+ REQUIRE_FALSE(Vulkan::StaticVertexRepackSelected(nullptr, 1));
+ REQUIRE(Vulkan::StaticVertexRepackSelected(nullptr, 4));
+ REQUIRE(Vulkan::StaticVertexRepackSelected("1", 1));
+ REQUIRE_FALSE(Vulkan::StaticVertexRepackSelected("0", 4));
+ REQUIRE(Vulkan::StaticVertexRepackSelected("other", 4));
+}
+
+TEST_CASE("Vertex repack planning keeps descriptor offsets aligned", "[vertex_repack]") {
+ for (const u32 alignment : {4U, 16U, 64U, 256U, 4096U}) {
+ for (const u32 offset : {0U, 1U, 3U, 255U, 4099U}) {
+ const auto plan = Vulkan::MakeVertexRepackParameters(offset, 70, alignment, 1U << 20,
+ 65535, 1U << 20);
+ REQUIRE(plan.has_value());
+ REQUIRE(plan->source_base % alignment == 0);
+ REQUIRE(plan->source_base + plan->source_bias == offset);
+ REQUIRE(plan->vertices == 2);
+ REQUIRE(plan->output_size == 144);
+ REQUIRE(plan->source_range >= plan->source_bias + 70);
+ REQUIRE(plan->source_range % 4 == 0);
+ REQUIRE(plan->dispatch_groups == 1);
+ }
+ }
+}
+
+TEST_CASE("Vertex repack planning rejects overflow and device-limit violations", "[vertex_repack]") {
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 0, 256, 1U << 20, 65535, 1U << 20));
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 0, 1U << 20, 65535, 1U << 20));
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 256, 71, 65535, 1U << 20));
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(255, 69, 256, 323, 65535, 1U << 20));
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 256, 1U << 20, 0, 1U << 20));
+ REQUIRE_FALSE(
+ Vulkan::MakeVertexRepackParameters(0, 0xffffffffU, 256, ~u64{0}, 0xffffffffU, ~u64{0}));
+ const auto one = Vulkan::MakeVertexRepackParameters(0, 69, 256, 72, 1, 72);
+ REQUIRE(one.has_value());
+ REQUIRE(one->source_range == 72);
+}
+
+TEST_CASE("Vertex repack planning keeps the source range inside the buffer", "[vertex_repack]") {
+ const auto clamped = Vulkan::MakeVertexRepackParameters(0, 69, 256, 1U << 20, 65535, 69);
+ REQUIRE(clamped.has_value());
+ REQUIRE(clamped->source_range == 69);
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(0, 69, 256, 1U << 20, 65535, 68));
+ REQUIRE_FALSE(Vulkan::MakeVertexRepackParameters(300, 69, 256, 1U << 20, 65535, 256));
+}
diff --git a/src/video_core/CMakeLists.txt b/src/video_core/CMakeLists.txt
index 5ed43fc903..b3ef88c5f1 100644
--- a/src/video_core/CMakeLists.txt
+++ b/src/video_core/CMakeLists.txt
@@ -235,6 +235,7 @@ add_library(video_core STATIC
shader_cache.h
shader_environment.cpp
shader_environment.h
+ shader_handle_program.h
shader_notify.cpp
shader_notify.h
smaa_area_tex.h
diff --git a/src/video_core/buffer_cache/buffer_cache.h b/src/video_core/buffer_cache/buffer_cache.h
index e90af3214d..40369b16ef 100644
--- a/src/video_core/buffer_cache/buffer_cache.h
+++ b/src/video_core/buffer_cache/buffer_cache.h
@@ -10,13 +10,16 @@
#include <memory>
#include <numeric>
#include <stdexcept>
+#include <utility>
#include <vector>
#include <fmt/ranges.h>
#include "common/range_sets.inc"
#include "video_core/buffer_cache/buffer_cache_base.h"
+#include "video_core/buffer_cache/uniform_stream_reuse.h"
#include "video_core/guest_memory.h"
#include "video_core/host1x/gpu_device_memory_manager.h"
+#include "video_core/nx2.h"
#include "video_core/nx2_profile.h"
#include "video_core/texture_cache/util.h"
@@ -27,6 +30,9 @@ using Core::DEVICE_PAGESIZE;
template <class P>
BufferCache<P>::BufferCache(Tegra::MaxwellDeviceMemoryManager& device_memory_, Runtime& runtime_)
: runtime{runtime_}, device_memory{device_memory_}, memory_tracker{device_memory} {
+ // A new runtime has a new scheduler (its ticks restart) and a new stream buffer: whatever the
+ // stream uniform reuse remembers refers to the old ones. Before the early return below.
+ VideoCommon::ResetUniformStreamReuse<Runtime>();
// Ensure the first slot is used for the null buffer
void(slot_buffers.insert(runtime, NullBufferParams{}));
gpu_modified_ranges.Clear();
@@ -100,6 +106,12 @@ void BufferCache<P>::TickFrame() {
return;
}
runtime.TickFrame(slot_buffers);
+ if (frame_tick % 600 == 0 && !uploaded_buffer_ranges.empty()) {
+ for (const auto& [address, size] : uploaded_buffer_ranges) {
+ device_memory.HintCold(address, size);
+ }
+ uploaded_buffer_ranges.clear();
+ }
if (static u64 frames{}; ++frames % 600 == 0) {
u64 total{};
size_t count{};
@@ -141,9 +153,10 @@ void BufferCache<P>::TickFrame() {
// title 1's buffers were ~200 MiB of an 8 GB GPU's ~6.8 GB in use, yet this collector
// ran aggressively every frame on the device-wide figure, evicting up to 64 buffers (and
// downloading the GPU-written ones, ~5% of the GPU thread) that were recreated later.
- // Collect when the buffers themselves are large.
- if (buffer_bytes >= 1_GiB) {
- RunGarbageCollector(buffer_bytes >= 2_GiB);
+ // Collect when the buffers themselves are large. At 1 GiB it held geometry of scenes
+ // long gone: title 1's title screen grew to ~0.9 GB of buffers in ten minutes.
+ if (buffer_bytes >= 512_MiB) {
+ RunGarbageCollector(buffer_bytes >= 1_GiB);
}
} else if (total_used_memory >= minimum_memory) {
RunGarbageCollector(total_used_memory >= critical_memory);
@@ -166,11 +179,13 @@ void BufferCache<P>::UnmapGPUMemory(size_t as_id, GPUVAddr gpu_addr, size_t size
template <class P>
void BufferCache<P>::WriteMemory(DAddr device_addr, u64 size) {
+ Nx2NoteListHostWrite(device_addr, size);
if (IsRegionGpuModified(device_addr, size)) {
ClearDownload(device_addr, size);
gpu_modified_ranges.Subtract(device_addr, size);
}
memory_tracker.MarkRegionAsCpuModified(device_addr, size);
+ MarkVertexBuffersStale(device_addr, size);
}
template <class P>
@@ -186,6 +201,7 @@ void BufferCache<P>::CachedWriteMemory(DAddr device_addr, u64 size) {
return;
}
+ Nx2NoteListHostWrite(device_addr, size);
tmp_buffer.resize_destructive(size);
device_memory.ReadBlockUnsafe(device_addr, tmp_buffer.data(), size);
@@ -220,8 +236,17 @@ std::optional<VideoCore::RasterizerDownloadArea> BufferCache<P>::GetFlushArea(DA
};
area->preemtive = !IsRegionGpuModified(device_addr_start_aligned,
device_addr_end_aligned - device_addr_start_aligned);
- memory_tracker.MarkRegionAsPreflushable(device_addr_start_aligned,
- device_addr_end_aligned - device_addr_start_aligned);
+ // Mark only data that the GPU wrote. A mark stays for the session and sends later GPU writes
+ // in the range to a download at each fence.
+ if (!area->preemtive) {
+ memory_tracker.MarkRegionAsPreflushable(device_addr_start_aligned,
+ device_addr_end_aligned - device_addr_start_aligned);
+ } else {
+ // The core keeps this area and has no mark. The next GPU write here must arm the range
+ // again, which ends the area and sets the trap.
+ read_trapped_ranges.Subtract(device_addr_start_aligned,
+ device_addr_end_aligned - device_addr_start_aligned);
+ }
return area;
}
@@ -255,6 +280,7 @@ bool BufferCache<P>::DMACopy(GPUVAddr src_address, GPUVAddr dest_address, u64 am
}
ClearDownload(*cpu_dest_address, amount);
+ MarkVertexBuffersStale(*cpu_dest_address, amount);
BufferId buffer_a;
BufferId buffer_b;
@@ -291,6 +317,10 @@ bool BufferCache<P>::DMACopy(GPUVAddr src_address, GPUVAddr dest_address, u64 am
const bool has_new_downloads = tmp_intervals.size() != 0;
for (const auto& pair : tmp_intervals) {
gpu_modified_ranges.Add(pair.first, pair.second);
+ if (Tegra::g_nx2_ampere.load(std::memory_order_relaxed)) {
+ Tegra::Nx2MarkGpuWritten(pair.first, pair.second);
+ Nx2NoteListWrite(buffer_b, pair.first, pair.second);
+ }
}
const auto& copy = copies[0];
src_buffer.MarkUsage(copy.src_offset, copy.size);
@@ -379,6 +409,10 @@ template <class P>
void BufferCache<P>::BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr gpu_addr,
u32 size) {
const std::optional<DAddr> device_addr = gpu_memory->GpuToCpuAddress(gpu_addr);
+ if (!device_addr) {
+ channel_state->uniform_buffers[stage][index] = NULL_BINDING;
+ return;
+ }
const Binding binding{
.device_addr = *device_addr,
.size = size,
@@ -389,7 +423,12 @@ void BufferCache<P>::BindGraphicsUniformBuffer(size_t stage, u32 index, GPUVAddr
template <class P>
void BufferCache<P>::DisableGraphicsUniformBuffer(size_t stage, u32 index) {
- channel_state->uniform_buffers[stage][index] = NULL_BINDING;
+ auto& current = channel_state->uniform_buffers[stage][index];
+ if (current.device_addr == 0 && current.size == 0 && current.buffer_id == NULL_BUFFER_ID)
+ [[likely]] {
+ return;
+ }
+ current = NULL_BINDING;
}
template <class P>
@@ -871,7 +910,39 @@ void BufferCache<P>::UpdateVertexBufferSlot(u32 index, const Binding& binding) {
}
template <class P>
+void BufferCache<P>::MarkVertexBuffersStale(DAddr device_addr, u64 size) {
+ if constexpr (requires { runtime.IsVertexRepacked(u32{}); }) {
+ u32 mask = enabled_vertex_buffers_mask;
+ while (mask != 0) {
+ const u32 index = std::countr_zero(mask);
+ mask &= mask - 1;
+ const Binding& binding = v_buffer[index];
+ if (device_addr < binding.device_addr + binding.size &&
+ binding.device_addr < device_addr + size) {
+ stale_vertex_buffers_mask |= 1U << index;
+ }
+ }
+ }
+}
+
+template <class P>
+void BufferCache<P>::RefreshRepackedVertexBuffers() {
+ if constexpr (requires { runtime.IsVertexRepacked(u32{}); }) {
+ u32 mask = std::exchange(stale_vertex_buffers_mask, 0) & enabled_vertex_buffers_mask;
+ auto& flags = maxwell3d->dirty.flags;
+ while (mask != 0) {
+ const u32 index = std::countr_zero(mask);
+ mask &= mask - 1;
+ if (runtime.IsVertexRepacked(index)) {
+ flags[Dirty::VertexBuffer0 + index] = true;
+ }
+ }
+ }
+}
+
+template <class P>
void BufferCache<P>::BindHostVertexBuffers() {
+ RefreshRepackedVertexBuffers();
#ifdef __ANDROID__
const bool use_optimized_vertex_buffers = Settings::values.use_optimized_vertex_buffers.GetValue();
@@ -994,6 +1065,26 @@ void BufferCache<P>::BindHostGraphicsUniformBuffers(size_t stage) {
template <class P>
void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32 binding_index, bool needs_bind) {
++channel_state->uniform_cache_shots[0];
+ // Bank 24 (root table 0, NVN2's push constants) is read from bank 17, which such games do not
+ // bind: stream the table's current words as that buffer.
+ if (index == 17 && maxwell3d->root_tables.loaded && channel_state->uniform_buffers[stage][index].size == 0) {
+ const auto& words = maxwell3d->root_tables.tables[0];
+ const u32 size = (*channel_state->uniform_buffer_sizes)[stage][index];
+ const u32 copied = (std::min)(size, static_cast<u32>(sizeof(words)));
+ if constexpr (IS_OPENGL) {
+ std::array<u8, sizeof(words)> data{};
+ std::memcpy(data.data(), words.data(), copied);
+ runtime.BindFastUniformBuffer(stage, binding_index, copied);
+ runtime.PushFastUniformBuffer(stage, binding_index, std::span<const u8>(data.data(), copied));
+ } else {
+ const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size);
+ std::memset(span.data(), 0, span.size());
+ std::memcpy(span.data(), words.data(), copied);
+ }
+ channel_state->fast_bound_uniform_buffers[stage] |= 1u << binding_index;
+ channel_state->uniform_buffer_binding_sizes[stage][binding_index] = size;
+ return;
+ }
const Binding& binding = channel_state->uniform_buffers[stage][index];
const DAddr device_addr = binding.device_addr;
const u32 size = (std::min)(binding.size, (*channel_state->uniform_buffer_sizes)[stage][index]);
@@ -1036,8 +1127,20 @@ void BufferCache<P>::BindHostGraphicsUniformBuffer(size_t stage, u32 index, u32
channel_state->fast_bound_uniform_buffers[stage] |= 1u << binding_index;
channel_state->uniform_buffer_binding_sizes[stage][binding_index] = size;
// Stream buffer path to avoid stalling on non-Nvidia drivers or Vulkan
- const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size);
- device_memory.ReadBlockUnsafe(device_addr, span.data(), size);
+ bool streamed = false;
+ if constexpr (!IS_OPENGL && VideoCommon::StreamUniformReusable<Runtime>) {
+ // The same bytes as this slot's previous bind, in the same command buffer: bind that
+ // stream allocation again instead of allocating and copying (uniform_stream_reuse.h).
+ // EDEN_NX2_UBOREUSE=0 turns it off.
+ if (VideoCommon::UniformStreamReuseEnabled()) {
+ streamed = VideoCommon::BindSmallUniformReusing(
+ runtime, device_memory, stage, index, binding_index, device_addr, size);
+ }
+ }
+ if (!streamed) {
+ const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size);
+ device_memory.ReadBlockUnsafe(device_addr, span.data(), size);
+ }
return;
}
// Classic cached path
@@ -1494,6 +1597,9 @@ void BufferCache<P>::UpdateDrawIndirect() {
template <class P>
void BufferCache<P>::UpdateUniformBuffers(size_t stage) {
+ if (channel_state->enabled_uniform_buffer_masks[stage] == 0) [[likely]] {
+ return;
+ }
ForEachEnabledBit(channel_state->enabled_uniform_buffer_masks[stage], [&](u32 index) {
Binding& binding = channel_state->uniform_buffers[stage][index];
if (binding.buffer_id) {
@@ -1511,6 +1617,9 @@ void BufferCache<P>::UpdateUniformBuffers(size_t stage) {
template <class P>
void BufferCache<P>::UpdateStorageBuffers(size_t stage) {
+ if (channel_state->enabled_storage_buffers[stage] == 0) [[likely]] {
+ return;
+ }
ForEachEnabledBit(channel_state->enabled_storage_buffers[stage], [&](u32 index) {
// Resolve buffer
Binding& binding = channel_state->storage_buffers[stage][index];
@@ -1523,6 +1632,9 @@ void BufferCache<P>::UpdateStorageBuffers(size_t stage) {
template <class P>
void BufferCache<P>::UpdateTextureBuffers(size_t stage) {
+ if (channel_state->enabled_texture_buffers[stage] == 0) [[likely]] {
+ return;
+ }
ForEachEnabledBit(channel_state->enabled_texture_buffers[stage], [&](u32 index) {
Binding& binding = channel_state->texture_buffers[stage][index];
binding.buffer_id = FindBuffer(binding.device_addr, binding.size, false);
@@ -1604,20 +1716,212 @@ void BufferCache<P>::MarkWrittenBuffer(BufferId buffer_id, DAddr device_addr, u3
}
}
memory_tracker.MarkRegionAsGpuModified(device_addr, size);
+ MarkVertexBuffersStale(device_addr, size);
gpu_modified_ranges.Add(device_addr, size);
if (DownloadsAtFence(device_addr, size)) {
uncommitted_gpu_modified_ranges.Add(device_addr, size);
} else {
ArmReadTraps(device_addr, size);
}
+ if (Tegra::g_nx2_ampere.load(std::memory_order_relaxed)) {
+ Tegra::Nx2MarkGpuWritten(device_addr, size);
+ Nx2NoteListWrite(buffer_id, device_addr, size);
+ }
+}
+
+template <class P>
+void BufferCache<P>::Nx2NoteListWrite(BufferId buffer_id, DAddr device_addr, u64 size) {
+ if constexpr (!IS_OPENGL) {
+ if (size == 0 || !Tegra::g_nx2_has_command_list_pages.load(std::memory_order_relaxed)) {
+ return;
+ }
+ constexpr u64 page_bits = Tegra::Nx2WrittenPageBits;
+ const u64 last = (std::min<u64>)((device_addr + size - 1) >> page_bits,
+ Tegra::g_nx2_command_list_pages.size() * 64 - 1);
+ for (u64 page = device_addr >> page_bits; page <= last;) {
+ // Storage buffers can be large: skip 64 pages at a time where none is a list page.
+ const u64 word =
+ Tegra::g_nx2_command_list_pages[page / 64].load(std::memory_order_relaxed) >>
+ (page % 64);
+ if (word == 0) {
+ page = (page / 64 + 1) * 64;
+ continue;
+ }
+ if (word & 1) {
+ const DAddr page_addr = page << page_bits;
+ {
+ std::scoped_lock lock{nx2_list_mutex};
+ Nx2ListPage& entry = nx2_list_pages[page_addr];
+ ++entry.writes;
+ if (!nx2_collect_list_copies && !entry.draw_written) {
+ entry.draw_written = true;
+ LOG_INFO(HW_GPU, "Command list page {:#x} is written by draws too: its "
+ "lists drain the split",
+ page_addr);
+ }
+ }
+ if (nx2_collect_list_copies) {
+ nx2_list_copy_requests.emplace_back(buffer_id, page_addr);
+ }
+ }
+ ++page;
+ }
+ }
+}
+
+template <class P>
+bool BufferCache<P>::Nx2CopyListPages(u64 tick) {
+ if constexpr (IS_OPENGL) {
+ return false;
+ } else {
+ if (nx2_list_copy_requests.empty()) {
+ return false;
+ }
+ std::ranges::sort(nx2_list_copy_requests, {}, &std::pair<BufferId, DAddr>::second);
+ constexpr DAddr page_size = DAddr{1} << Tegra::Nx2WrittenPageBits;
+ bool recorded = false;
+ for (size_t i = 0; i < nx2_list_copy_requests.size(); ++i) {
+ const auto [buffer_id, page] = nx2_list_copy_requests[i];
+ if (i + 1 < nx2_list_copy_requests.size() &&
+ nx2_list_copy_requests[i + 1].second == page) {
+ continue;
+ }
+ Buffer& buffer = slot_buffers[buffer_id];
+ const DAddr begin = (std::max<DAddr>)(page, buffer.CpuAddr());
+ const DAddr end =
+ (std::min<DAddr>)(page + page_size, buffer.CpuAddr() + buffer.SizeBytes());
+ if (begin >= end) {
+ continue;
+ }
+ Nx2Staging staging = runtime.DownloadStagingBuffer(page_size, true);
+ const BufferCopy copy{
+ .src_offset = buffer.Offset(begin),
+ .dst_offset = staging.offset + (begin - page),
+ .size = end - begin,
+ };
+ buffer.MarkUsage(copy.src_offset, copy.size);
+ runtime.CopyBuffer(staging.buffer, buffer, std::span{&copy, 1}, true);
+ recorded = true;
+ std::scoped_lock lock{nx2_list_mutex};
+ Nx2ListPage& entry = nx2_list_pages[page];
+ if (entry.copy) {
+ nx2_retired_list_copies.emplace_back(entry.copy_tick, std::move(*entry.copy));
+ }
+ entry.copy = staging;
+ entry.copy_writes = entry.writes;
+ entry.copy_tick = tick;
+ entry.copy_begin = static_cast<u32>(begin - page);
+ entry.copy_end = static_cast<u32>(end - page);
+ }
+ nx2_list_copy_requests.clear();
+ std::erase_if(nx2_retired_list_copies, [this](auto& retired) {
+ if (!runtime.IsFree(retired.first)) {
+ return false;
+ }
+ runtime.FreeDeferredStagingBuffer(retired.second);
+ return true;
+ });
+ return recorded;
+ }
+}
+
+template <class P>
+auto BufferCache<P>::Nx2ReadListCopy(DAddr device_addr, std::span<u8> out) -> Nx2ListRead {
+ if constexpr (IS_OPENGL) {
+ return Nx2ListRead::Unknown;
+ } else {
+ constexpr DAddr page_size = DAddr{1} << Tegra::Nx2WrittenPageBits;
+ const DAddr page = device_addr & ~(page_size - 1);
+ const u64 offset = device_addr - page;
+ if (offset + out.size() > page_size) {
+ return Nx2ListRead::Unknown;
+ }
+ const auto current = [&](const Nx2ListPage& entry) {
+ return entry.copy && !entry.draw_written && entry.copy_writes == entry.writes &&
+ offset >= entry.copy_begin && offset + out.size() <= entry.copy_end;
+ };
+ u64 tick{};
+ u64 writes{};
+ {
+ std::scoped_lock lock{nx2_list_mutex};
+ const auto it = nx2_list_pages.find(page);
+ if (it == nx2_list_pages.end()) {
+ return Nx2ListRead::Unknown;
+ }
+ const Nx2ListPage& entry = it->second;
+ // Pages the CPU writes the lists of (GPU-written once, long before) stay synced.
+ if (!entry.draw_written && entry.synced_writes == entry.writes) {
+ return Nx2ListRead::Synced;
+ }
+ if (!current(entry)) {
+ return Nx2ListRead::Unknown;
+ }
+ // The lists after the first of a batch find the copy done: read it at once.
+ if (runtime.IsFree(entry.copy_tick)) {
+ std::memcpy(out.data(), entry.copy->mapped_span.data() + offset, out.size());
+ return Nx2ListRead::Copied;
+ }
+ tick = entry.copy_tick;
+ writes = entry.copy_writes;
+ }
+ // Submitted with the work that wrote the page (Nx2CopyListPages's caller flushes).
+ runtime.WaitSubmittedTick(tick);
+ std::scoped_lock lock{nx2_list_mutex};
+ const auto it = nx2_list_pages.find(page);
+ if (it == nx2_list_pages.end() || !current(it->second) || it->second.copy_tick != tick ||
+ it->second.copy_writes != writes) {
+ return Nx2ListRead::Unknown;
+ }
+ std::memcpy(out.data(), it->second.copy->mapped_span.data() + offset, out.size());
+ return Nx2ListRead::Copied;
+ }
+}
+
+template <class P>
+void BufferCache<P>::Nx2NoteListSynced(DAddr page) {
+ if constexpr (!IS_OPENGL) {
+ std::scoped_lock lock{nx2_list_mutex};
+ Nx2ListPage& entry = nx2_list_pages[page];
+ entry.synced_writes = entry.writes;
+ }
+}
+
+template <class P>
+void BufferCache<P>::Nx2NoteListHostWrite(DAddr device_addr, u64 size) {
+ if constexpr (!IS_OPENGL) {
+ if (size == 0 || !Tegra::g_nx2_has_command_list_pages.load(std::memory_order_relaxed)) {
+ return;
+ }
+ constexpr u64 page_bits = Tegra::Nx2WrittenPageBits;
+ const u64 last = (std::min<u64>)((device_addr + size - 1) >> page_bits,
+ Tegra::g_nx2_command_list_pages.size() * 64 - 1);
+ for (u64 page = device_addr >> page_bits; page <= last; ++page) {
+ if (!((Tegra::g_nx2_command_list_pages[page / 64].load(std::memory_order_relaxed) >>
+ (page % 64)) &
+ 1)) {
+ continue;
+ }
+ std::scoped_lock lock{nx2_list_mutex};
+ const auto it = nx2_list_pages.find(page << page_bits);
+ if (it == nx2_list_pages.end()) {
+ continue;
+ }
+ Nx2ListPage& entry = it->second;
+ const bool synced = entry.synced_writes == entry.writes;
+ ++entry.writes;
+ if (synced) {
+ entry.synced_writes = entry.writes;
+ }
+ }
+ }
}
template <class P>
BufferId BufferCache<P>::FindBuffer(DAddr device_addr, u32 size, bool sparse_compatible) {
- if (device_addr == 0) {
+ const u64 page = device_addr >> CACHING_PAGEBITS;
+ if (device_addr == 0 || page >= page_table.size()) {
return NULL_BUFFER_ID;
}
- const u64 page = device_addr >> CACHING_PAGEBITS;
const BufferId buffer_id = page_table[page];
if (buffer_id) {
Buffer& buffer = slot_buffers[buffer_id];
@@ -1927,6 +2231,11 @@ void BufferCache<P>::MappedUploadMemory([[maybe_unused]] Buffer& buffer,
u8* const src_pointer = staging_pointer.data() + copy.src_offset;
const DAddr device_addr = buffer.CpuAddr() + copy.dst_offset;
device_memory.ReadBlockUnsafe(device_addr, src_pointer, copy.size);
+ // Large uploads are mostly geometry the host GPU now has its own copy of: TickFrame
+ // marks their guest memory cold (see TextureCache::RefreshContents).
+ if (copy.size >= 256_KiB) {
+ uploaded_buffer_ranges.emplace_back(device_addr, copy.size);
+ }
// Apply the staging offset
copy.src_offset += upload_staging.offset;
}
@@ -1958,6 +2267,7 @@ void BufferCache<P>::InlineMemoryImplementation(DAddr dest_address, size_t copy_
std::span<const u8> inlined_buffer) {
ClearDownload(dest_address, copy_size);
gpu_modified_ranges.Subtract(dest_address, copy_size);
+ MarkVertexBuffersStale(dest_address, copy_size);
BufferId buffer_id = FindBuffer(dest_address, static_cast<u32>(copy_size), false);
auto& buffer = slot_buffers[buffer_id];
diff --git a/src/video_core/buffer_cache/buffer_cache_base.h b/src/video_core/buffer_cache/buffer_cache_base.h
index d8a61b7913..86c05c8f22 100644
--- a/src/video_core/buffer_cache/buffer_cache_base.h
+++ b/src/video_core/buffer_cache/buffer_cache_base.h
@@ -14,7 +14,9 @@
#include <memory>
#include <mutex>
#include <numeric>
+#include <optional>
#include <span>
+#include <unordered_map>
#include <vector>
#include "common/container/unordered_map.h"
@@ -57,7 +59,8 @@ constexpr u32 NUM_VERTEX_BUFFERS = 32;
constexpr u32 NUM_TRANSFORM_FEEDBACK_BUFFERS = 4;
constexpr u32 NUM_GRAPHICS_UNIFORM_BUFFERS = 18;
constexpr u32 NUM_COMPUTE_UNIFORM_BUFFERS = 8;
-constexpr u32 NUM_STORAGE_BUFFERS = 16;
+// Native compute shaders bind up to 18 global memory ranges; the enable masks are 32 bits wide.
+constexpr u32 NUM_STORAGE_BUFFERS = 32;
constexpr u32 NUM_TEXTURE_BUFFERS = 32;
constexpr u32 NUM_STAGES = 5;
@@ -321,6 +324,28 @@ public:
bool DMAClear(GPUVAddr src_address, u64 amount, u32 value);
+ /// Switch 2 GPU-written command lists (nx2.h, g_nx2_command_list_pages). While collecting (a
+ /// compute dispatch or a DMA copy being recorded), writes to command list pages are noted;
+ /// Nx2CopyListPages then records copies of those pages into host memory after the work.
+ void Nx2CollectListCopies(bool collect) {
+ nx2_collect_list_copies = collect;
+ if (!collect) {
+ nx2_list_copy_requests.clear();
+ }
+ }
+ /// True when it recorded copies, which the GPU completes at `tick` (the caller submits).
+ bool Nx2CopyListPages(u64 tick);
+ enum class Nx2ListRead { Unknown, Copied, Synced };
+ /// Front thread: Synced when guest memory holds the page `device_addr` lies in (flushed and
+ /// written by no GPU work since), else Copied with `out` filled from the copy of its last
+ /// write once the GPU is done with it. Unknown otherwise: the caller drains and flushes.
+ Nx2ListRead Nx2ReadListCopy(DAddr device_addr, std::span<u8> out);
+ /// Front thread, the split drained: the command list page was flushed to guest memory.
+ void Nx2NoteListSynced(DAddr page);
+ /// CPU and inline writes: they reach guest memory, so a synced page stays synced, but its copy
+ /// no longer holds the page.
+ void Nx2NoteListHostWrite(DAddr device_addr, u64 size);
+
/// Return true when a CPU region is modified from the GPU
[[nodiscard]] bool IsRegionGpuModified(DAddr addr, size_t size);
@@ -440,6 +465,10 @@ private:
void MarkWrittenBuffer(BufferId buffer_id, DAddr device_addr, u32 size, bool needs_sync = false);
+ /// Counts a GPU write to the command list pages in the range (invalidating their copies) and,
+ /// while collecting, asks for new copies.
+ void Nx2NoteListWrite(BufferId buffer_id, DAddr device_addr, u64 size);
+
[[nodiscard]] BufferId FindBuffer(DAddr device_addr, u32 size, bool sparse_compatible);
void SynchronizeBufferWrites(Buffer& buffer);
@@ -497,6 +526,13 @@ private:
void UpdateVertexBufferSlot(u32 index, const Binding& binding);
+ /// Flags the vertex buffers overlapping a written range so host copies derived from them are
+ /// rebuilt.
+ void MarkVertexBuffersStale(DAddr device_addr, u64 size);
+
+ /// Re-dirties repacked vertex buffers whose source data was written since they were bound.
+ void RefreshRepackedVertexBuffers();
+
void ClearDownload(DAddr base_addr, u64 size);
void InlineMemoryImplementation(DAddr dest_address, size_t copy_size,
@@ -517,6 +553,7 @@ private:
u32 last_index_count = 0;
u32 enabled_vertex_buffers_mask = 0;
+ u32 stale_vertex_buffers_mask = 0;
u64 vertex_buffers_serial = 0;
std::array<Binding, 32> v_buffer{};
@@ -580,6 +617,8 @@ private:
size_t immediate_buffer_capacity = 0;
Common::ScratchBuffer<u8> immediate_buffer_alloc;
+ /// Guest memory large uploads came from since the last cold hint (device address, size).
+ std::vector<std::pair<DAddr, u64>> uploaded_buffer_ranges;
struct LRUItemParams {
using ObjectType = BufferId;
@@ -602,6 +641,29 @@ private:
// Covers the device address space (MaxwellDeviceTraits::device_virtual_bits).
std::array<BufferId, ((1ULL << 36) >> CACHING_PAGEBITS)> page_table;
Common::ScratchBuffer<u8> tmp_buffer;
+
+ /// A command list page (Nx2CopyListPages): the GPU writes noted to it and the last copy.
+ using Nx2Staging = decltype(std::declval<Runtime&>().DownloadStagingBuffer(size_t{}, true));
+ struct Nx2ListPage {
+ u64 writes{}; ///< GPU writes noted to the page
+ u64 copy_writes{}; ///< `writes` when the copy was made: the copy is current while equal
+ u64 copy_tick{}; ///< GPU tick that completes the copy
+ /// `writes` when the front last flushed the page: guest memory holds it while equal
+ u64 synced_writes{~u64{0}};
+ u32 copy_begin{}; ///< Bytes of the page the copy holds (a buffer can end inside it)
+ u32 copy_end{};
+ /// Written outside a compute dispatch or a DMA copy (a draw's storage writes) at least
+ /// once: the front can't tell from what it sent whether such a write is still queued, so
+ /// the page's lists always drain the split instead of reading a copy.
+ bool draw_written{};
+ std::optional<Nx2Staging> copy;
+ };
+ std::mutex nx2_list_mutex; ///< nx2_list_pages: the split's front reads the copies
+ std::unordered_map<DAddr, Nx2ListPage> nx2_list_pages;
+ /// Copies replaced while the GPU may still write them: freed once it is past their tick.
+ std::vector<std::pair<u64, Nx2Staging>> nx2_retired_list_copies;
+ boost::container::small_vector<std::pair<BufferId, DAddr>, 8> nx2_list_copy_requests;
+ bool nx2_collect_list_copies{};
};
} // namespace VideoCommon
diff --git a/src/video_core/buffer_cache/uniform_stream_reuse.h b/src/video_core/buffer_cache/uniform_stream_reuse.h
new file mode 100644
index 0000000000..7ba31c4717
--- /dev/null
+++ b/src/video_core/buffer_cache/uniform_stream_reuse.h
@@ -0,0 +1,121 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <array>
+#include <concepts>
+#include <cstddef>
+#include <cstdlib>
+#include <cstring>
+#include <span>
+#include <type_traits>
+
+#include "common/common_types.h"
+
+namespace VideoCommon {
+
+/// What a buffer cache runtime offers for stream uniform reuse.
+/// - StreamUniformRef: a small trivially copyable description of one stream allocation (the
+/// host buffer, its offset and whatever else the runtime needs to add it to a descriptor
+/// payload again, e.g. the device address of the descriptor buffer path).
+/// - LastStreamUniformRef(): the allocation made by the latest BindMappedUniformBuffer call.
+/// - RebindStreamUniform(ref, size): adds `ref` to the descriptor payload exactly as
+/// BindMappedUniformBuffer added the allocation the first time.
+template <class R>
+concept StreamUniformReusable = requires(R& runtime, u32 size) {
+ typename R::StreamUniformRef;
+ requires std::is_trivially_copyable_v<typename R::StreamUniformRef>;
+ { runtime.LastStreamUniformRef() } -> std::same_as<typename R::StreamUniformRef>;
+ runtime.RebindStreamUniform(runtime.LastStreamUniformRef(), size);
+ { runtime.CurrentTick() } -> std::convertible_to<u64>;
+};
+
+/// EDEN_NX2_UBOREUSE=0 turns the reuse off (every small uniform buffer is copied, as before).
+[[nodiscard]] inline bool UniformStreamReuseEnabled() noexcept {
+ static const bool enabled = [] {
+ const char* const value = std::getenv("EDEN_NX2_UBOREUSE");
+ return value == nullptr || value[0] != '0';
+ }();
+ return enabled;
+}
+
+template <class Ref>
+class UniformStreamReuse {
+public:
+ static constexpr u32 MaxBytes = 4096; // BufferCache's DEFAULT_SKIP_CACHE_SIZE
+ static constexpr size_t NumStages = 5;
+ static constexpr size_t NumSlots = 18; // Maxwell constant buffers per stage
+
+ struct Slot {
+ Ref ref{};
+ u64 tick{};
+ u32 size{}; // 0: nothing streamed yet
+ std::array<u8, MaxBytes> bytes{};
+ };
+
+ void Reset() noexcept {
+ for (auto& stage : slots) {
+ for (Slot& slot : stage) {
+ slot.size = 0;
+ slot.tick = 0;
+ }
+ }
+ }
+
+ std::array<std::array<Slot, NumSlots>, NumStages> slots{};
+ std::array<u8, MaxBytes> scratch{};
+};
+
+template <class Ref>
+[[nodiscard]] inline UniformStreamReuse<Ref>& UniformStreamReuseState() noexcept {
+ static UniformStreamReuse<Ref> state;
+ return state;
+}
+
+/// Forgets every slot. Call it when a buffer cache (and so its runtime, scheduler and stream
+/// buffer) is created: ticks restart with a new scheduler and the old references are dead.
+template <class Runtime>
+inline void ResetUniformStreamReuse() noexcept {
+ if constexpr (StreamUniformReusable<Runtime>) {
+ UniformStreamReuseState<typename Runtime::StreamUniformRef>().Reset();
+ }
+}
+
+/// Binds the small uniform buffer at `device_addr` for (stage, cbuf `index`) through the stream
+/// buffer, reusing the slot's previous allocation when the bytes and the command buffer are the
+/// same. Returns false (nothing bound) when the buffer does not qualify, so the caller binds it
+/// the classic way.
+template <class Runtime, class DeviceMemory>
+[[nodiscard]] bool BindSmallUniformReusing(Runtime& runtime, DeviceMemory& device_memory,
+ size_t stage, u32 index, u32 binding_index,
+ u64 device_addr, u32 size) {
+ using Reuse = UniformStreamReuse<typename Runtime::StreamUniformRef>;
+ if (size == 0 || size > Reuse::MaxBytes || stage >= Reuse::NumStages ||
+ index >= Reuse::NumSlots) {
+ return false;
+ }
+ Reuse& reuse = UniformStreamReuseState<typename Runtime::StreamUniformRef>();
+ typename Reuse::Slot& slot = reuse.slots[stage][index];
+ u8* const scratch = reuse.scratch.data();
+ device_memory.ReadBlockUnsafe(device_addr, scratch, size);
+
+ const bool same_bytes =
+ slot.size == size && std::memcmp(slot.bytes.data(), scratch, size) == 0;
+ if (same_bytes && slot.tick == runtime.CurrentTick()) {
+ runtime.RebindStreamUniform(slot.ref, size);
+ return true;
+ }
+ const std::span<u8> span = runtime.BindMappedUniformBuffer(stage, binding_index, size);
+ std::memcpy(span.data(), scratch, size);
+ slot.ref = runtime.LastStreamUniformRef();
+ // After the allocation: the tick the stream region is tagged with is the one current now.
+ slot.tick = runtime.CurrentTick();
+ if (!same_bytes) {
+ slot.size = size;
+ std::memcpy(slot.bytes.data(), scratch, size);
+ }
+ return true;
+}
+
+} // namespace VideoCommon
diff --git a/src/video_core/control/scheduler.cpp b/src/video_core/control/scheduler.cpp
index dc42906752..97d2dcd482 100644
--- a/src/video_core/control/scheduler.cpp
+++ b/src/video_core/control/scheduler.cpp
@@ -48,6 +48,7 @@ void Scheduler::Push(GPU& gpu, s32 channel, CommandList&& entries) {
std::find(pending_channels.begin(), pending_channels.end(), channel_state) ==
pending_channels.end()) {
pending_channels.push_back(channel_state);
+ any_pending.store(true, std::memory_order_release);
}
if (!pending_channels.empty()) {
ResumePending(gpu);
@@ -77,6 +78,7 @@ void Scheduler::ResumePending(GPU& gpu) {
std::erase_if(pending_channels, [](const auto& state) {
return !state->payload->dma_pusher.HasPendingAcquire();
});
+ any_pending.store(!pending_channels.empty(), std::memory_order_release);
}
void Scheduler::DeclareChannel(std::shared_ptr<ChannelState> new_channel) {
diff --git a/src/video_core/control/scheduler.h b/src/video_core/control/scheduler.h
index 9a0f1b919f..b1aaf46b60 100644
--- a/src/video_core/control/scheduler.h
+++ b/src/video_core/control/scheduler.h
@@ -6,6 +6,7 @@
#pragma once
+#include <atomic>
#include <memory>
#include <mutex>
#include <vector>
@@ -30,6 +31,10 @@ public:
[[nodiscard]] bool HasPendingChannels() const {
return !pending_channels.empty();
}
+ /// HasPendingChannels for threads other than the GPU thread.
+ [[nodiscard]] bool HasPendingChannelsAnyThread() const {
+ return any_pending.load(std::memory_order_acquire);
+ }
void DeclareChannel(std::shared_ptr<ChannelState> new_channel);
@@ -39,6 +44,7 @@ private:
::Common::unordered_map<s32, std::shared_ptr<ChannelState>> channels;
std::mutex scheduling_guard;
std::vector<std::shared_ptr<ChannelState>> pending_channels; ///< Suspended on an acquire
+ std::atomic<bool> any_pending{};
};
} // namespace Control
diff --git a/src/video_core/dirty_flags.cpp b/src/video_core/dirty_flags.cpp
index e6856aaca3..2f359749aa 100644
--- a/src/video_core/dirty_flags.cpp
+++ b/src/video_core/dirty_flags.cpp
@@ -64,6 +64,8 @@ void SetupDirtyRenderTargets(Maxwell3D::DirtyState::Tables& tables) {
void SetupDirtyShaders(Maxwell3D::DirtyState::Tables& tables) {
FillBlock(tables[0], OFF(pipelines), NUM(pipelines[0]) * Maxwell3D::Regs::MaxShaderProgram,
Shaders);
+ // Task and mesh programs are translated with the mesh registers.
+ FillBlock(tables[1], Maxwell3D::Regs::MeshControl, 4, Shaders);
}
} // Anonymous namespace
diff --git a/src/video_core/dma_pusher.cpp b/src/video_core/dma_pusher.cpp
index 88d571ae5f..6a79e0570e 100644
--- a/src/video_core/dma_pusher.cpp
+++ b/src/video_core/dma_pusher.cpp
@@ -9,6 +9,7 @@
#include <cstdlib>
#include <cstdio>
+#include "common/scope_exit.h"
#include "common/settings.h"
#include "core/core.h"
#include "core/memory.h"
@@ -155,13 +156,39 @@ bool DmaPusher::Step() {
// (the split only starts with unsafe reads; the settings can change mid-game).
const bool use_safe = !VideoCommon::GpuSplit::Get().Active() &&
(Settings::IsDMALevelDefault() ? Settings::IsGPULevelHigh() : Settings::IsDMALevelSafe());
- if (use_safe) {
+ // The words as submitted, unless this is a resumed entry or the GPU has newer ones the
+ // safe read below would flush (with the split, the front can't tell: it reads memory
+ // unflushed anyway). Entries on pages the GPU writes (culling) always take the flushing
+ // read: their words at submit time are an older frame's.
+ const bool submitted = command_resume_offset == 0 &&
+ dma_pushbuffer_subindex < command_list.submitted_offsets.size() &&
+ (command_list.snapshot_suspensions & 1) == 0 &&
+ command_list.snapshot_suspensions ==
+ suspensions.load(std::memory_order_acquire) &&
+ !IsGpuWrittenCommands(dma_state.dma_get,
+ size_t{header.size} * sizeof(u32)) &&
+ !(use_safe && memory_manager.IsMemoryDirty(dma_state.dma_get,
+ header.size * sizeof(u32)));
+ if (submitted) {
+ const std::span<const CommandHeader> commands{
+ command_list.submitted_words.data() +
+ command_list.submitted_offsets[dma_pushbuffer_subindex],
+ header.size};
+ RecordParsedEntry(header, commands);
+ command_resume_offset += ProcessCommands(commands, 0);
+ } else if (use_safe) {
Tegra::Memory::GpuGuestMemory<Tegra::CommandHeader, Tegra::Memory::GuestMemoryFlags::SafeRead>headers(memory_manager, dma_state.dma_get, header.size, &command_headers);
const std::span<const CommandHeader> commands{headers.data(), headers.size()};
RecordParsedEntry(header, commands);
command_resume_offset +=
ProcessCommands(commands.subspan(command_resume_offset), command_resume_offset);
+ } else if (ReadGpuWrittenCommands(dma_state.dma_get, static_cast<u32>(header.size))) {
+ const std::span<const CommandHeader> commands{command_headers.data(), header.size};
+ RecordParsedEntry(header, commands);
+ command_resume_offset +=
+ ProcessCommands(commands.subspan(command_resume_offset), command_resume_offset);
} else {
+ FlushGpuWrittenCommands(dma_state.dma_get, size_t{header.size} * sizeof(u32));
Tegra::Memory::GpuGuestMemory<Tegra::CommandHeader, Tegra::Memory::GuestMemoryFlags::UnsafeRead>headers(memory_manager, dma_state.dma_get, header.size, &command_headers);
const std::span<const CommandHeader> commands{headers.data(), headers.size()};
RecordParsedEntry(header, commands);
@@ -364,6 +391,15 @@ void DmaPusher::CallMethod(u32 argument) {
ReportUnboundSubchannel();
return;
}
+ // Compute dispatches and DMA copies write the command lists ReadGpuWrittenCommands reads.
+ const auto engine_type = subchannel_type[dma_state.subchannel];
+ const bool writer = engine_type == Engines::EngineTypes::KeplerCompute ||
+ engine_type == Engines::EngineTypes::MaxwellDMA;
+ SCOPE_EXIT {
+ if (writer) {
+ split.NoteWriter();
+ }
+ };
if (split.Active()) {
const GPUVAddr segment = dma_state.dma_get + dma_state.dma_word_offset;
if (split.FrontMacros()) {
@@ -414,6 +450,15 @@ void DmaPusher::CallMultiMethod(const u32* base_start, u32 num_methods) {
ReportUnboundSubchannel();
return;
}
+ // Compute dispatches and DMA copies write the command lists ReadGpuWrittenCommands reads.
+ const auto engine_type = subchannel_type[dma_state.subchannel];
+ const bool writer = engine_type == Engines::EngineTypes::KeplerCompute ||
+ engine_type == Engines::EngineTypes::MaxwellDMA;
+ SCOPE_EXIT {
+ if (writer) {
+ split.NoteWriter();
+ }
+ };
if (split.Active()) {
const GPUVAddr segment = dma_state.dma_get + dma_state.dma_word_offset;
if (split.FrontMacros()) {
@@ -445,6 +490,102 @@ void DmaPusher::CallMultiMethod(const u32* base_start, u32 num_methods) {
}
}
+bool DmaPusher::IsGpuWrittenCommands(GPUVAddr address, size_t size) {
+ if (!Tegra::g_nx2_ampere.load(std::memory_order_relaxed) || size == 0) {
+ return false;
+ }
+ for (GPUVAddr page = address & ~GPUVAddr{0xfff}; page < address + size; page += 0x1000) {
+ if (const auto device = memory_manager.GpuToCpuAddress(page);
+ device && Tegra::Nx2IsGpuWritten(*device)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool DmaPusher::ReadGpuWrittenCommands(GPUVAddr address, u32 count) {
+ // The pages are marked as command list pages: from the next write on, the buffer cache copies
+ // them to host memory right after the dispatch or copy that writes them and submits. The
+ // front then waits only for the back to run that work and for the GPU to finish the copy, not
+ // for everything queued and a cache flush (FlushGpuWrittenCommands: ~400 GPU round trips a
+ // second in a Mario Kart World race, during which the back had nothing to do). Pages
+ // FlushGpuWrittenCommands flushed whole and no GPU work wrote since are read from guest
+ // memory.
+ if (!Tegra::g_nx2_ampere.load(std::memory_order_relaxed) || count == 0 ||
+ rasterizer == nullptr) {
+ return false;
+ }
+ const GPUVAddr end = address + u64{count} * sizeof(u32);
+ bool written = false;
+ for (GPUVAddr page = address & ~GPUVAddr{0xfff}; page < end; page += 0x1000) {
+ if (const auto device = memory_manager.GpuToCpuAddress(page);
+ device && Tegra::Nx2IsGpuWritten(*device)) {
+ Tegra::Nx2MarkCommandListPage(*device);
+ written = true;
+ }
+ }
+ if (!written) {
+ return false;
+ }
+ if (auto& split = VideoCommon::GpuSplit::Get(); split.Active()) {
+ split.WaitForWriters();
+ }
+ using GpuWrittenList = VideoCore::RasterizerInterface::GpuWrittenList;
+ command_headers.resize_destructive(count);
+ u8* const out = reinterpret_cast<u8*>(command_headers.data());
+ for (GPUVAddr chunk = address; chunk < end;) {
+ const GPUVAddr chunk_end = (std::min)(end, (chunk & ~GPUVAddr{0xfff}) + 0x1000);
+ const std::span<u8> part{out + (chunk - address), static_cast<size_t>(chunk_end - chunk)};
+ const auto device = memory_manager.GpuToCpuAddress(chunk);
+ if (!device) {
+ return false;
+ }
+ GpuWrittenList where = GpuWrittenList::Synced;
+ if (Tegra::Nx2IsGpuWritten(*device)) {
+ where = rasterizer->ReadGpuWrittenCommandList(*device, part);
+ }
+ if (where == GpuWrittenList::Unknown) {
+ return false;
+ }
+ if (where == GpuWrittenList::Synced) {
+ memory_manager.ReadBlockUnsafe(chunk, part.data(), part.size());
+ }
+ chunk = chunk_end;
+ }
+ return true;
+}
+
+void DmaPusher::FlushGpuWrittenCommands(GPUVAddr address, size_t size) {
+ // Title 1 runs small command lists its culling dispatches fill in each frame. Accurate GPU
+ // mode reads every command list through the caches (safe reads); here only the ones on pages
+ // the GPU has written are, or their draws run with an older frame's words (trees vanish).
+ // These are the lists ReadGpuWrittenCommands can't take from a copy or guest memory (a page's
+ // first list, or pages draws write too).
+ if (!IsGpuWrittenCommands(address, size)) {
+ return;
+ }
+ // The caches belong to the split's back: it first runs what the front queued (the dispatch
+ // that writes these words among it). The whole pages are flushed: their next lists read
+ // guest memory until GPU work writes them again.
+ VideoCommon::GpuSplit::Get().Drain();
+ const GPUVAddr first_page = address & ~GPUVAddr{0xfff};
+ const GPUVAddr end_page = (address + size + 0xfff) & ~GPUVAddr{0xfff};
+ if (memory_manager.IsMemoryDirty(first_page, end_page - first_page,
+ VideoCommon::CacheType::BufferCache)) {
+ memory_manager.FlushRegion(first_page, end_page - first_page,
+ VideoCommon::CacheType::BufferCache);
+ }
+ if (rasterizer == nullptr) {
+ return;
+ }
+ for (GPUVAddr page = first_page; page < end_page; page += 0x1000) {
+ if (const auto device = memory_manager.GpuToCpuAddress(page);
+ device && Tegra::Nx2IsCommandListPage(*device)) {
+ rasterizer->NoteGpuWrittenCommandListSynced(*device);
+ }
+ }
+}
+
void DmaPusher::ReportUnboundSubchannel() {
// Methods sent to a subchannel with no engine (no SetObject yet) are dropped.
static std::atomic<u32> reported{};
diff --git a/src/video_core/dma_pusher.h b/src/video_core/dma_pusher.h
index 82d29edb4b..feab652bb5 100644
--- a/src/video_core/dma_pusher.h
+++ b/src/video_core/dma_pusher.h
@@ -7,6 +7,7 @@
#pragma once
#include <array>
+#include <atomic>
#include <condition_variable>
#include <optional>
#include <span>
@@ -117,6 +118,14 @@ struct CommandList final {
boost::container::small_vector<CommandHeader, 512> prefetch_command_list;
/// EDEN_NX2_PB_CHECK: hash of each entry's words when it was submitted.
std::vector<u64> submit_hashes;
+ /// The entries' words when they were submitted (NX2), each entry's at submitted_offsets[i]:
+ /// executed instead of the memory, which the game may already be refilling for a later frame.
+ std::vector<CommandHeader> submitted_words;
+ std::vector<u32> submitted_offsets;
+ /// DmaPusher::suspensions when the words were copied. A list copied during a suspension, or
+ /// that waited behind one, reads memory instead: games write the commands it runs before they
+ /// release the acquire.
+ u64 snapshot_suspensions = 0;
};
/// FNV-1a over pushbuffer words (EDEN_NX2_PB_CHECK).
@@ -135,6 +144,10 @@ inline u64 HashPushbufferWords(std::span<const u32> words) {
/// details on this implementation.
class DmaPusher final {
public:
+ /// Counts suspensions on a semaphore acquire and the resumes after them, so it is odd while
+ /// the channel is suspended (see CommandList::snapshot_suspensions).
+ std::atomic<u64> suspensions{0};
+
explicit DmaPusher(Core::System& system_, MemoryManager& memory_manager_, Control::ChannelState& channel_state_);
~DmaPusher();
@@ -182,6 +195,15 @@ public:
void DumpCurrentEntry(const char* reason);
void CheckSubmitHash(const CommandListHeader& header, u64 submit_hash);
void RecordParsedEntry(const CommandListHeader& header, std::span<const CommandHeader> commands);
+ /// Whether the entry lies on device pages the GPU has written (NX2).
+ bool IsGpuWrittenCommands(GPUVAddr address, size_t size);
+ /// Reads a command list on pages the GPU wrote into command_headers, from the copies the
+ /// buffer cache made after the work that wrote them or from guest memory for pages flushed
+ /// since. False when not all are current (or the list isn't GPU-written): the caller drains,
+ /// flushes and reads guest memory.
+ bool ReadGpuWrittenCommands(GPUVAddr address, u32 count);
+ /// Brings command list words the GPU wrote (still only in host buffers) to guest memory.
+ void FlushGpuWrittenCommands(GPUVAddr address, size_t size);
private:
diff --git a/src/video_core/engines/draw_manager.cpp b/src/video_core/engines/draw_manager.cpp
index 5891c25622..564220b71c 100644
--- a/src/video_core/engines/draw_manager.cpp
+++ b/src/video_core/engines/draw_manager.cpp
@@ -72,14 +72,49 @@ void Maxwell3D::DrawManager::Clear(Maxwell3D& maxwell3d, u32 layer_count) {
}
}
-void Maxwell3D::DrawManager::DrawDeferred(Maxwell3D& maxwell3d) {
+void Maxwell3D::DrawManager::DrawDeferred(Maxwell3D& maxwell3d, bool loop_continues) {
if (draw_state.draw_mode != DrawMode::Instance || draw_state.instance_count == 0) {
+ if (!loop_continues) {
+ draw_state.drawn_instances = 0;
+ }
return;
}
- DrawEnd(maxwell3d, draw_state.instance_count + 1, true);
+ // The first draw of a batch also covers the loop's first instance; a batch that continues a
+ // loop split by a register write draws only the instances after the ones already drawn.
+ const bool continued = draw_state.drawn_instances != 0;
+ DrawEnd(maxwell3d, draw_state.instance_count + (continued ? 0 : 1), true);
+ draw_state.drawn_instances =
+ loop_continues ? draw_state.drawn_instances + draw_state.instance_count : 0;
draw_state.instance_count = 0;
}
+bool Maxwell3D::DrawManager::IsInstanceLoopMethod(u32 method) noexcept {
+ switch (method) {
+ case MAXWELL3D_REG_INDEX(draw.begin):
+ case MAXWELL3D_REG_INDEX(draw.end):
+ case MAXWELL3D_REG_INDEX(vertex_buffer.first):
+ case MAXWELL3D_REG_INDEX(vertex_buffer.count):
+ case MAXWELL3D_REG_INDEX(index_buffer.first):
+ case MAXWELL3D_REG_INDEX(index_buffer.count):
+ case MAXWELL3D_REG_INDEX(index_buffer32_first):
+ case MAXWELL3D_REG_INDEX(index_buffer16_first):
+ case MAXWELL3D_REG_INDEX(index_buffer8_first):
+ case MAXWELL3D_REG_INDEX(index_buffer32_subsequent):
+ case MAXWELL3D_REG_INDEX(index_buffer16_subsequent):
+ case MAXWELL3D_REG_INDEX(index_buffer8_subsequent):
+ case MAXWELL3D_REG_INDEX(draw_inline_index):
+ case MAXWELL3D_REG_INDEX(inline_index_2x16.count):
+ case MAXWELL3D_REG_INDEX(inline_index_2x16.even):
+ case MAXWELL3D_REG_INDEX(inline_index_4x8.count):
+ case MAXWELL3D_REG_INDEX(inline_index_4x8.index0):
+ case MAXWELL3D_REG_INDEX(vertex_array_instance_first):
+ case MAXWELL3D_REG_INDEX(vertex_array_instance_subsequent):
+ return true;
+ default:
+ return false;
+ }
+}
+
void Maxwell3D::DrawManager::DrawArray(Maxwell3D& maxwell3d, Maxwell3D::Regs::PrimitiveTopology topology, u32 vertex_first, u32 vertex_count, u32 base_instance, u32 num_instances) {
draw_state.topology = topology;
draw_state.vertex_buffer.first = vertex_first;
@@ -94,6 +129,7 @@ void Maxwell3D::DrawManager::DrawArrayInstanced(Maxwell3D& maxwell3d, Maxwell3D:
draw_state.vertex_buffer.count = vertex_count;
if (!subsequent) {
draw_state.instance_count = 1;
+ draw_state.drawn_instances = 0;
}
draw_state.base_instance = draw_state.instance_count - 1;
draw_state.draw_mode = DrawMode::Instance;
@@ -193,6 +229,9 @@ void Maxwell3D::DrawManager::DrawEnd(Maxwell3D& maxwell3d, u32 instance_count, b
[[fallthrough]];
case DrawMode::General:
draw_state.base_instance = maxwell3d.regs.global_base_instance_index;
+ if (draw_state.draw_mode == DrawMode::Instance && draw_state.drawn_instances != 0) {
+ draw_state.base_instance += draw_state.drawn_instances + 1;
+ }
draw_state.base_index = maxwell3d.regs.global_base_vertex_index;
if (draw_state.draw_indexed) {
draw_state.index_buffer = maxwell3d.regs.index_buffer;
@@ -274,6 +313,8 @@ void Maxwell3D::DrawManager::UpdateTopology(Maxwell3D& maxwell3d) {
}
void Maxwell3D::DrawManager::ProcessDraw(Maxwell3D& maxwell3d, bool draw_indexed, u32 instance_count) {
+ draw_state.draw_element_count =
+ draw_indexed ? draw_state.index_buffer.count : draw_state.vertex_buffer.count;
LOG_TRACE(HW_GPU, "called, topology={}, count={}", draw_state.topology, draw_indexed ? draw_state.index_buffer.count : draw_state.vertex_buffer.count);
UpdateTopology(maxwell3d);
if (maxwell3d.ShouldExecute()) {
@@ -282,6 +323,7 @@ void Maxwell3D::DrawManager::ProcessDraw(Maxwell3D& maxwell3d, bool draw_indexed
}
void Maxwell3D::DrawManager::ProcessDrawIndirect(Maxwell3D& maxwell3d) {
+ draw_state.draw_element_count = 0;
LOG_TRACE(HW_GPU, "called, topology={}, is_indexed={}, includes_count={}, buffer_size={}, max_draw_count={}", draw_state.topology, indirect_state.is_indexed, indirect_state.include_count, indirect_state.buffer_size, indirect_state.max_draw_counts);
UpdateTopology(maxwell3d);
if (maxwell3d.ShouldExecute()) {
diff --git a/src/video_core/engines/fermi_2d.h b/src/video_core/engines/fermi_2d.h
index 7fddf9fdc1..04a12fd351 100644
--- a/src/video_core/engines/fermi_2d.h
+++ b/src/video_core/engines/fermi_2d.h
@@ -70,6 +70,9 @@ public:
ROP = 4,
SrcCopyPremult = 5,
BlendPremult = 6,
+ // Not a hardware operation: a copy that decodes sRGB-encoded values to linear, for the
+ // nx2 upscale fallback standing in for a program that decodes its output.
+ SrcCopySrgbToLinear = 7,
};
enum class MemoryLayout : u32 {
diff --git a/src/video_core/engines/kepler_compute.cpp b/src/video_core/engines/kepler_compute.cpp
index ab2ba10162..925b1b2ca1 100644
--- a/src/video_core/engines/kepler_compute.cpp
+++ b/src/video_core/engines/kepler_compute.cpp
@@ -6,6 +6,7 @@
#include <algorithm>
#include <atomic>
+#include <bit>
#include <bitset>
#include <cstdlib>
#include <fstream>
@@ -557,6 +558,7 @@ bool KeplerCompute::DispatchNativeCompute(const NativeQmdScheduler::Descriptor&
// The console runs this on tensor cores; stand in with a plain upscale of its input.
return UpscaleInsteadOfNativeProgram();
}
+ RememberDlssMotionVectors();
return false;
}
return true;
@@ -588,10 +590,38 @@ void KeplerCompute::DescribeNativeResources(const NativeQmdScheduler::Descriptor
std::atomic<u32> g_nx2_trace_draws_after_upscale{};
+// Fire Emblem: Fortune's Weave's first DLSS pass (rejected, before the network) has the input colour
+// at cbuf0+0x278, its motion vectors (R16G16 float, the input's size) at +0x290 and this frame's
+// camera jitter (x, y; input pixels, Halton bases 2 and 3 less 0.5) at +0x1b0.
+void KeplerCompute::RememberDlssMotionVectors() {
+ const auto& cbuf = launch_description.const_buffer_config[0];
+ if (cbuf.size.Value() < 0x294) return;
+ const GPUVAddr cbuf_address = (GPUVAddr{cbuf.address_high} << 32) | cbuf.address_low;
+ const auto image = [&](u32 offset) -> std::optional<Texture::TICEntry> {
+ const u32 tic_index = memory_manager.Read<u32>(cbuf_address + offset) & 0xfffff;
+ if (tic_index > regs.tic.limit) return std::nullopt;
+ return GetTICEntry(tic_index);
+ };
+ const auto color = image(0x278), motion = image(0x290);
+ if (!color || !motion || color->format != Texture::TextureFormat::B10G11R11 ||
+ motion->format != Texture::TextureFormat::R16G16 || motion->Address() == 0 ||
+ motion->Width() != color->Width() || motion->Height() != color->Height() ||
+ !motion->IsBlockLinear())
+ return;
+ dlss_motion_vectors = *motion;
+ dlss_motion_color = color->Address();
+ dlss_jitter = {std::bit_cast<f32>(memory_manager.Read<u32>(cbuf_address + 0x1b0)),
+ std::bit_cast<f32>(memory_manager.Read<u32>(cbuf_address + 0x1b4))};
+}
+
// Title 4's upscaler reads the tonemapped frame through the texture handle at cbuf0+0x90 and
// writes the output resolution image through the surface handle at cbuf0+0xa0 (from its
-// TLD/SUST operands). Blit one to the other with bilinear filtering when both are plain 2D images
-// of the same format and the output is larger.
+// TLD/SUST operands). Kirby Air Riders' reads a 2048x1152 image at cbuf0+0x80 and writes a
+// 2560x1440 one at +0x90; Fire Emblem: Fortune's Weave's DLSS network ends with a pass that reads
+// the input colour through cbuf0+0x328 and writes the DLSS output through cbuf0+0x348 (B10G11R11;
+// 720p to 1440p docked). Blit one to the other with bilinear filtering when both are plain 2D
+// images of the same format and the output is larger: title 4's slots first, then Fortune's
+// Weave's, else the first two handles in cbuf0+0x80..0x11c that form such a pair.
bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
const auto& cbuf = launch_description.const_buffer_config[0];
const GPUVAddr cbuf_address = (GPUVAddr{cbuf.address_high} << 32) | cbuf.address_low;
@@ -600,10 +630,37 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
if (tic_index > regs.tic.limit) return std::nullopt;
return GetTICEntry(tic_index);
};
- const auto input = image(0x90), output = image(0xa0);
- if (!input || !output || input->format != output->format || input->Address() == 0 ||
- output->Address() == 0 || output->Width() <= input->Width() || !input->IsBlockLinear() ||
- !output->IsBlockLinear())
+ const auto upscale_pair = [](const std::optional<Texture::TICEntry>& from,
+ const std::optional<Texture::TICEntry>& to) {
+ return from && to && from->format == to->format && from->Address() != 0 &&
+ to->Address() != 0 && to->Width() > from->Width() && from->IsBlockLinear() &&
+ to->IsBlockLinear();
+ };
+ std::optional<Texture::TICEntry> input = image(0x90), output = image(0xa0);
+ bool fortunes_weave = false;
+ if (!upscale_pair(input, output)) {
+ input = image(0x328);
+ output = image(0x348);
+ fortunes_weave = upscale_pair(input, output);
+ }
+ if (!upscale_pair(input, output)) {
+ input.reset();
+ output.reset();
+ for (u32 first = 0x80; first < 0x120 && !input; first += 4) {
+ const auto from = memory_manager.Read<u32>(cbuf_address + first) != 0 ? image(first)
+ : std::nullopt;
+ for (u32 second = first + 4; from && second < 0x120; second += 4) {
+ if (memory_manager.Read<u32>(cbuf_address + second) == 0) continue;
+ const auto to = image(second);
+ if (upscale_pair(from, to)) {
+ input = from;
+ output = to;
+ break;
+ }
+ }
+ }
+ }
+ if (!input || !output)
return false;
using RTFormat = Tegra::RenderTargetFormat;
RTFormat format{};
@@ -628,8 +685,32 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
result.addr_lower = static_cast<u32>(tic.Address());
return result;
};
+ // Title 4's program ends by decoding its result from sRGB to linear (the 0.04045 threshold
+ // and the 2.4 exponent as half-precision immediates of HSETP2 and HMUL2). The copy of its
+ // sRGB input left the frame gamma-encoded twice, washed out: decode in the copy when the
+ // program does.
+ static std::unordered_map<GPUVAddr, bool> decodes_srgb;
+ const GPUVAddr program = regs.code_loc.Address();
+ const auto [known, inserted] = decodes_srgb.try_emplace(program, false);
+ if (inserted) {
+ bool threshold = false, exponent = false;
+ std::array<u32, 0x400> block{};
+ for (u64 offset = 0; offset < 0x20000 && !(threshold && exponent); offset += sizeof(block)) {
+ if (!memory_manager.IsFullyMappedRange(program + offset, sizeof(block))) break;
+ memory_manager.ReadBlock(program + offset, block.data(), sizeof(block));
+ for (size_t word = 0; word < block.size(); word += 4) {
+ const u32 opcode = block[word] & 0xfff;
+ threshold |= opcode == 0x434 && block[word + 1] == 0x292d292d;
+ exponent |= opcode == 0x832 && block[word + 1] == 0x40cc40cc;
+ }
+ }
+ known->second = threshold && exponent;
+ LOG_INFO(HW_GPU, "Upscale fallback for program {:#x}: {}", program,
+ known->second ? "decodes sRGB like the program" : "plain copy");
+ }
const Fermi2D::Config config{
- .operation = Fermi2D::Operation::SrcCopy,
+ .operation = known->second ? Fermi2D::Operation::SrcCopySrgbToLinear
+ : Fermi2D::Operation::SrcCopy,
.filter = Fermi2D::Filter::Bilinear,
.must_accelerate = true,
.dst_x0 = 0,
@@ -700,7 +781,24 @@ bool KeplerCompute::UpscaleInsteadOfNativeProgram() {
source.format = static_cast<RTFormat>(std::strtoul(value, nullptr, 16));
}
}
- const bool done = rasterizer->AccelerateSurfaceCopy(source, surface(*output), config);
+ // Fortune's Weave: a temporal upscale along its motion vectors that undoes this frame's camera
+ // jitter (both from the frame's first DLSS pass) and accumulates the frames as DLSS would.
+ // EDEN_NX2_TEMPORAL_UPSCALE=0 blits instead.
+ const auto upscale_temporally = [&] {
+ static const bool enabled = [] {
+ const char* value = std::getenv("EDEN_NX2_TEMPORAL_UPSCALE");
+ return value == nullptr || std::string_view{value} != "0";
+ }();
+ if (!fortunes_weave || !enabled || !dlss_motion_vectors ||
+ dlss_motion_color != input->Address())
+ return false;
+ Fermi2D::Surface motion = surface(*dlss_motion_vectors);
+ motion.format = RTFormat::R16G16_FLOAT;
+ return rasterizer->AccelerateTemporalUpscale(source, surface(*output), motion, dlss_jitter,
+ {1.0f, 1.0f});
+ };
+ const bool done = upscale_temporally() ||
+ rasterizer->AccelerateSurfaceCopy(source, surface(*output), config);
if (static u32 checked{}; checked++ == sample_at) {
const u64 size = u64{output->Width()} * output->Height() * 4;
if (const auto device = memory_manager.GpuToCpuAddress(output->Address())) {
diff --git a/src/video_core/engines/kepler_compute.h b/src/video_core/engines/kepler_compute.h
index b525636dde..b5aee3706a 100644
--- a/src/video_core/engines/kepler_compute.h
+++ b/src/video_core/engines/kepler_compute.h
@@ -266,7 +266,15 @@ private:
bool DispatchNativeCompute(const NativeQmdScheduler::Descriptor& descriptor);
void ProcessNativeReportSemaphore(u32 argument);
void DescribeNativeResources(const NativeQmdScheduler::Descriptor& qmd);
+ void RememberDlssMotionVectors();
bool UpscaleInsteadOfNativeProgram();
+
+ /// Motion vectors of Fire Emblem: Fortune's Weave's DLSS evaluation, from its first pass (run
+ /// earlier in the frame than the network pass the stand-in replaces), with the input colour
+ /// image and camera jitter they go with.
+ std::optional<Texture::TICEntry> dlss_motion_vectors;
+ GPUVAddr dlss_motion_color{};
+ std::array<f32, 2> dlss_jitter{};
bool ProcessNativeMethod(Core::System& system, u32 method, u32 argument, bool is_last_call);
NativeQmdScheduler native_qmd;
std::array<u32, 0xc00>* native_data_ram{};
diff --git a/src/video_core/engines/maxwell_3d.cpp b/src/video_core/engines/maxwell_3d.cpp
index 6adfcf89b8..92313988bd 100644
--- a/src/video_core/engines/maxwell_3d.cpp
+++ b/src/video_core/engines/maxwell_3d.cpp
@@ -164,6 +164,8 @@ bool Maxwell3D::IsMethodExecutable(u32 method) {
case MAXWELL3D_REG_INDEX(vertex_array_instance_first):
case MAXWELL3D_REG_INDEX(vertex_array_instance_subsequent):
case MAXWELL3D_REG_INDEX(draw_texture.src_y0):
+ case RootTables::SelectMethod:
+ case RootTables::LoadMethod:
case MAXWELL3D_REG_INDEX(wait_for_idle):
case MAXWELL3D_REG_INDEX(shadow_ram_control):
case MAXWELL3D_REG_INDEX(load_mme.instruction_ptr):
@@ -336,6 +338,12 @@ void Maxwell3D::ConsumeSinkImpl(Core::System& system) {
}
void Maxwell3D::ProcessDirtyRegisters(u32 method, u32 argument) {
+ // The instances after the first are drawn together once their loop ends, and they must see
+ // the state they were begun with: NVN's draw macros reset the base instance (and its copy in
+ // c[0][0x644]) right after the loop, before the macro returns.
+ if (draw_manager.HasDeferredDraw() && !DrawManager::IsInstanceLoopMethod(method)) [[unlikely]] {
+ draw_manager.DrawDeferred(*this, true);
+ }
regs.reg_array[method] = argument;
for (auto const& table : dirty.tables)
dirty.flags[table[method]] = true;
@@ -346,6 +354,17 @@ void Maxwell3D::ProcessMethodCall(u32 method, u32 argument, u32 nonshadow_argume
LOG_DEBUG(HW_GPU, "Native GPU surface clear: value={:#x}", argument);
}
switch (method) {
+ case RootTables::SelectMethod:
+ root_tables.selected = argument & 7;
+ root_tables.offset = (argument >> 8) & 0xff;
+ return;
+ case RootTables::LoadMethod:
+ if (root_tables.offset < 256 && root_tables.offset % 4 == 0) {
+ root_tables.tables[root_tables.selected][root_tables.offset / 4] = argument;
+ root_tables.loaded = true;
+ }
+ root_tables.offset += 4;
+ return;
case MAXWELL3D_REG_INDEX(wait_for_idle):
return rasterizer->WaitForIdle();
case MAXWELL3D_REG_INDEX(shadow_ram_control):
@@ -499,6 +518,10 @@ void Maxwell3D::DumpRecentWrites(u32 method) {
}
LOG_ERROR(HW_GPU, "Write to {:#x} while macro {:#x} collects parameters ({} so far); last writes:",
method, executing_macro, macro_params.size());
+ if (!record_write_history) {
+ LOG_ERROR(HW_GPU, "Register write history disabled; EDEN_NX2_3D_HISTORY=1 enables it");
+ return;
+ }
for (size_t i = 0; i < recent_writes.size(); ++i) {
const auto& w = recent_writes[(recent_write_index + i) % recent_writes.size()];
if (w.amount != 0) {
@@ -516,6 +539,10 @@ void Maxwell3D::CallMultiMethod(Core::System& system, u32 method, const u32* bas
ProcessMacro(system, method, base_start, amount, amount == methods_pending);
return;
}
+ // As in ProcessDirtyRegisters: the paths below write without it.
+ if (draw_manager.HasDeferredDraw() && !DrawManager::IsInstanceLoopMethod(method)) [[unlikely]] {
+ draw_manager.DrawDeferred(*this, true);
+ }
switch (method) {
case MAXWELL3D_REG_INDEX(const_buffer.buffer):
case MAXWELL3D_REG_INDEX(const_buffer.buffer) + 1:
@@ -795,7 +822,6 @@ struct Maxwell3D::FrontState {
Regs::ShadowRamControl control{};
u32 executing_macro{};
std::vector<u32> parameters;
- std::vector<u32> fifo;
u64 calls{};
/// A method the draw manager handles was sent since the back last ran DrawDeferred: only
/// then can it have a deferred instanced draw to issue at the end of a macro.
@@ -819,6 +845,8 @@ bool FrontHandledOutsideDrawManager(u32 method) {
return true;
}
switch (method) {
+ case Maxwell3D::RootTables::SelectMethod:
+ case Maxwell3D::RootTables::LoadMethod:
case MAXWELL3D_REG_INDEX(wait_for_idle):
case MAXWELL3D_REG_INDEX(shadow_ram_control):
case MAXWELL3D_REG_INDEX(load_mme.instruction_ptr):
@@ -1070,9 +1098,9 @@ void Maxwell3D::FrontRunMacro(Core::System& system, VideoCommon::GpuSplit& split
engine.native_jit = std::make_shared<TuringMacroJit>();
}
engine.native_jit->Prepare(engine.native_code, engine.native_code_generation);
- fs.fifo.assign(fs.parameters.begin(), fs.parameters.end());
+ const size_t parameter_count = fs.parameters.size();
const auto result = engine.native_jit->Execute(
- start, fs.fifo, std::span<u32>{native_data_ram},
+ start, fs.parameters, std::span<u32>{native_data_ram},
[&fs](u32 address) { return address < Regs::NUM_REGS ? fs.regs[address] : 0U; },
[&](u32 address, u32 value) {
if (address >= MacroRegistersStart) {
@@ -1088,7 +1116,7 @@ void Maxwell3D::FrontRunMacro(Core::System& system, VideoCommon::GpuSplit& split
if (!result.success) {
engine.native_execution_failed = true;
LOG_ERROR(HW_GPU, "Native MME (JIT, split front) failed: start={:#x}, consumed={}/{}, error={}",
- start, result.consumed, fs.parameters.size(), result.error);
+ start, result.consumed, parameter_count, result.error);
}
}
fs.parameters.clear();
@@ -1123,7 +1151,7 @@ void Maxwell3D::FrontDmaRead(VideoCommon::GpuSplit& split, FrontState& fs, u32 m
for (u32 i = 0; i < words; ++i) {
const u32 word = memory_manager.Read<u32>(source + u64{i} * sizeof(u32));
if (method == 0x158) {
- fs.fifo.push_back(word);
+ fs.parameters.push_back(word);
} else if (ram + i < native_data_ram.size()) {
native_data_ram[ram + i] = word;
}
diff --git a/src/video_core/engines/maxwell_3d.h b/src/video_core/engines/maxwell_3d.h
index 0121f1a01c..7646ddbbe3 100644
--- a/src/video_core/engines/maxwell_3d.h
+++ b/src/video_core/engines/maxwell_3d.h
@@ -2541,6 +2541,19 @@ public:
return IsShaderConfigEnabled(static_cast<std::size_t>(type));
}
+ // Switch 2 (NVN) task and mesh pipelines (clc597.h): SET_MESH_CONTROL, SET_MESH_SHADER_A
+ // and _B, SET_MESH_INIT_SHADER. While mesh control is on, the vertex slot holds the task
+ // program (or the mesh program when there is no task), the tessellation slot the mesh
+ // program and the geometry slot a pass-through header naming the per-primitive outputs.
+ static constexpr std::size_t MeshControl = 0x114c / 4;
+ static constexpr std::size_t MeshShaderA = 0x1150 / 4;
+ static constexpr std::size_t MeshShaderB = 0x1154 / 4;
+ static constexpr std::size_t MeshInitShader = 0x1158 / 4;
+
+ bool IsMeshEnabled() const {
+ return (reg_array[MeshControl] & 1) != 0;
+ }
+
struct ConstantBuffer {
u32 size;
u32 address_high;
@@ -3051,11 +3064,14 @@ public:
Maxwell3D::Regs::PrimitiveTopology topology{};
DrawMode draw_mode{};
bool draw_indexed{};
+ u32 draw_element_count{}; // Zero for indirect draws whose count is not known yet.
u32 base_index{};
Maxwell3D::Regs::VertexBuffer vertex_buffer;
Maxwell3D::Regs::IndexBuffer index_buffer;
u32 base_instance{};
u32 instance_count{};
+ /// Deferred instances already drawn by flushes in the middle of the current loop.
+ u32 drawn_instances{};
std::vector<u8> inline_index_draw_indexes;
};
struct DrawTextureState {
@@ -3082,7 +3098,14 @@ public:
};
void ProcessMethodCall(Maxwell3D& maxwell3d, u32 method, u32 argument);
void Clear(Maxwell3D& maxwell3d, u32 layer_count);
- void DrawDeferred(Maxwell3D& maxwell3d);
+ /// Draws the waiting instances. loop_continues is set when the loop goes on afterwards.
+ void DrawDeferred(Maxwell3D& maxwell3d, bool loop_continues = false);
+ /// Instances after the first wait to be drawn together (DrawDeferred).
+ [[nodiscard]] bool HasDeferredDraw() const noexcept {
+ return draw_state.draw_mode == DrawMode::Instance && draw_state.instance_count != 0;
+ }
+ /// Methods an instance loop writes between its draws; any other write ends the loop.
+ [[nodiscard]] static bool IsInstanceLoopMethod(u32 method) noexcept;
void DrawArray(Maxwell3D& maxwell3d, Maxwell3D::Regs::PrimitiveTopology topology, u32 vertex_first, u32 vertex_count, u32 base_instance, u32 num_instances);
void DrawArrayInstanced(Maxwell3D& maxwell3d, Maxwell3D::Regs::PrimitiveTopology topology, u32 vertex_first, u32 vertex_count, bool subsequent);
void DrawIndex(Maxwell3D& maxwell3d, Maxwell3D::Regs::PrimitiveTopology topology, u32 index_first, u32 index_count, u32 base_index, u32 base_instance, u32 num_instances);
@@ -3160,6 +3183,18 @@ public:
std::array<u32, 0xc00>& GetNativeDataRam() { return native_data_ram; }
std::span<u32> GetNativeMacroPositions() { return native_macro_context ? std::span<u32>{native_macro_context->positions} : std::span<u32>{macro_positions}; }
+ /// Root tables: eight 256-byte tables written inline (method 0x504 selects a table, bits 0-2,
+ /// and a byte offset, bits 8-15; each 0x508 writes the next word) that shaders read as constant
+ /// banks 24-31. NVN2 keeps push constants in bank 24, which the Ampere lowering reads as bank 17.
+ struct RootTables {
+ static constexpr u32 SelectMethod = 0x504 / 4;
+ static constexpr u32 LoadMethod = 0x508 / 4;
+ std::array<std::array<u32, 64>, 8> tables{};
+ u32 selected{};
+ u32 offset{};
+ bool loaded{};
+ } root_tables;
+
/// Write the value to the register identified by method.
void CallMethod(Core::System& system, u32 method, u32 method_argument, bool is_last_call) override;
@@ -3318,7 +3353,17 @@ private:
};
std::array<RecentWrite, 48> recent_writes{};
size_t recent_write_index = 0;
+ const bool record_write_history{
+#ifdef NDEBUG
+ std::getenv("EDEN_NX2_3D_HISTORY") != nullptr
+#else
+ true
+#endif
+ };
void RecordWrite(u32 method, u32 argument, u32 amount, u32 pending) {
+ if (!record_write_history) {
+ return;
+ }
recent_writes[recent_write_index++ % recent_writes.size()] = {method, argument, amount, pending};
}
void DumpRecentWrites(u32 method);
diff --git a/src/video_core/engines/puller.cpp b/src/video_core/engines/puller.cpp
index 0bb1809aa3..37ac3ec851 100644
--- a/src/video_core/engines/puller.cpp
+++ b/src/video_core/engines/puller.cpp
@@ -116,6 +116,7 @@ bool Puller::ResumeAcquire(DmaPusher& dma_pusher) {
return false;
}
pending_acquire.reset();
+ dma_pusher.suspensions.fetch_add(1, std::memory_order_release);
Nx2SemaphoreWaiters::Get().Remove(this);
return true;
}
@@ -257,6 +258,7 @@ void Puller::ProcessSemaphoreTriggerMethod(DmaPusher& dma_pusher) {
dma_pusher.rasterizer->ReleaseFences();
if (!waiters.Latched(this) && !acquire.Matches(ReadSemaphoreWord(dma_pusher, acquire.address))) {
pending_acquire = acquire;
+ dma_pusher.suspensions.fetch_add(1, std::memory_order_release);
VideoCommon::Nx2Profile::Add(VideoCommon::Nx2Profile::Counter::AcquireWaits, 1);
} else {
waiters.Remove(this);
diff --git a/src/video_core/engines/sw_blitter/blitter.cpp b/src/video_core/engines/sw_blitter/blitter.cpp
index 8bcc2f7a76..78363bed11 100644
--- a/src/video_core/engines/sw_blitter/blitter.cpp
+++ b/src/video_core/engines/sw_blitter/blitter.cpp
@@ -5,6 +5,8 @@
#include <cmath>
#include <vector>
+#include "common/alignment.h"
+#include "common/literals.h"
#include "common/scratch_buffer.h"
#include "video_core/engines/sw_blitter/blitter.h"
#include "video_core/engines/sw_blitter/converter.h"
@@ -23,6 +25,7 @@ using VideoCore::Surface::PixelFormatFromRenderTargetFormat;
namespace Tegra::Engines::Blitter {
using namespace Texture;
+using namespace Common::Literals;
namespace {
@@ -125,6 +128,46 @@ void ProcessPitchLinear(std::span<const u8> input, std::span<u8> output, size_t
}
}
+struct SubSurface {
+ u64 offset; ///< From the surface's address
+ size_t size; ///< Bytes
+ u32 height; ///< Lines the part has
+ u32 origin_y; ///< The blit's first line in it
+};
+
+// The part of a surface a blit covers: the rows of blocks (block linear) or the lines (pitch) from
+// its first line to its last. A tile copied into a layer of a large texture used to read and write
+// back the whole layer (64-256 MiB), which also gave every block of a Switch 2 sparse layer memory.
+SubSurface CoveredRows(const Fermi2D::Surface& surface, u32 bytes_per_pixel, s32 x0, s32 y0,
+ u32 extent_x, u32 extent_y, size_t surface_size) {
+ const SubSurface whole{0, surface_size, surface.height, static_cast<u32>(y0)};
+ if (x0 < 0 || y0 < 0 || extent_y == 0) {
+ return whole;
+ }
+ const u64 first_line = static_cast<u32>(y0);
+ if (surface.linear == Fermi2D::MemoryLayout::BlockLinear) {
+ if (surface.depth != 1) {
+ return whole;
+ }
+ const u32 block_height = surface.block_height;
+ const u32 block_depth = surface.block_depth;
+ const u32 lines_shift = GOB_SIZE_Y_SHIFT + block_height;
+ const u64 row_size =
+ u64{Common::AlignUpLog2(surface.width * bytes_per_pixel, GOB_SIZE_X_SHIFT)}
+ << (lines_shift + block_depth);
+ const u64 first_row = first_line >> lines_shift;
+ const u64 rows = ((first_line + extent_y - 1) >> lines_shift) - first_row + 1;
+ const SubSurface part{first_row * row_size, static_cast<size_t>(rows * row_size),
+ static_cast<u32>(rows << lines_shift),
+ static_cast<u32>(first_line - (first_row << lines_shift))};
+ return part.offset + part.size <= surface_size ? part : whole;
+ }
+ const u64 size = (extent_y - 1) * u64{surface.pitch} +
+ (static_cast<u64>(x0) + extent_x) * bytes_per_pixel;
+ const SubSurface part{first_line * surface.pitch, static_cast<size_t>(size), extent_y, 0};
+ return part.offset + part.size <= surface_size ? part : whole;
+}
+
} // namespace
struct SoftwareBlitEngine::BlitEngineImpl {
@@ -133,6 +176,23 @@ struct SoftwareBlitEngine::BlitEngineImpl {
Common::ScratchBuffer<u8> dst_buffer;
Common::ScratchBuffer<f32> intermediate_src;
Common::ScratchBuffer<f32> intermediate_dst;
+
+ // Buffers grown for one large surface are freed after the blit rather than kept for the rest
+ // of the session; small ones stay. 16 MiB kept title 1's boot mip blits (64/16/4/1 MiB levels)
+ // in 16 such buffers, 256 MiB, across its 2D engines.
+ void ReleaseLargeBuffers() {
+ constexpr size_t LargeCapacity = 2_MiB;
+ for (auto* const buffer : {&tmp_buffer, &src_buffer, &dst_buffer}) {
+ if (buffer->capacity() > LargeCapacity) {
+ buffer->release();
+ }
+ }
+ for (auto* const buffer : {&intermediate_src, &intermediate_dst}) {
+ if (buffer->capacity() * sizeof(f32) > LargeCapacity) {
+ buffer->release();
+ }
+ }
+ }
ConverterFactory converter_factory;
};
@@ -161,9 +221,11 @@ bool SoftwareBlitEngine::Blit(Fermi2D::Surface& src, Fermi2D::Surface& dst,
const auto src_bytes_per_pixel = BytesPerBlock(PixelFormatFromRenderTargetFormat(src.format));
const auto dst_bytes_per_pixel = BytesPerBlock(PixelFormatFromRenderTargetFormat(dst.format));
const size_t src_size = get_surface_size(src, src_bytes_per_pixel);
+ const SubSurface src_part = CoveredRows(src, src_bytes_per_pixel, config.src_x0, config.src_y0,
+ src_extent_x, src_extent_y, src_size);
Tegra::Memory::GpuGuestMemory<u8, Tegra::Memory::GuestMemoryFlags::SafeRead> tmp_buffer(
- memory_manager, src.Address(), src_size, &impl->tmp_buffer);
+ memory_manager, src.Address() + src_part.offset, src_part.size, &impl->tmp_buffer);
const size_t src_copy_size = src_extent_x * src_extent_y * src_bytes_per_pixel;
const size_t dst_copy_size = dst_extent_x * dst_extent_y * dst_bytes_per_pixel;
@@ -202,12 +264,13 @@ bool SoftwareBlitEngine::Blit(Fermi2D::Surface& src, Fermi2D::Surface& dst,
impl->dst_buffer.resize_destructive(dst_copy_size);
if (src.linear == Fermi2D::MemoryLayout::BlockLinear) {
- UnswizzleSubrect(impl->src_buffer, tmp_buffer, src_bytes_per_pixel, src.width, src.height,
- src.depth, config.src_x0, config.src_y0, src_extent_x, src_extent_y,
- src.block_height, src.block_depth, src_extent_x * src_bytes_per_pixel);
+ UnswizzleSubrect(impl->src_buffer, tmp_buffer, src_bytes_per_pixel, src.width,
+ src_part.height, src.depth, config.src_x0, src_part.origin_y,
+ src_extent_x, src_extent_y, src.block_height, src.block_depth,
+ src_extent_x * src_bytes_per_pixel);
} else {
ProcessPitchLinear<false>(tmp_buffer, impl->src_buffer, src_extent_x, src_extent_y,
- src.pitch, config.src_x0, config.src_y0, src_bytes_per_pixel);
+ src.pitch, config.src_x0, src_part.origin_y, src_bytes_per_pixel);
}
// Conversion Phase
@@ -222,18 +285,25 @@ bool SoftwareBlitEngine::Blit(Fermi2D::Surface& src, Fermi2D::Surface& dst,
}
const size_t dst_size = get_surface_size(dst, dst_bytes_per_pixel);
- Tegra::Memory::GpuGuestMemoryScoped<u8, Tegra::Memory::GuestMemoryFlags::SafeReadWrite>
- tmp_buffer2(memory_manager, dst.Address(), dst_size, &impl->tmp_buffer);
-
- if (dst.linear == Fermi2D::MemoryLayout::BlockLinear) {
- SwizzleSubrect(tmp_buffer2, impl->dst_buffer, dst_bytes_per_pixel, dst.width, dst.height,
- dst.depth, config.dst_x0, config.dst_y0, dst_extent_x, dst_extent_y,
- dst.block_height, dst.block_depth, dst_extent_x * dst_bytes_per_pixel);
- } else {
- ProcessPitchLinear<true>(impl->dst_buffer, tmp_buffer2, dst_extent_x, dst_extent_y,
- dst.pitch, config.dst_x0, config.dst_y0,
- static_cast<size_t>(dst_bytes_per_pixel));
- }
+ const SubSurface dst_part = CoveredRows(dst, dst_bytes_per_pixel, config.dst_x0, config.dst_y0,
+ dst_extent_x, dst_extent_y, dst_size);
+ {
+ Tegra::Memory::GpuGuestMemoryScoped<u8, Tegra::Memory::GuestMemoryFlags::SafeReadWrite>
+ tmp_buffer2(memory_manager, dst.Address() + dst_part.offset, dst_part.size,
+ &impl->tmp_buffer);
+
+ if (dst.linear == Fermi2D::MemoryLayout::BlockLinear) {
+ SwizzleSubrect(tmp_buffer2, impl->dst_buffer, dst_bytes_per_pixel, dst.width,
+ dst_part.height, dst.depth, config.dst_x0, dst_part.origin_y,
+ dst_extent_x, dst_extent_y, dst.block_height, dst.block_depth,
+ dst_extent_x * dst_bytes_per_pixel);
+ } else {
+ ProcessPitchLinear<true>(impl->dst_buffer, tmp_buffer2, dst_extent_x, dst_extent_y,
+ dst.pitch, config.dst_x0, dst_part.origin_y,
+ static_cast<size_t>(dst_bytes_per_pixel));
+ }
+ } // tmp_buffer2 writes the destination back here
+ impl->ReleaseLargeBuffers();
return true;
}
diff --git a/src/video_core/fence_manager.h b/src/video_core/fence_manager.h
index 61c9844cf2..d9f0ac2837 100644
--- a/src/video_core/fence_manager.h
+++ b/src/video_core/fence_manager.h
@@ -18,6 +18,7 @@
#include <queue>
#include "common/common_types.h"
+#include "common/logging.h"
#include "common/settings.h"
#include "common/thread.h"
#include "video_core/delayed_destruction_ring.h"
@@ -123,6 +124,13 @@ public:
}
void SignalSyncPoint(u32 value) {
+ // The id comes from a command list (the 3D engine's sync method, the puller's fence
+ // action). Past the syncpoint table, the increment would walk memory after it as an
+ // action list; command words read wrong carry any id (Splatoon Raiders on Linux).
+ if (value >= Tegra::Host1x::SyncpointManager::NUM_MAX_SYNCPOINTS) {
+ LOG_ERROR(HW_GPU, "Signal of syncpoint {} past the table, ignored", value);
+ return;
+ }
syncpoint_manager.IncrementGuest(value);
std::function<void()> func([this, value] { syncpoint_manager.IncrementHost(value); });
SignalFence(std::move(func));
diff --git a/src/video_core/gpu.cpp b/src/video_core/gpu.cpp
index 53f8f063f7..6dbabcb43a 100644
--- a/src/video_core/gpu.cpp
+++ b/src/video_core/gpu.cpp
@@ -210,6 +210,10 @@ struct GPU::Impl {
gpu_thread.Run(std::move(func), is_async);
}
+ void MapInOrder(std::function<void()>&& func) {
+ gpu_thread.Run(std::move(func), is_async, true);
+ }
+
/// Notify rasterizer that any caches of the specified region should be flushed to Switch memory
void FlushRegion(DAddr addr, u64 size) {
gpu_thread.FlushRegion(addr, size, is_async);
@@ -228,7 +232,9 @@ struct GPU::Impl {
raster_area.start_address, raster_area.end_address,
reads.load(std::memory_order_relaxed));
}
- const u64 fence = RequestSyncOperation([this, &raster_area]() {
+ const u64 fence = RequestSyncOperation([this, raster_area]() {
+ // Apply collected guest writes before downloading GPU data over guest memory.
+ InvalidateGPUCache();
renderer->ReadRasterizer()->FlushRegion(raster_area.start_address, raster_area.end_address - raster_area.start_address);
});
gpu_thread.TickGPU(is_async);
@@ -505,6 +511,10 @@ bool GPU::IsAsync() const {
return impl->is_async;
}
+bool GPU::HasSuspendedChannels() const {
+ return impl->scheduler.HasPendingChannelsAnyThread();
+}
+
bool GPU::UseNvdec() const {
return impl->use_nvdec;
}
@@ -549,6 +559,10 @@ void GPU::RunInOrder(std::function<void()>&& func) {
impl->RunInOrder(std::move(func));
}
+void GPU::MapInOrder(std::function<void()>&& func) {
+ impl->MapInOrder(std::move(func));
+}
+
void GPU::InvalidateRegion(DAddr addr, u64 size, bool preserve_gpu_writes) {
impl->InvalidateRegion(addr, size, preserve_gpu_writes);
}
diff --git a/src/video_core/gpu.h b/src/video_core/gpu.h
index 3a6546b41b..d37dfa5990 100644
--- a/src/video_core/gpu.h
+++ b/src/video_core/gpu.h
@@ -217,6 +217,9 @@ public:
[[nodiscard]] bool IsAsync() const;
+ /// Whether a channel is suspended on a semaphore acquire (callable from any thread).
+ [[nodiscard]] bool HasSuspendedChannels() const;
+
[[nodiscard]] bool UseNvdec() const;
void RendererFrameEndNotify();
@@ -254,6 +257,10 @@ public:
/// Runs func on the GPU thread after the command lists submitted so far.
void RunInOrder(std::function<void()>&& func);
+ /// RunInOrder for GPU memory maps: also done before the GPU thread reads the command lists
+ /// submitted later (with the GPU split, on its front rather than its back).
+ void MapInOrder(std::function<void()>&& func);
+
/// Notify rasterizer that any caches of the specified region should be invalidated
void InvalidateRegion(DAddr addr, u64 size, bool preserve_gpu_writes = false);
diff --git a/src/video_core/gpu_split.cpp b/src/video_core/gpu_split.cpp
index ac0d3f25f1..ef0cdae123 100644
--- a/src/video_core/gpu_split.cpp
+++ b/src/video_core/gpu_split.cpp
@@ -138,6 +138,7 @@ void GpuSplit::Stop() {
// never ran, it waited on its syncpoint forever), and the engines it held were freed.
ring.reset();
write_position = 0;
+ last_writer_position = 0;
published_local = 0;
consumed_cache = 0;
publish_every = PublishEvery;
@@ -216,6 +217,28 @@ void GpuSplit::Drain() {
}
}
+void GpuSplit::WaitForWriters() {
+ const u64 target = std::min(last_writer_position, write_position);
+ consumed_cache = consumed.load(std::memory_order_acquire);
+ if (consumed_cache >= target) {
+ return;
+ }
+ wait_target.store(target, std::memory_order_seq_cst);
+ Publish();
+ for (u32 spins = 0;; ++spins) {
+ consumed_cache = consumed.load(std::memory_order_acquire);
+ if (consumed_cache >= target) {
+ wait_target.store(~u64{0}, std::memory_order_relaxed);
+ return;
+ }
+ if (spins < 16384) {
+ Pause();
+ } else {
+ std::this_thread::yield();
+ }
+ }
+}
+
void GpuSplit::SwitchEngine(Tegra::Engines::EngineInterface* engine) {
front_engine = engine;
u64* const entry = Reserve(2);
@@ -346,8 +369,11 @@ void GpuSplit::BackLoop(std::stop_token stop_token) {
const size_t index = read_position & RingMask;
const size_t size = Execute(&ring[index]);
read_position += size != 0 ? size : RingWords - index;
- // Hand ring space back now and then while working through a long batch.
- if (read_position - last_store >= 4096 && read_position != available) {
+ // Hand ring space back now and then while working through a long batch, and at once
+ // when the front waits for this position (WaitForWriters).
+ if ((read_position - last_store >= 4096 ||
+ read_position >= wait_target.load(std::memory_order_relaxed)) &&
+ read_position != available) {
consumed.store(read_position, std::memory_order_release);
last_store = read_position;
}
diff --git a/src/video_core/gpu_split.h b/src/video_core/gpu_split.h
index d6f91e4141..c7c7118481 100644
--- a/src/video_core/gpu_split.h
+++ b/src/video_core/gpu_split.h
@@ -100,6 +100,13 @@ public:
/// Waits until the back executed everything sent so far.
void Drain();
+ /// Compute or copy work was just sent: what can write GPU-written command lists.
+ void NoteWriter() {
+ last_writer_position = write_position;
+ }
+ /// Waits until the back executed the compute and copy work sent so far (not what came after).
+ void WaitForWriters();
+
private:
enum class Kind : u64 {
Wrap, ///< The rest of the ring is unused: continue at its start
@@ -156,6 +163,7 @@ private:
// Front side.
u64 write_position{};
+ u64 last_writer_position{};
u64 published_local{};
u64 consumed_cache{};
/// Words between publishes: small right after a drain, while the back has nothing to do.
@@ -169,6 +177,8 @@ private:
alignas(64) std::atomic<u64> published{};
alignas(64) std::atomic<u64> consumed{};
+ /// A position the front waits for (WaitForWriters): the back stores `consumed` once past it.
+ alignas(64) std::atomic<u64> wait_target{~u64{0}};
alignas(64) std::atomic<bool> back_waiting{};
std::atomic<u32> wake{};
std::jthread back_thread;
diff --git a/src/video_core/gpu_thread.cpp b/src/video_core/gpu_thread.cpp
index 55200b0647..3af8cb65a8 100644
--- a/src/video_core/gpu_thread.cpp
+++ b/src/video_core/gpu_thread.cpp
@@ -99,7 +99,12 @@ void ThreadManager::StartThread(VideoCore::RendererBase& renderer, Core::Fronten
} else if (std::holds_alternative<GPUTickCommand>(next.data)) {
split.Defer([gpu = &system.GPU()] { gpu->TickWork(); });
} else if (auto* run = std::get_if<RunCommand>(&next.data)) {
- split.DeferFunction(std::move(run->func));
+ if (run->before_later_reads) {
+ split.Drain();
+ run->func();
+ } else {
+ split.DeferFunction(std::move(run->func));
+ }
} else if (const auto* flush = std::get_if<FlushRegionCommand>(&next.data)) {
split.Defer([rasterizer_ptr, addr = flush->addr, size = flush->size] {
rasterizer_ptr->FlushRegion(addr, size);
@@ -155,8 +160,8 @@ void ThreadManager::TickGPU(bool is_async) {
PushCommand(GPUTickCommand(), false, is_async);
}
-void ThreadManager::Run(std::function<void()>&& func, bool is_async) {
- PushCommand(RunCommand{std::move(func)}, false, is_async);
+void ThreadManager::Run(std::function<void()>&& func, bool is_async, bool before_later_reads) {
+ PushCommand(RunCommand{std::move(func), before_later_reads}, false, is_async);
}
void ThreadManager::InvalidateRegion(DAddr addr, u64 size) {
diff --git a/src/video_core/gpu_thread.h b/src/video_core/gpu_thread.h
index 796c0d89f6..927453dab0 100644
--- a/src/video_core/gpu_thread.h
+++ b/src/video_core/gpu_thread.h
@@ -80,6 +80,9 @@ struct GPUTickCommand final {};
/// Command to run a function on the GPU thread after the commands queued before it
struct RunCommand final {
std::function<void()> func;
+ /// With the GPU split, run on its front (after draining the back) instead of deferring to the
+ /// back, so command lists read after this one see what func did (GMMU maps).
+ bool before_later_reads{};
};
using CommandData =
@@ -132,7 +135,7 @@ public:
void TickGPU(bool is_async);
/// Runs func on the GPU thread once the commands queued before it have been processed.
- void Run(std::function<void()>&& func, bool is_async);
+ void Run(std::function<void()>&& func, bool is_async, bool before_later_reads = false);
void NotifyShutdown();
diff --git a/src/video_core/host1x/codecs/codec.cpp b/src/video_core/host1x/codecs/codec.cpp
index 56332866e5..3cbef1d891 100644
--- a/src/video_core/host1x/codecs/codec.cpp
+++ b/src/video_core/host1x/codecs/codec.cpp
@@ -104,6 +104,8 @@ std::string_view Codec::GetCurrentCodecName() const {
return "H265";
case Host1x::NvdecCommon::VideoCodec::VP9:
return "VP9";
+ case Host1x::NvdecCommon::VideoCodec::AV1:
+ return "AV1";
default:
return "Unknown";
}
diff --git a/src/video_core/host1x/ffmpeg.cpp b/src/video_core/host1x/ffmpeg.cpp
index 676e5820a5..1af8307adc 100644
--- a/src/video_core/host1x/ffmpeg.cpp
+++ b/src/video_core/host1x/ffmpeg.cpp
@@ -163,6 +163,8 @@ Decoder::Decoder(Tegra::Host1x::NvdecCommon::VideoCodec codec) {
return AV_CODEC_ID_VP8;
case Tegra::Host1x::NvdecCommon::VideoCodec::VP9:
return AV_CODEC_ID_VP9;
+ case Tegra::Host1x::NvdecCommon::VideoCodec::AV1:
+ return AV_CODEC_ID_AV1;
default:
UNIMPLEMENTED_MSG("Unknown codec {}", codec);
return AV_CODEC_ID_NONE;
diff --git a/src/video_core/host1x/nvdec_common.h b/src/video_core/host1x/nvdec_common.h
index 0857a9f95c..26d7c86c98 100644
--- a/src/video_core/host1x/nvdec_common.h
+++ b/src/video_core/host1x/nvdec_common.h
@@ -22,6 +22,7 @@ enum class VideoCodec : u64 {
VP8 = 0x5,
H265 = 0x7,
VP9 = 0x9,
+ AV1 = 0xA,
};
struct Offset {
diff --git a/src/video_core/host1x/nx2_video_decoder.cpp b/src/video_core/host1x/nx2_video_decoder.cpp
index 7d185ca573..9b34bd84af 100644
--- a/src/video_core/host1x/nx2_video_decoder.cpp
+++ b/src/video_core/host1x/nx2_video_decoder.cpp
@@ -35,6 +35,10 @@ std::optional<NvdecCommon::VideoCodec> CodecFromMime(std::string_view mime) {
if (mime.find("vp8") != std::string_view::npos) {
return NvdecCommon::VideoCodec::VP8;
}
+ // "video/av01" or "video/av1". FFmpeg decodes AV1 with libdav1d.
+ if (mime.find("av01") != std::string_view::npos || mime.find("av1") != std::string_view::npos) {
+ return NvdecCommon::VideoCodec::AV1;
+ }
if (mime.find("avc") != std::string_view::npos || mime.find("264") != std::string_view::npos) {
return NvdecCommon::VideoCodec::H264;
}
diff --git a/src/video_core/host1x/syncpoint_manager.h b/src/video_core/host1x/syncpoint_manager.h
index 847ed20c8d..d6e65af3eb 100644
--- a/src/video_core/host1x/syncpoint_manager.h
+++ b/src/video_core/host1x/syncpoint_manager.h
@@ -18,6 +18,8 @@ namespace Host1x {
class SyncpointManager {
public:
+ static constexpr size_t NUM_MAX_SYNCPOINTS = 192;
+
u32 GetGuestSyncpointValue(u32 id) const {
return syncpoints_guest[id].load(std::memory_order_acquire);
}
@@ -78,8 +80,6 @@ private:
void Wait(std::atomic<u32>& syncpoint, std::condition_variable& wait_cv, u32 expected_value);
- static constexpr size_t NUM_MAX_SYNCPOINTS = 192;
-
std::array<std::atomic<u32>, NUM_MAX_SYNCPOINTS> syncpoints_guest{};
std::array<std::atomic<u32>, NUM_MAX_SYNCPOINTS> syncpoints_host{};
diff --git a/src/video_core/host_shaders/CMakeLists.txt b/src/video_core/host_shaders/CMakeLists.txt
index 35b350c2cd..3dc26ec3ba 100644
--- a/src/video_core/host_shaders/CMakeLists.txt
+++ b/src/video_core/host_shaders/CMakeLists.txt
@@ -14,9 +14,11 @@ set(GLSL_INCLUDES
)
set(SHADER_FILES
+ ${CMAKE_CURRENT_SOURCE_DIR}/vulkan_vertex_repack.comp
${CMAKE_CURRENT_SOURCE_DIR}/astc_decoder.comp
${CMAKE_CURRENT_SOURCE_DIR}/bc_encoder.comp
${CMAKE_CURRENT_SOURCE_DIR}/blit_color_float.frag
+ ${CMAKE_CURRENT_SOURCE_DIR}/blit_color_srgb_to_linear.frag
${CMAKE_CURRENT_SOURCE_DIR}/block_linear_unswizzle_2d.comp
${CMAKE_CURRENT_SOURCE_DIR}/blit_color_msaa.frag
${CMAKE_CURRENT_SOURCE_DIR}/blit_depth.frag
@@ -71,6 +73,7 @@ set(SHADER_FILES
${CMAKE_CURRENT_SOURCE_DIR}/smaa_blending_weight_calculation.frag
${CMAKE_CURRENT_SOURCE_DIR}/smaa_neighborhood_blending.vert
${CMAKE_CURRENT_SOURCE_DIR}/smaa_neighborhood_blending.frag
+ ${CMAKE_CURRENT_SOURCE_DIR}/temporal_upscale.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_blit_depth_stencil.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_color_clear.frag
${CMAKE_CURRENT_SOURCE_DIR}/vulkan_color_clear.vert
diff --git a/src/video_core/host_shaders/blit_color_srgb_to_linear.frag b/src/video_core/host_shaders/blit_color_srgb_to_linear.frag
new file mode 100644
index 0000000000..1a38cb52a1
--- /dev/null
+++ b/src/video_core/host_shaders/blit_color_srgb_to_linear.frag
@@ -0,0 +1,18 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#version 450
+
+layout(binding = 0) uniform sampler2D tex;
+
+layout(location = 0) in vec2 texcoord;
+layout(location = 0) out vec4 color;
+
+// Copy of an sRGB-encoded source into a linear destination: the piecewise sRGB decode on the
+// color channels, alpha unchanged.
+void main() {
+ const vec4 value = textureLod(tex, texcoord, 0);
+ const vec3 low = value.rgb / 12.92;
+ const vec3 high = pow((value.rgb + 0.055) / 1.055, vec3(2.4));
+ color = vec4(mix(high, low, lessThan(value.rgb, vec3(0.04045))), value.a);
+}
diff --git a/src/video_core/host_shaders/temporal_upscale.frag b/src/video_core/host_shaders/temporal_upscale.frag
new file mode 100644
index 0000000000..0e76df2b43
--- /dev/null
+++ b/src/video_core/host_shaders/temporal_upscale.frag
@@ -0,0 +1,132 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+// Stands in for a game's DLSS evaluation (the tensor-core network cannot run here): a temporal
+// upscale. Each frame's input samples are placed where the camera jitter put them and weighted by
+// their distance to the output pixel; they accumulate with the previous output, reprojected along
+// the game's motion vectors (bicubic) and clipped to the current 3x3 neighbourhood.
+
+#version 450
+
+layout(binding = 0) uniform sampler2D input_color;
+layout(binding = 1) uniform sampler2D motion_vectors;
+layout(binding = 2) uniform sampler2D history;
+
+layout(push_constant) uniform PushConstants {
+ // The vertex stage's texture scale and offset come first.
+ layout(offset = 16) vec2 jitter; // this frame's sample offset, in input pixels
+ vec2 motion_scale; // motion vector -> UV offset to the previous frame
+ float current_weight; // largest weight of this frame; 1 ignores the history
+};
+
+layout(location = 0) in vec2 texcoord; // 0 to 1 across the output
+layout(location = 0) out vec4 color;
+
+// The blit helper's samplers take unnormalized (texel) coordinates.
+
+// Blending in a range-compressed space keeps bright pixels from dominating the average.
+vec3 Compress(vec3 c) {
+ return c / (1.0 + max(max(c.r, c.g), c.b));
+}
+
+vec3 Expand(vec3 c) {
+ c = clamp(c, vec3(0.0), vec3(1.0 - 1.0 / 65504.0));
+ return c / (1.0 - max(max(c.r, c.g), c.b));
+}
+
+vec3 RgbToYCoCg(vec3 c) {
+ return vec3(dot(c, vec3(0.25, 0.5, 0.25)), dot(c, vec3(0.5, 0.0, -0.5)),
+ dot(c, vec3(-0.25, 0.5, -0.25)));
+}
+
+vec3 YCoCgToRgb(vec3 c) {
+ return vec3(c.x + c.y - c.z, c.x + c.z, c.x - c.y - c.z);
+}
+
+// Moves value towards the box centre until it lies inside the box.
+vec3 ClipToBox(vec3 value, vec3 low, vec3 high) {
+ const vec3 center = 0.5 * (low + high);
+ const vec3 extent = 0.5 * (high - low) + 1.0 / 65504.0;
+ const vec3 offset = value - center;
+ const vec3 units = abs(offset / extent);
+ const float largest = max(units.x, max(units.y, units.z));
+ return largest > 1.0 ? center + offset / largest : value;
+}
+
+// Catmull-Rom filtered history from five bilinear fetches: repeated bilinear reprojection would
+// blur the accumulated image a little more every frame.
+vec3 SampleHistory(vec2 position) {
+ const vec2 texel1 = floor(position - 0.5) + 0.5;
+ const vec2 f = position - texel1;
+ const vec2 w0 = f * (-0.5 + f * (1.0 - 0.5 * f));
+ const vec2 w1 = 1.0 + f * f * (-2.5 + 1.5 * f);
+ const vec2 w2 = f * (0.5 + f * (2.0 - 1.5 * f));
+ const vec2 w3 = f * f * (-0.5 + 0.5 * f);
+ const vec2 w12 = w1 + w2;
+ const vec2 texel0 = texel1 - 1.0;
+ const vec2 texel3 = texel1 + 2.0;
+ const vec2 texel12 = texel1 + w2 / w12;
+ vec3 sum = texture(history, vec2(texel12.x, texel0.y)).rgb * (w12.x * w0.y);
+ sum += texture(history, vec2(texel0.x, texel12.y)).rgb * (w0.x * w12.y);
+ sum += texture(history, texel12).rgb * (w12.x * w12.y);
+ sum += texture(history, vec2(texel3.x, texel12.y)).rgb * (w3.x * w12.y);
+ sum += texture(history, vec2(texel12.x, texel3.y)).rgb * (w12.x * w3.y);
+ const float weight = w12.x * w0.y + w0.x * w12.y + w12.x * w12.y + w3.x * w12.y + w12.x * w3.y;
+ return max(sum / weight, vec3(0.0));
+}
+
+void main() {
+ const ivec2 input_size = textureSize(input_color, 0);
+ const ivec2 motion_size = textureSize(motion_vectors, 0);
+ // The output pixel's centre in input pixels. Input texel k sampled the scene at k + 0.5 - jitter
+ // (the sign measured against consecutive frames).
+ const vec2 target = texcoord * vec2(input_size);
+ const ivec2 nearest = ivec2(floor(target + jitter));
+
+ vec3 filtered = vec3(0.0);
+ float total_weight = 0.0;
+ float nearest_weight = 0.0;
+ vec3 sum = vec3(0.0);
+ vec3 squares = vec3(0.0);
+ vec2 motion = vec2(0.0);
+ float longest = -1.0;
+ for (int y = -1; y <= 1; ++y) {
+ for (int x = -1; x <= 1; ++x) {
+ const ivec2 texel = clamp(nearest + ivec2(x, y), ivec2(0), input_size - 1);
+ const vec3 sample_color = RgbToYCoCg(Compress(texelFetch(input_color, texel, 0).rgb));
+ // A Blackman-Harris-like window over the sample's distance to the output pixel.
+ const vec2 distance = vec2(texel) + 0.5 - jitter - target;
+ const float weight = exp(-2.29 * dot(distance, distance));
+ filtered += sample_color * weight;
+ total_weight += weight;
+ nearest_weight = max(nearest_weight, weight);
+ sum += sample_color;
+ squares += sample_color * sample_color;
+ // The longest motion around the pixel, so that an edge follows the object in front.
+ const vec2 candidate =
+ texelFetch(motion_vectors, clamp(texel, ivec2(0), motion_size - 1), 0).rg;
+ const float length_squared = dot(candidate, candidate);
+ if (length_squared > longest) {
+ longest = length_squared;
+ motion = candidate;
+ }
+ }
+ }
+ const vec3 current = filtered / total_weight;
+ const vec3 mean = sum / 9.0;
+ const vec3 deviation = sqrt(max(squares / 9.0 - mean * mean, vec3(0.0)));
+ const vec3 low = mean - deviation;
+ const vec3 high = mean + deviation;
+
+ const vec2 previous = texcoord + motion * motion_scale;
+ // A frame whose samples land close to this pixel counts more; one whose samples land between
+ // pixels mostly keeps the history (from other jitter positions) there.
+ float weight = current_weight * nearest_weight;
+ if (current_weight >= 1.0 || any(lessThan(previous, vec2(0.0))) ||
+ any(greaterThan(previous, vec2(1.0)))) {
+ weight = 1.0;
+ }
+ const vec2 history_position = previous * vec2(textureSize(history, 0));
+ const vec3 past = ClipToBox(RgbToYCoCg(Compress(SampleHistory(history_position))), low, high);
+ color = vec4(Expand(YCoCgToRgb(mix(past, current, weight))), 1.0);
+}
diff --git a/src/video_core/host_shaders/vulkan_vertex_repack.comp b/src/video_core/host_shaders/vulkan_vertex_repack.comp
new file mode 100644
index 0000000000..de9c649d93
--- /dev/null
+++ b/src/video_core/host_shaders/vulkan_vertex_repack.comp
@@ -0,0 +1,26 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#version 450
+layout(local_size_x = 128) in;
+layout(std430, set = 0, binding = 0) readonly buffer Source { uint source_words[]; };
+layout(std430, set = 0, binding = 1) writeonly buffer Destination { uint dest_words[]; };
+layout(push_constant) uniform Parameters { uint source_bias; uint source_size; uint total_words; };
+
+// Diagnostic conversion of the captured 69-byte layout: insert three bytes before offset 65.
+void main() {
+ uint word = gl_GlobalInvocationID.x;
+ if (word >= total_words) return;
+ uint vertex = word / 18;
+ uint offset = (word % 18) * 4;
+ uint value = 0;
+ for (uint i = 0; i < 4; ++i) {
+ uint dest_byte = offset + i;
+ if (dest_byte >= 65 && dest_byte < 68) continue;
+ uint src_byte = vertex * 69 + dest_byte - (dest_byte >= 68 ? 3 : 0);
+ if (src_byte >= source_size) continue;
+ uint address = source_bias + src_byte;
+ uint byte_value = (source_words[address / 4] >> ((address % 4) * 8)) & 255;
+ value |= byte_value << (i * 8);
+ }
+ dest_words[word] = value;
+}
diff --git a/src/video_core/memory_manager.cpp b/src/video_core/memory_manager.cpp
index 9111ba10d2..ddfd2529fd 100644
--- a/src/video_core/memory_manager.cpp
+++ b/src/video_core/memory_manager.cpp
@@ -302,7 +302,7 @@ void MemoryManager::Write(GPUVAddr addr, T data) {
return;
}
// A Switch 2 sparse block nobody wrote yet gets its memory now.
- Tegra::Nx2BackSparseWrite(addr, sizeof(T));
+ Tegra::Nx2BackSparseWrite(addr, sizeof(T), &data);
if (auto page_pointer{GetPointer(addr)}; page_pointer) {
std::memcpy(page_pointer, &data, sizeof(T));
return;
@@ -431,11 +431,14 @@ void MemoryManager::ReadBlockImpl(GPUVAddr gpu_src_addr, void* dest_buffer, std:
if (!unsafe) {
rasterizer->FlushRegion(dev_addr_base, copy_amount, which);
}
- if (!IsBigPageContinuous(page_index)) [[unlikely]] {
- memory.ReadBlockUnsafe(dev_addr_base, dest_buffer, copy_amount);
- } else {
- u8* physical = memory.GetPointer<u8>(dev_addr_base);
+ // A continuous big page may still have no host memory (a Switch 2 region backed as it is
+ // written, while its backing is pending): the per-page copy handles that.
+ u8* const physical = IsBigPageContinuous(page_index) ? memory.GetPointer<u8>(dev_addr_base)
+ : nullptr;
+ if (physical != nullptr) [[likely]] {
std::memcpy(dest_buffer, physical, copy_amount);
+ } else {
+ memory.ReadBlockUnsafe(dev_addr_base, dest_buffer, copy_amount);
}
dest_buffer = static_cast<u8*>(dest_buffer) + copy_amount;
};
@@ -466,7 +469,7 @@ void MemoryManager::ReadBlockUnsafe(GPUVAddr gpu_src_addr, void* dest_buffer, co
void MemoryManager::WriteBlockImpl(GPUVAddr gpu_dest_addr, const void* src_buffer, std::size_t size, [[maybe_unused]] VideoCommon::CacheType which, bool unsafe) {
// Give Switch 2 sparse pages the write lands on their memory first (otherwise it is dropped).
- Tegra::Nx2BackSparseWrite(gpu_dest_addr, size);
+ Tegra::Nx2BackSparseWrite(gpu_dest_addr, size, src_buffer);
auto just_advance = [&]([[maybe_unused]] std::size_t page_index, [[maybe_unused]] std::size_t offset, std::size_t copy_amount) {
src_buffer = static_cast<const u8*>(src_buffer) + copy_amount;
};
@@ -485,11 +488,12 @@ void MemoryManager::WriteBlockImpl(GPUVAddr gpu_dest_addr, const void* src_buffe
if (!unsafe) {
rasterizer->InvalidateRegion(dev_addr_base, copy_amount, which);
}
- if (!IsBigPageContinuous(page_index)) [[unlikely]] {
- memory.WriteBlockUnsafe(dev_addr_base, src_buffer, copy_amount);
- } else {
- u8* physical = memory.GetPointer<u8>(dev_addr_base);
+ u8* const physical = IsBigPageContinuous(page_index) ? memory.GetPointer<u8>(dev_addr_base)
+ : nullptr;
+ if (physical != nullptr) [[likely]] {
std::memcpy(physical, src_buffer, copy_amount);
+ } else {
+ memory.WriteBlockUnsafe(dev_addr_base, src_buffer, copy_amount);
}
src_buffer = static_cast<const u8*>(src_buffer) + copy_amount;
};
diff --git a/src/video_core/nx2.h b/src/video_core/nx2.h
index 824dc76044..92714d52d2 100644
--- a/src/video_core/nx2.h
+++ b/src/video_core/nx2.h
@@ -9,6 +9,7 @@
#include <functional>
#include <map>
#include <mutex>
+#include <utility>
#include <vector>
#include "common/common_types.h"
@@ -19,6 +20,76 @@ namespace Tegra {
// Ampere classes and the Turing+ macro ISA, which Eden's Maxwell engines don't implement.
inline std::atomic<bool> g_nx2_ampere{false};
+// Device pages the GPU has written through the buffer cache (shader stores, copies), ever. Title 1
+// runs command lists the GPU writes each frame (20-byte entries its culling fills in); read
+// without a flush they are stale and the draws behind them (trees) go missing. The bits never
+// clear: the dispatch that rewrites a page may still be queued on the split's back when its front
+// reads the command list. One bit per 4 KiB of the 36-bit device address space (2 MiB).
+inline constexpr u64 Nx2WrittenPageBits = 12;
+inline std::array<std::atomic<u64>, (u64{1} << (36 - Nx2WrittenPageBits)) / 64> g_nx2_gpu_written_pages{};
+
+inline void Nx2MarkGpuWritten(u64 device_address, u64 size) {
+ if (size == 0) {
+ return;
+ }
+ const u64 last = std::min<u64>((device_address + size - 1) >> Nx2WrittenPageBits,
+ g_nx2_gpu_written_pages.size() * 64 - 1);
+ for (u64 page = device_address >> Nx2WrittenPageBits; page <= last; ++page) {
+ auto& word = g_nx2_gpu_written_pages[page / 64];
+ const u64 bit = u64{1} << (page % 64);
+ if ((word.load(std::memory_order_relaxed) & bit) == 0) {
+ word.fetch_or(bit, std::memory_order_relaxed);
+ }
+ }
+}
+
+// GPU-written device pages the front has read command lists from (same layout as above). The
+// buffer cache copies these pages into host memory right after the compute or copy work that
+// writes them and submits, so the front reads the copy once the GPU is done instead of draining
+// the split and flushing the caches (BufferCache::Nx2ReadListCopy).
+inline std::array<std::atomic<u64>, (u64{1} << (36 - Nx2WrittenPageBits)) / 64> g_nx2_command_list_pages{};
+inline std::atomic<bool> g_nx2_has_command_list_pages{false};
+
+inline void Nx2MarkCommandListPage(u64 device_address) {
+ const u64 page = device_address >> Nx2WrittenPageBits;
+ if (page >= g_nx2_command_list_pages.size() * 64) {
+ return;
+ }
+ auto& word = g_nx2_command_list_pages[page / 64];
+ const u64 bit = u64{1} << (page % 64);
+ if ((word.load(std::memory_order_relaxed) & bit) == 0) {
+ word.fetch_or(bit, std::memory_order_relaxed);
+ g_nx2_has_command_list_pages.store(true, std::memory_order_relaxed);
+ }
+}
+
+[[nodiscard]] inline bool Nx2IsCommandListPage(u64 device_address) {
+ const u64 page = device_address >> Nx2WrittenPageBits;
+ if (page >= g_nx2_command_list_pages.size() * 64) {
+ return false;
+ }
+ return (g_nx2_command_list_pages[page / 64].load(std::memory_order_relaxed) >> (page % 64)) & 1;
+}
+
+/// A new emulation session starts: no page has been written by its GPU yet.
+inline void Nx2ClearGpuWrittenPages() {
+ for (auto& word : g_nx2_gpu_written_pages) {
+ word.store(0, std::memory_order_relaxed);
+ }
+ for (auto& word : g_nx2_command_list_pages) {
+ word.store(0, std::memory_order_relaxed);
+ }
+ g_nx2_has_command_list_pages.store(false, std::memory_order_relaxed);
+}
+
+[[nodiscard]] inline bool Nx2IsGpuWritten(u64 device_address) {
+ const u64 page = device_address >> Nx2WrittenPageBits;
+ if (page >= g_nx2_gpu_written_pages.size() * 64) {
+ return false;
+ }
+ return (g_nx2_gpu_written_pages[page / 64].load(std::memory_order_relaxed) >> (page % 64)) & 1;
+}
+
// NVN2 queues release their progress values into the second page of a two-page GPU-accessible
// region and GPU acquires read the same offsets in the first page (the CPU polls the second).
// Something on the console copies one into the other; until that is known, the puller mirrors
@@ -58,8 +129,9 @@ inline void Nx2ClearSemaphoreRegions() {
// Switch 2 sparse regions (nx2:gpu 214, word 7 bit 0x200: title 1's terrain texture arrays and
// layer pools, 170 MB-5.8 GB each, of which the game fills a little) get memory per block when the
// GPU first writes there, not in full. nx2:gpu installs the handler and the span its regions cover;
-// the GPU memory manager calls Nx2BackSparseWrite before each write.
-using Nx2SparseBackingHandler = std::function<void(u64 gpu_address, u64 size)>;
+// the GPU memory manager calls Nx2BackSparseWrite before each write, with the bytes it writes
+// (blocks nobody wrote that only get zeros need no memory: they read zero already).
+using Nx2SparseBackingHandler = std::function<void(u64 gpu_address, u64 size, const u8* data)>;
inline std::recursive_mutex g_nx2_sparse_backing_mutex;
inline Nx2SparseBackingHandler g_nx2_sparse_backing;
inline std::atomic<u64> g_nx2_sparse_low{~u64{0}};
@@ -83,14 +155,14 @@ inline void Nx2AddSparseSpan(u64 gpu_address, u64 size) {
}
}
-inline void Nx2BackSparseWrite(u64 gpu_address, u64 size) {
+inline void Nx2BackSparseWrite(u64 gpu_address, u64 size, const void* data) {
if (gpu_address + size <= g_nx2_sparse_low.load(std::memory_order_relaxed) ||
gpu_address >= g_nx2_sparse_high.load(std::memory_order_relaxed)) [[likely]] {
return;
}
std::scoped_lock lk{g_nx2_sparse_backing_mutex};
if (g_nx2_sparse_backing) {
- g_nx2_sparse_backing(gpu_address, size);
+ g_nx2_sparse_backing(gpu_address, size, static_cast<const u8*>(data));
}
}
@@ -101,7 +173,13 @@ struct Nx2SparseRegionState {
u64 size;
u64 block_size;
std::vector<bool> written;
+ /// Once the game maps its own memory into the region (nx2:gpu 216): which tiles it maps.
+ /// Only those hold data then (writes elsewhere are dropped, as on the console).
+ std::vector<bool> mapped;
};
+
+/// The granularity of the game's sparse tiles (nx2:gpu 216 maps 64 KiB pages).
+inline constexpr u64 Nx2SparseTileSize = 64ULL << 10;
inline std::mutex g_nx2_sparse_state_mutex;
inline std::map<u64, Nx2SparseRegionState> g_nx2_sparse_state;
@@ -129,6 +207,68 @@ inline void Nx2SparseBlockWritten(u64 region_address, u64 block) {
}
}
+/// True unless the block of the region at region_address is known to hold nothing (no write from
+/// the CPU or the GPU yet).
+inline bool Nx2SparseBlockIsWritten(u64 region_address, u64 block) {
+ std::scoped_lock lk{g_nx2_sparse_state_mutex};
+ const auto it = g_nx2_sparse_state.find(region_address);
+ return it == g_nx2_sparse_state.end() || block >= it->second.written.size() ||
+ it->second.written[block];
+}
+
+/// The game maps its own memory over [offset, +size) of the region at region_address (mapped), or
+/// unmaps it there.
+inline void Nx2SparseTilesMapped(u64 region_address, u64 offset, u64 size, bool mapped) {
+ std::scoped_lock lk{g_nx2_sparse_state_mutex};
+ const auto it = g_nx2_sparse_state.find(region_address);
+ if (it == g_nx2_sparse_state.end() || size == 0) {
+ return;
+ }
+ auto& region = it->second;
+ if (region.mapped.empty()) {
+ region.mapped.resize((region.size + Nx2SparseTileSize - 1) / Nx2SparseTileSize);
+ }
+ const u64 last = (std::min)((offset + size - 1) / Nx2SparseTileSize, u64{region.mapped.size() - 1});
+ for (u64 tile = offset / Nx2SparseTileSize; tile <= last; ++tile) {
+ region.mapped[tile] = mapped;
+ }
+}
+
+/// The parts of [gpu_address, +size), which lies in one sparse region, that can hold data: the
+/// tiles the game maps there if it maps any, otherwise the blocks written. As (offset from
+/// gpu_address, size) runs.
+inline std::vector<std::pair<u64, u64>> Nx2SparseDataRanges(u64 gpu_address, u64 size) {
+ std::vector<std::pair<u64, u64>> ranges;
+ std::scoped_lock lk{g_nx2_sparse_state_mutex};
+ auto it = g_nx2_sparse_state.upper_bound(gpu_address);
+ if (it == g_nx2_sparse_state.begin()) {
+ return ranges;
+ }
+ --it;
+ const auto& [start, region] = *it;
+ if (size == 0 || gpu_address + size > start + region.size) {
+ return ranges;
+ }
+ const bool by_tile = !region.mapped.empty();
+ const u64 unit = by_tile ? Nx2SparseTileSize : region.block_size;
+ const std::vector<bool>& present = by_tile ? region.mapped : region.written;
+ const u64 begin = gpu_address - start;
+ const u64 end = begin + size;
+ for (u64 index = begin / unit; index * unit < end && index < present.size(); ++index) {
+ if (!present[index]) {
+ continue;
+ }
+ const u64 run_begin = (std::max)(index * unit, begin) - begin;
+ const u64 run_end = (std::min)((index + 1) * unit, end) - begin;
+ if (!ranges.empty() && ranges.back().first + ranges.back().second == run_begin) {
+ ranges.back().second += run_end - run_begin;
+ } else {
+ ranges.emplace_back(run_begin, run_end - run_begin);
+ }
+ }
+ return ranges;
+}
+
/// True when [gpu_address, +size) lies in one sparse region.
inline bool Nx2InSparseRegion(u64 gpu_address, u64 size) {
if (size == 0 || gpu_address + size <= g_nx2_sparse_low.load(std::memory_order_relaxed) ||
diff --git a/src/video_core/rasterizer_interface.h b/src/video_core/rasterizer_interface.h
index b527c0e2a9..e9eaa2b3c2 100644
--- a/src/video_core/rasterizer_interface.h
+++ b/src/video_core/rasterizer_interface.h
@@ -6,6 +6,7 @@
#pragma once
+#include <array>
#include <functional>
#include <optional>
#include <span>
@@ -93,6 +94,20 @@ public:
virtual void FlushRegion(DAddr addr, u64 size,
VideoCommon::CacheType which = VideoCommon::CacheType::All) = 0;
+ /// Switch 2, from the GPU thread's front: where a GPU-written command list's words are.
+ enum class GpuWrittenList {
+ Unknown, ///< Neither: the caller drains, flushes the page and notes it synced
+ Copied, ///< `out` (within one 4 KiB page) is filled from the copy made after its writer
+ Synced, ///< Guest memory holds the page: no GPU work wrote it since it was flushed
+ };
+ virtual GpuWrittenList ReadGpuWrittenCommandList([[maybe_unused]] DAddr addr,
+ [[maybe_unused]] std::span<u8> out) {
+ return GpuWrittenList::Unknown;
+ }
+
+ /// Switch 2: the front flushed this command list page with the split drained.
+ virtual void NoteGpuWrittenCommandListSynced([[maybe_unused]] DAddr page) {}
+
/// Check if the the specified memory area requires flushing to CPU Memory.
virtual bool MustFlushRegion(DAddr addr, u64 size,
VideoCommon::CacheType which = VideoCommon::CacheType::All) = 0;
@@ -162,6 +177,16 @@ public:
return false;
}
+ /// Stand-in for a DLSS evaluation: temporal upscale of src into dst along motion_vectors
+ /// (jitter in src pixels; motion_scale turns a motion vector into a UV offset to the previous
+ /// frame). False when unsupported; the caller then falls back to AccelerateSurfaceCopy.
+ [[nodiscard]] virtual bool AccelerateTemporalUpscale(
+ const Tegra::Engines::Fermi2D::Surface& src, const Tegra::Engines::Fermi2D::Surface& dst,
+ const Tegra::Engines::Fermi2D::Surface& motion_vectors, std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale) {
+ return false;
+ }
+
[[nodiscard]] virtual Tegra::Engines::AccelerateDMAInterface& AccessAccelerateDMA() = 0;
virtual void AccelerateInlineToMemory(GPUVAddr address, size_t copy_size,
diff --git a/src/video_core/renderer_opengl/gl_compute_pipeline.cpp b/src/video_core/renderer_opengl/gl_compute_pipeline.cpp
index f0a7baf9aa..5fc10cfce2 100644
--- a/src/video_core/renderer_opengl/gl_compute_pipeline.cpp
+++ b/src/video_core/renderer_opengl/gl_compute_pipeline.cpp
@@ -12,6 +12,7 @@
#include "video_core/renderer_opengl/gl_compute_pipeline.h"
#include "video_core/renderer_opengl/gl_shader_manager.h"
#include "video_core/renderer_opengl/gl_shader_util.h"
+#include "video_core/shader_handle_program.h"
namespace OpenGL {
@@ -105,6 +106,19 @@ void ComputePipeline::Configure() {
const auto& cbufs{qmd.const_buffer_config};
const bool via_header_index{qmd.linked_tsc != 0};
const auto read_handle{[&](const auto& desc, u32 index) {
+ if (desc.handle_program != ~0U) {
+ // Read by the shader from a storage buffer: computed for this dispatch.
+ return TexturePair(VideoCommon::EvaluateTextureHandle(
+ info, desc.handle_program, *gpu_memory,
+ [&](u32 cbuf) -> GPUVAddr {
+ return cbuf < cbufs.size() &&
+ ((qmd.const_buffer_enable_mask >> cbuf) &
+ 1) != 0
+ ? cbufs[cbuf].Address()
+ : GPUVAddr{};
+ }),
+ via_header_index);
+ }
ASSERT(((qmd.const_buffer_enable_mask >> desc.cbuf_index) & 1) != 0);
const u32 index_offset{index << desc.size_shift};
const u32 offset{desc.cbuf_offset + index_offset};
diff --git a/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp b/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp
index ecd6544b10..4e02f0569b 100644
--- a/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp
+++ b/src/video_core/renderer_opengl/gl_graphics_pipeline.cpp
@@ -17,6 +17,7 @@
#include "video_core/renderer_opengl/gl_shader_manager.h"
#include "video_core/renderer_opengl/gl_shader_util.h"
#include "video_core/renderer_opengl/gl_state_tracker.h"
+#include "video_core/shader_handle_program.h"
#include "video_core/shader_notify.h"
#include "video_core/texture_cache/texture_cache.h"
@@ -308,6 +309,17 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) {
}
const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
const auto read_handle{[&](const auto& desc, u32 index) {
+ if (desc.handle_program != ~0U) {
+ // Read by the shader from a storage buffer: computed for this draw.
+ return TexturePair(VideoCommon::EvaluateTextureHandle(
+ info, desc.handle_program, *gpu_memory,
+ [&](u32 cbuf) -> GPUVAddr {
+ return cbuf < cbufs.size() && cbufs[cbuf].enabled
+ ? cbufs[cbuf].address
+ : GPUVAddr{};
+ }),
+ via_header_index);
+ }
ASSERT(cbufs[desc.cbuf_index].enabled);
const u32 index_offset{index << desc.size_shift};
const u32 offset{desc.cbuf_offset + index_offset};
diff --git a/src/video_core/renderer_opengl/gl_shader_cache.cpp b/src/video_core/renderer_opengl/gl_shader_cache.cpp
index 277cf222fa..fd27fb1805 100644
--- a/src/video_core/renderer_opengl/gl_shader_cache.cpp
+++ b/src/video_core/renderer_opengl/gl_shader_cache.cpp
@@ -54,7 +54,7 @@ using VideoCommon::LoadPipelines;
using VideoCommon::SerializePipeline;
using Context = ShaderContext::Context;
-constexpr u32 CACHE_VERSION = 15;
+constexpr u32 CACHE_VERSION = 16;
template <typename Container>
auto MakeSpan(Container& container) {
@@ -578,7 +578,8 @@ std::unique_ptr<ComputePipeline> ShaderCache::CreateComputePipeline(
const ComputePipelineKey& key, const VideoCommon::ShaderInfo* shader) {
const GPUVAddr program_base{kepler_compute->regs.code_loc.Address()};
const auto& qmd{kepler_compute->launch_description};
- ComputeEnvironment env{*kepler_compute, *gpu_memory, program_base, qmd.program_start};
+ ComputeEnvironment env{*kepler_compute, *gpu_memory, program_base, qmd.program_start,
+ shader->lowered_native};
env.SetCachedSize(shader->size_bytes);
main_pools.ReleaseContents();
diff --git a/src/video_core/renderer_opengl/present/layer.cpp b/src/video_core/renderer_opengl/present/layer.cpp
index 9e4a0f854a..2656892f4a 100644
--- a/src/video_core/renderer_opengl/present/layer.cpp
+++ b/src/video_core/renderer_opengl/present/layer.cpp
@@ -183,6 +183,11 @@ void Layer::ConfigureFramebufferTexture(const Tegra::FramebufferConfig& framebuf
framebuffer_texture.gl_format = GL_RGBA;
framebuffer_texture.gl_type = GL_UNSIGNED_INT_8_8_8_8_REV;
break;
+ case Service::android::PixelFormat::Rgba1010102:
+ internal_format = GL_RGB10_A2;
+ framebuffer_texture.gl_format = GL_RGBA;
+ framebuffer_texture.gl_type = GL_UNSIGNED_INT_2_10_10_10_REV;
+ break;
case Service::android::PixelFormat::Rgb565:
internal_format = GL_RGB565;
framebuffer_texture.gl_format = GL_RGB;
diff --git a/src/video_core/renderer_vulkan/blit_image.cpp b/src/video_core/renderer_vulkan/blit_image.cpp
index ddd52e9426..506daef11e 100644
--- a/src/video_core/renderer_vulkan/blit_image.cpp
+++ b/src/video_core/renderer_vulkan/blit_image.cpp
@@ -11,6 +11,7 @@
#include "common/div_ceil.h"
#include "common/settings.h"
#include "video_core/host_shaders/blit_color_float_frag_spv.h"
+#include "video_core/host_shaders/blit_color_srgb_to_linear_frag_spv.h"
#include "video_core/host_shaders/blit_color_msaa_frag_spv.h"
#include "video_core/host_shaders/blit_depth_frag_spv.h"
#include "video_core/host_shaders/blit_depth_msaa_frag_spv.h"
@@ -33,6 +34,7 @@
#include "video_core/host_shaders/convert_non_msaa_to_msaa_depth_stencil_frag_spv.h"
#include "video_core/host_shaders/convert_s8d24_to_abgr8_frag_spv.h"
#include "video_core/host_shaders/full_screen_triangle_vert_spv.h"
+#include "video_core/host_shaders/temporal_upscale_frag_spv.h"
#include "video_core/host_shaders/vulkan_blit_depth_stencil_frag_spv.h"
#include "video_core/host_shaders/vulkan_color_clear_frag_spv.h"
#include "video_core/host_shaders/vulkan_color_clear_vert_spv.h"
@@ -91,6 +93,15 @@ struct PushConstants {
std::array<float, 2> tex_offset;
};
+// The vertex stage's PushConstants first, then those of temporal_upscale.frag.
+struct TemporalUpscalePushConstants {
+ PushConstants vertex;
+ std::array<float, 2> jitter;
+ std::array<float, 2> motion_scale;
+ float current_weight;
+ float padding;
+};
+
struct MSAACopyPushConstants {
std::array<s32, 2> dst_offset;
std::array<s32, 2> src_offset;
@@ -109,6 +120,11 @@ constexpr std::array TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS{
TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<0>,
TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<1>,
};
+constexpr std::array THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS{
+ TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<0>,
+ TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<1>,
+ TEXTURE_DESCRIPTOR_SET_LAYOUT_BINDING<2>,
+};
constexpr VkDescriptorSetLayoutCreateInfo ONE_TEXTURE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO{
.sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
.pNext = nullptr,
@@ -133,6 +149,13 @@ constexpr VkDescriptorSetLayoutCreateInfo TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_CRE
.bindingCount = static_cast<u32>(TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.size()),
.pBindings = TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.data(),
};
+constexpr VkDescriptorSetLayoutCreateInfo THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_CREATE_INFO{
+ .sType = VK_STRUCTURE_TYPE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .bindingCount = static_cast<u32>(THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.size()),
+ .pBindings = THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_BINDINGS.data(),
+};
template <VkShaderStageFlags stageFlags, size_t size>
inline constexpr VkPushConstantRange PUSH_CONSTANT_RANGE{
.stageFlags = stageFlags,
@@ -357,6 +380,32 @@ void UpdateOneTextureDescriptorSet(const Device& device, VkDescriptorSet descrip
device.GetLogical().UpdateDescriptorSets(write_descriptor_set, nullptr);
}
+void UpdateThreeTexturesDescriptorSet(const Device& device, VkDescriptorSet descriptor_set,
+ VkSampler sampler, std::array<VkImageView, 3> image_views) {
+ std::array<VkDescriptorImageInfo, 3> image_infos{};
+ std::array<VkWriteDescriptorSet, 3> write_descriptor_sets{};
+ for (u32 binding = 0; binding < 3; ++binding) {
+ image_infos[binding] = VkDescriptorImageInfo{
+ .sampler = sampler,
+ .imageView = image_views[binding],
+ .imageLayout = VK_IMAGE_LAYOUT_GENERAL,
+ };
+ write_descriptor_sets[binding] = VkWriteDescriptorSet{
+ .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET,
+ .pNext = nullptr,
+ .dstSet = descriptor_set,
+ .dstBinding = binding,
+ .dstArrayElement = 0,
+ .descriptorCount = 1,
+ .descriptorType = VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER,
+ .pImageInfo = &image_infos[binding],
+ .pBufferInfo = nullptr,
+ .pTexelBufferView = nullptr,
+ };
+ }
+ device.GetLogical().UpdateDescriptorSets(write_descriptor_sets, nullptr);
+}
+
void UpdateTwoTexturesDescriptorSet(const Device& device, VkDescriptorSet descriptor_set,
VkSampler sampler, VkImageView image_view_0,
VkImageView image_view_1) {
@@ -590,10 +639,14 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_,
ONE_TEXTURE_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)),
two_textures_set_layout(device.GetLogical().CreateDescriptorSetLayout(
TWO_TEXTURES_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)),
+ three_textures_set_layout(device.GetLogical().CreateDescriptorSetLayout(
+ THREE_TEXTURES_DESCRIPTOR_SET_LAYOUT_CREATE_INFO)),
one_texture_descriptor_allocator{
descriptor_pool.Allocator(device_, scheduler_, *one_texture_set_layout, TEXTURE_DESCRIPTOR_BANK_INFO<1>)},
two_textures_descriptor_allocator{
descriptor_pool.Allocator(device_, scheduler_, *two_textures_set_layout, TEXTURE_DESCRIPTOR_BANK_INFO<2>)},
+ three_textures_descriptor_allocator{
+ descriptor_pool.Allocator(device_, scheduler_, *three_textures_set_layout, TEXTURE_DESCRIPTOR_BANK_INFO<3>)},
one_texture_pipeline_layout(device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo(
one_texture_set_layout.address(),
PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_VERTEX_BIT, sizeof(PushConstants)>))),
@@ -601,6 +654,11 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_,
device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo(
two_textures_set_layout.address(),
PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_VERTEX_BIT, sizeof(PushConstants)>))),
+ temporal_upscale_pipeline_layout(
+ device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo(
+ three_textures_set_layout.address(),
+ PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
+ sizeof(TemporalUpscalePushConstants)>))),
clear_color_pipeline_layout(device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo(
nullptr, PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(float) * 4>))),
msaa_copy_pipeline_layout(device.GetLogical().CreatePipelineLayout(PipelineLayoutCreateInfo(
@@ -612,6 +670,8 @@ BlitImageHelper::BlitImageHelper(const Device& device_, Scheduler& scheduler_,
PUSH_CONSTANT_RANGE<VK_SHADER_STAGE_FRAGMENT_BIT, sizeof(MSAACopyPushConstants)>))),
full_screen_vert(BuildShader(device, FULL_SCREEN_TRIANGLE_VERT_SPV)),
blit_color_to_color_frag(BuildShader(device, BLIT_COLOR_FLOAT_FRAG_SPV)),
+ temporal_upscale_frag(BuildShader(device, TEMPORAL_UPSCALE_FRAG_SPV)),
+ blit_color_srgb_to_linear_frag(BuildShader(device, BLIT_COLOR_SRGB_TO_LINEAR_FRAG_SPV)),
blit_color_msaa_frag(BuildShader(device, BLIT_COLOR_MSAA_FRAG_SPV)),
blit_depth_stencil_frag(device.IsExtShaderStencilExportSupported()
? BuildShader(device, VULKAN_BLIT_DEPTH_STENCIL_FRAG_SPV)
@@ -702,6 +762,42 @@ void BlitImageHelper::BlitColor(const Framebuffer* dst_framebuffer, VkImageView
});
}
+void BlitImageHelper::TemporalUpscale(const Framebuffer* dst_framebuffer,
+ const ImageView& input_color, const ImageView& motion_vectors,
+ VkImageView history, const Region2D& dst_region,
+ std::array<f32, 2> jitter, std::array<f32, 2> motion_scale,
+ f32 current_weight) {
+ const VkPipelineLayout layout = *temporal_upscale_pipeline_layout;
+ const VkPipeline pipeline = FindOrEmplaceTemporalUpscalePipeline(dst_framebuffer->RenderPass());
+ const VkSampler sampler = *linear_sampler;
+ const std::array<VkImageView, 3> views{input_color.Handle(Shader::TextureType::Color2D),
+ motion_vectors.Handle(Shader::TextureType::Color2D),
+ history};
+ const TemporalUpscalePushConstants push_constants{
+ .vertex = {.tex_scale = {1.0f, 1.0f}, .tex_offset = {0.0f, 0.0f}},
+ .jitter = jitter,
+ .motion_scale = motion_scale,
+ .current_weight = current_weight,
+ .padding = 0.0f,
+ };
+ RecordShaderReadBarrier(scheduler, input_color);
+ RecordShaderReadBarrier(scheduler, motion_vectors);
+ scheduler.RequestRenderpass(dst_framebuffer);
+ scheduler.Record([this, dst_region, pipeline, layout, sampler, views,
+ push_constants](vk::CommandBuffer cmdbuf) {
+ const VkDescriptorSet descriptor_set = three_textures_descriptor_allocator.Commit();
+ UpdateThreeTexturesDescriptorSet(device, descriptor_set, sampler, views);
+ cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_GRAPHICS, pipeline);
+ cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_GRAPHICS, layout, 0, descriptor_set,
+ nullptr);
+ BindBlitState(cmdbuf, dst_region);
+ cmdbuf.PushConstants(layout, VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT,
+ push_constants);
+ cmdbuf.Draw(3, 1, 0, 0);
+ });
+ scheduler.InvalidateState();
+}
+
void BlitImageHelper::BlitImpl(const Framebuffer* dst_framebuffer,
const ImageView& src_image_view, const Region2D& dst_region,
const Region2D& src_region, VkPipeline pipeline, VkSampler sampler,
@@ -1237,7 +1333,10 @@ VkPipeline BlitImageHelper::FindOrEmplaceColorPipeline(const BlitImagePipelineKe
}
blit_color_keys.push_back(key);
- const std::array stages = MakeStages(*full_screen_vert, *blit_color_to_color_frag);
+ const bool srgb_to_linear =
+ key.operation == Tegra::Engines::Fermi2D::Operation::SrcCopySrgbToLinear;
+ const std::array stages = MakeStages(
+ *full_screen_vert, srgb_to_linear ? *blit_color_srgb_to_linear_frag : *blit_color_to_color_frag);
const VkPipelineColorBlendAttachmentState blend_attachment{
.blendEnable = VK_FALSE,
.srcColorBlendFactor = VK_BLEND_FACTOR_ZERO,
@@ -1285,6 +1384,59 @@ VkPipeline BlitImageHelper::FindOrEmplaceColorPipeline(const BlitImagePipelineKe
return *blit_color_pipelines.back();
}
+VkPipeline BlitImageHelper::FindOrEmplaceTemporalUpscalePipeline(VkRenderPass renderpass) {
+ const auto it = std::ranges::find(temporal_upscale_keys, renderpass);
+ if (it != temporal_upscale_keys.end()) {
+ return *temporal_upscale_pipelines[std::distance(temporal_upscale_keys.begin(), it)];
+ }
+ temporal_upscale_keys.push_back(renderpass);
+ const std::array stages = MakeStages(*full_screen_vert, *temporal_upscale_frag);
+ const VkPipelineColorBlendAttachmentState blend_attachment{
+ .blendEnable = VK_FALSE,
+ .srcColorBlendFactor = VK_BLEND_FACTOR_ZERO,
+ .dstColorBlendFactor = VK_BLEND_FACTOR_ZERO,
+ .colorBlendOp = VK_BLEND_OP_ADD,
+ .srcAlphaBlendFactor = VK_BLEND_FACTOR_ZERO,
+ .dstAlphaBlendFactor = VK_BLEND_FACTOR_ZERO,
+ .alphaBlendOp = VK_BLEND_OP_ADD,
+ .colorWriteMask = VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT |
+ VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT,
+ };
+ const VkPipelineColorBlendStateCreateInfo color_blend_create_info{
+ .sType = VK_STRUCTURE_TYPE_PIPELINE_COLOR_BLEND_STATE_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .logicOpEnable = VK_FALSE,
+ .logicOp = VK_LOGIC_OP_CLEAR,
+ .attachmentCount = 1,
+ .pAttachments = &blend_attachment,
+ .blendConstants = {0.0f, 0.0f, 0.0f, 0.0f},
+ };
+ const VkPipelineInputAssemblyStateCreateInfo input_assembly_ci = GetPipelineInputAssemblyStateCreateInfo(device);
+ temporal_upscale_pipelines.push_back(device.GetLogical().CreateGraphicsPipeline({
+ .sType = VK_STRUCTURE_TYPE_GRAPHICS_PIPELINE_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .stageCount = static_cast<u32>(stages.size()),
+ .pStages = stages.data(),
+ .pVertexInputState = &PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO,
+ .pInputAssemblyState = &input_assembly_ci,
+ .pTessellationState = nullptr,
+ .pViewportState = &PIPELINE_VIEWPORT_STATE_CREATE_INFO,
+ .pRasterizationState = &PIPELINE_RASTERIZATION_STATE_CREATE_INFO,
+ .pMultisampleState = &PIPELINE_MULTISAMPLE_STATE_CREATE_INFO,
+ .pDepthStencilState = nullptr,
+ .pColorBlendState = &color_blend_create_info,
+ .pDynamicState = &PIPELINE_DYNAMIC_STATE_CREATE_INFO,
+ .layout = *temporal_upscale_pipeline_layout,
+ .renderPass = renderpass,
+ .subpass = 0,
+ .basePipelineHandle = VK_NULL_HANDLE,
+ .basePipelineIndex = 0,
+ }, device.StaticPipelineCache()));
+ return *temporal_upscale_pipelines.back();
+}
+
VkPipeline BlitImageHelper::FindOrEmplaceDepthStencilPipeline(const BlitImagePipelineKey& key) {
const auto it = std::ranges::find(blit_depth_stencil_keys, key);
if (it != blit_depth_stencil_keys.end()) {
diff --git a/src/video_core/renderer_vulkan/blit_image.h b/src/video_core/renderer_vulkan/blit_image.h
index 62e6743a23..4741f9fe45 100644
--- a/src/video_core/renderer_vulkan/blit_image.h
+++ b/src/video_core/renderer_vulkan/blit_image.h
@@ -82,6 +82,13 @@ public:
VkImage src_image, VkSampler src_sampler, const Region2D& dst_region,
const Region2D& src_region, const Extent3D& src_size);
+ /// Temporal upscale (temporal_upscale.frag) into dst_framebuffer: the jittered input colour,
+ /// accumulated with the history image reprojected along the motion vectors.
+ void TemporalUpscale(const Framebuffer* dst_framebuffer, const ImageView& input_color,
+ const ImageView& motion_vectors, VkImageView history,
+ const Region2D& dst_region, std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale, f32 current_weight);
+
void BlitColorMSAA(const Framebuffer* dst_framebuffer, const ImageView& src_image_view,
const Region2D& dst_region, const Region2D& src_region);
@@ -170,6 +177,8 @@ private:
[[nodiscard]] VkPipeline FindOrEmplaceColorPipeline(const BlitImagePipelineKey& key);
+ [[nodiscard]] VkPipeline FindOrEmplaceTemporalUpscalePipeline(VkRenderPass renderpass);
+
[[nodiscard]] VkPipeline FindOrEmplaceDepthStencilPipeline(const BlitImagePipelineKey& key);
[[nodiscard]] VkPipeline FindOrEmplaceClearColorPipeline(const BlitImagePipelineKey& key);
@@ -207,15 +216,20 @@ private:
vk::DescriptorSetLayout one_texture_set_layout;
vk::DescriptorSetLayout two_textures_set_layout;
+ vk::DescriptorSetLayout three_textures_set_layout;
DescriptorAllocator one_texture_descriptor_allocator;
DescriptorAllocator two_textures_descriptor_allocator;
+ DescriptorAllocator three_textures_descriptor_allocator;
vk::PipelineLayout one_texture_pipeline_layout;
vk::PipelineLayout two_textures_pipeline_layout;
+ vk::PipelineLayout temporal_upscale_pipeline_layout;
vk::PipelineLayout clear_color_pipeline_layout;
vk::PipelineLayout msaa_copy_pipeline_layout;
vk::PipelineLayout msaa_copy_depth_stencil_pipeline_layout;
vk::ShaderModule full_screen_vert;
vk::ShaderModule blit_color_to_color_frag;
+ vk::ShaderModule temporal_upscale_frag;
+ vk::ShaderModule blit_color_srgb_to_linear_frag;
vk::ShaderModule blit_color_msaa_frag;
vk::ShaderModule blit_depth_stencil_frag;
vk::ShaderModule blit_depth_frag;
@@ -246,6 +260,8 @@ private:
std::vector<BlitImagePipelineKey> blit_color_keys;
std::vector<vk::Pipeline> blit_color_pipelines;
+ std::vector<VkRenderPass> temporal_upscale_keys;
+ std::vector<vk::Pipeline> temporal_upscale_pipelines;
std::vector<BlitImagePipelineKey> blit_depth_stencil_keys;
std::vector<vk::Pipeline> blit_depth_stencil_pipelines;
std::vector<BlitImagePipelineKey> clear_color_keys;
diff --git a/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp b/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp
index 9dac98c830..76a564aae9 100644
--- a/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp
+++ b/src/video_core/renderer_vulkan/fixed_pipeline_state.cpp
@@ -14,6 +14,7 @@
#include "common/settings.h"
#include "video_core/engines/maxwell_3d.h"
#include "video_core/renderer_vulkan/fixed_pipeline_state.h"
+#include "video_core/renderer_vulkan/vertex_repack.h"
#include "video_core/renderer_vulkan/vk_state_tracker.h"
namespace Vulkan {
@@ -268,6 +269,18 @@ void FixedPipelineState::Refresh(Tegra::Engines::Maxwell3D& maxwell3d, DynamicFe
if (xfb_enabled) {
RefreshXfbState(xfb_state, regs);
}
+ if (!dynamic_vertex_input && features.has_static_vertex_repack) {
+ const u32 repack_mask = VertexRepackMask(regs);
+ // Refresh from guest values even when only the stride changed: offsets must not accumulate.
+ for (size_t index = 0; index < attributes.size(); ++index) {
+ const auto& input = regs.vertex_attrib_format[index];
+ attributes[index].offset.Assign(RepackedVertexOffset(repack_mask, input.buffer, input.offset));
+ }
+ for (u32 binding = 0; binding < vertex_strides.size(); ++binding) {
+ vertex_strides[binding] = static_cast<u16>(RepackedVertexStride(
+ repack_mask, binding, regs.vertex_streams[binding].stride));
+ }
+ }
}
void FixedPipelineState::BlendingAttachment::Refresh(const Maxwell& regs, size_t index) {
diff --git a/src/video_core/renderer_vulkan/fixed_pipeline_state.h b/src/video_core/renderer_vulkan/fixed_pipeline_state.h
index 36a4b4b7a2..1fe5cd75f3 100644
--- a/src/video_core/renderer_vulkan/fixed_pipeline_state.h
+++ b/src/video_core/renderer_vulkan/fixed_pipeline_state.h
@@ -33,6 +33,7 @@ struct DynamicFeatures {
bool has_dynamic_state3_logic_op_enable;
bool has_dynamic_state3_line_stipple_enable;
bool has_dynamic_vertex_input;
+ bool has_static_vertex_repack;
bool has_color_write_enable;
bool has_provoking_vertex;
bool has_provoking_vertex_first_mode;
diff --git a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp
index c8e43d0650..e03e5786fe 100644
--- a/src/video_core/renderer_vulkan/maxwell_to_vk.cpp
+++ b/src/video_core/renderer_vulkan/maxwell_to_vk.cpp
@@ -333,6 +333,10 @@ VkShaderStageFlagBits ShaderStage(Shader::Stage stage) {
return VK_SHADER_STAGE_FRAGMENT_BIT;
case Shader::Stage::Compute:
return VK_SHADER_STAGE_COMPUTE_BIT;
+ case Shader::Stage::Task:
+ return VK_SHADER_STAGE_TASK_BIT_EXT;
+ case Shader::Stage::Mesh:
+ return VK_SHADER_STAGE_MESH_BIT_EXT;
}
UNIMPLEMENTED_MSG("Unimplemented shader stage={}", stage);
return {};
diff --git a/src/video_core/renderer_vulkan/present/layer.cpp b/src/video_core/renderer_vulkan/present/layer.cpp
index fa24c02f6d..5652090bd7 100644
--- a/src/video_core/renderer_vulkan/present/layer.cpp
+++ b/src/video_core/renderer_vulkan/present/layer.cpp
@@ -52,6 +52,8 @@ VkFormat GetFormat(const Tegra::FramebufferConfig& framebuffer) {
return VK_FORMAT_R5G6B5_UNORM_PACK16;
case Service::android::PixelFormat::Bgra8888:
return VK_FORMAT_B8G8R8A8_UNORM;
+ case Service::android::PixelFormat::Rgba1010102:
+ return VK_FORMAT_A2B10G10R10_UNORM_PACK32;
default:
UNIMPLEMENTED_MSG("Unknown framebuffer pixel format: {}",
static_cast<u32>(framebuffer.pixel_format));
diff --git a/src/video_core/renderer_vulkan/vertex_repack.h b/src/video_core/renderer_vulkan/vertex_repack.h
new file mode 100644
index 0000000000..493eb41bd1
--- /dev/null
+++ b/src/video_core/renderer_vulkan/vertex_repack.h
@@ -0,0 +1,114 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <algorithm>
+#include <cstdlib>
+#include <cstring>
+#include <limits>
+#include <optional>
+#include "video_core/engines/maxwell_3d.h"
+
+namespace Vulkan {
+
+constexpr bool VertexRepackInputSupported(bool dynamic_input, bool experimental_static) {
+ return dynamic_input || experimental_static;
+}
+
+// Pipelines without dynamic vertex input repack when the device cannot fetch the 69-byte stride.
+// EGLANTINE_EXPERIMENTAL_STATIC_VERTEX_REPACK=1 forces the repack on, =0 forces it off.
+inline bool StaticVertexRepackSelected(const char* override_value, u32 stride_alignment) {
+ if (override_value && std::strcmp(override_value, "1") == 0) {
+ return true;
+ }
+ if (override_value && std::strcmp(override_value, "0") == 0) {
+ return false;
+ }
+ return stride_alignment > 1 && 69 % stride_alignment != 0;
+}
+
+inline bool StaticVertexRepackEnabled(u32 stride_alignment) {
+ return StaticVertexRepackSelected(std::getenv("EGLANTINE_EXPERIMENTAL_STATIC_VERTEX_REPACK"),
+ stride_alignment);
+}
+
+struct VertexRepackParameters {
+ u32 vertices;
+ u32 output_size;
+ u32 source_base;
+ u32 source_bias;
+ u32 source_range;
+ u32 dispatch_groups;
+};
+
+inline std::optional<VertexRepackParameters> MakeVertexRepackParameters(u32 offset, u32 size,
+ u64 alignment,
+ u64 max_storage_range,
+ u32 max_dispatch_groups,
+ u64 buffer_size) {
+ if (size == 0 || alignment == 0) {
+ return std::nullopt;
+ }
+ const u64 vertices = (u64{size} + 68) / 69;
+ const u64 output_size = vertices * 72;
+ const u64 base = u64{offset} / alignment * alignment;
+ const u64 bias = offset - base;
+ // The word-rounded range must not run past the end of the source buffer
+ const u64 available = buffer_size > base ? buffer_size - base : 0;
+ const u64 range = (std::min)((bias + size + 3) / 4 * 4, available);
+ const u64 groups = (vertices * 18 + 127) / 128;
+ if (range < bias + size || output_size > std::numeric_limits<u32>::max() ||
+ range > std::numeric_limits<u32>::max() || output_size > max_storage_range ||
+ range > max_storage_range || groups > max_dispatch_groups) {
+ return std::nullopt;
+ }
+ return VertexRepackParameters{static_cast<u32>(vertices), static_cast<u32>(output_size),
+ static_cast<u32>(base), static_cast<u32>(bias),
+ static_cast<u32>(range), static_cast<u32>(groups)};
+}
+
+inline bool VertexRepackEnabled() {
+ static const bool enabled = [] {
+ const char* value = std::getenv("EGLANTINE_TEST_VERTEX_REPACK");
+ return !value || std::strcmp(value, "0") != 0;
+ }();
+ return enabled;
+}
+
+inline u32 VertexRepackMask(const Tegra::Engines::Maxwell3D::Regs& regs) {
+ using Attribute = Tegra::Engines::Maxwell3D::Regs::VertexAttribute;
+ if (!VertexRepackEnabled()) {
+ return 0;
+ }
+ u32 candidates = 0;
+ u32 excluded = 0;
+ for (const auto& attribute : regs.vertex_attrib_format) {
+ const u32 binding = attribute.buffer;
+ if (attribute.constant || regs.vertex_streams[binding].stride != 69) {
+ continue;
+ }
+ if (attribute.size == Attribute::Size::Invalid) {
+ excluded |= 1U << binding;
+ continue;
+ }
+ const u32 offset = attribute.offset;
+ const u32 size = attribute.SizeInBytes();
+ if (offset == 65 && size == 4) {
+ candidates |= 1U << binding;
+ }
+ if (offset + size > 69 || (offset < 65 && offset + size > 65)) {
+ excluded |= 1U << binding;
+ }
+ }
+ return candidates & ~excluded;
+}
+
+inline u32 RepackedVertexOffset(u32 mask, u32 binding, u32 offset) {
+ return offset + ((mask & (1U << binding)) && offset >= 65 ? 3U : 0U);
+}
+
+inline u32 RepackedVertexStride(u32 mask, u32 binding, u32 stride) {
+ return mask & (1U << binding) ? 72U : stride;
+}
+
+} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
index 493de2cc20..43cb7d9414 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.cpp
@@ -7,11 +7,13 @@
#include <algorithm>
#include <array>
#include <cstring>
+#include <cstdlib>
#include <span>
#include <vector>
#include "video_core/buffer_cache/buffer_cache_base.h"
#include "video_core/renderer_vulkan/vk_buffer_cache.h"
+#include "video_core/renderer_vulkan/vertex_repack.h"
#include "video_core/renderer_vulkan/maxwell_to_vk.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
@@ -359,6 +361,18 @@ BufferCacheRuntime::BufferCacheRuntime(const Device& device_, MemoryAllocator& m
compute_pass_descriptor_queue),
multi_range_buffers(device_) {
const VkDriverIdKHR driver_id = device.GetDriverID();
+ const bool enable_vertex_repack = VertexRepackEnabled();
+ LOG_INFO(Render_Vulkan, "69-byte vertex layout repacking {} ({})",
+ enable_vertex_repack ? "enabled" : "disabled",
+ "portable Vulkan layout fallback");
+ static_vertex_repack =
+ StaticVertexRepackEnabled(device.GetMinVertexInputBindingStrideAlignment());
+ LOG_INFO(Render_Vulkan, "Vertex repacking without dynamic vertex input {}",
+ static_vertex_repack ? "enabled" : "disabled");
+ if (enable_vertex_repack) {
+ vertex_repack_pass = std::make_unique<VertexRepackPass>(
+ device, scheduler, descriptor_pool, staging_pool, compute_pass_descriptor_queue);
+ }
limit_dynamic_storage_buffers = driver_id == VK_DRIVER_ID_QUALCOMM_PROPRIETARY ||
driver_id == VK_DRIVER_ID_ARM_PROPRIETARY;
if (limit_dynamic_storage_buffers) {
@@ -374,6 +388,18 @@ BufferCacheRuntime::BufferCacheRuntime(const Device& device_, MemoryAllocator& m
scheduler_, staging_pool_);
}
+u32 BufferCacheRuntime::SetVertexRepackLayout(const Tegra::Engines::Maxwell3D::Regs& regs,
+ bool dynamic_input) {
+ const u32 old_mask = vertex_repack_mask;
+ const bool input_supported = VertexRepackInputSupported(dynamic_input, static_vertex_repack);
+ vertex_repack_mask = vertex_repack_pass && input_supported ? VertexRepackMask(regs) : 0;
+ if (vertex_repack_mask != old_mask) {
+ LOG_DEBUG(Render_Vulkan, "Vertex repack selection: mask={:#x}, dynamic_input={}",
+ vertex_repack_mask, dynamic_input);
+ }
+ return old_mask ^ vertex_repack_mask;
+}
+
StagingBufferRef BufferCacheRuntime::UploadStagingBuffer(size_t size) {
return staging_pool.Request(size, MemoryUsage::Upload);
}
@@ -430,6 +456,10 @@ void BufferCacheRuntime::Wait(u64 tick) {
scheduler.Wait(tick);
}
+void BufferCacheRuntime::WaitSubmittedTick(u64 tick) {
+ scheduler.GetMasterSemaphore().Wait(tick);
+}
+
void BufferCacheRuntime::Finish() {
scheduler.Finish();
}
@@ -629,6 +659,17 @@ void BufferCacheRuntime::BindVertexBuffer(u32 index, VkBuffer buffer, u32 offset
if (index >= device.GetMaxVertexInputBindings()) {
return;
}
+ if (buffer != VK_NULL_HANDLE && size != 0 && IsVertexRepacked(index)) {
+ const auto repacked =
+ vertex_repack_pass->Assemble(buffer, offset, size, u64{offset} + size);
+ if (!repacked) {
+ return;
+ }
+ buffer = repacked->first;
+ offset = static_cast<u32>(repacked->second);
+ size = static_cast<u32>((u64{size} + 68) / 69 * 72);
+ stride = 72;
+ }
if (device.IsExtExtendedDynamicStateSupported()) {
scheduler.Record([index, buffer, offset, size, stride](vk::CommandBuffer cmdbuf) {
const VkDeviceSize vk_offset = buffer != VK_NULL_HANDLE ? offset : 0;
@@ -661,6 +702,25 @@ void BufferCacheRuntime::BindVertexBuffers(VideoCommon::HostBindings<Buffer>& bi
}
}
buffer_handles[i] = handle;
+ const u32 binding = bindings.min_index + i;
+ if (handle != VK_NULL_HANDLE && IsVertexRepacked(binding) && bindings.strides[i] == 69 &&
+ bindings.sizes[i] > 0) {
+ const auto repacked = vertex_repack_pass->Assemble(
+ handle, static_cast<u32>(bindings.offsets[i]), static_cast<u32>(bindings.sizes[i]),
+ bindings.buffers[i]->SizeBytes());
+ if (!repacked) {
+ continue;
+ }
+ buffer_handles[i] = repacked->first;
+ bindings.offsets[i] = repacked->second;
+ bindings.sizes[i] = (bindings.sizes[i] + 68) / 69 * 72;
+ bindings.strides[i] = 72;
+ static bool reported = false;
+ if (!reported) {
+ LOG_INFO(Render_Vulkan, "GPU vertex repacking active: 69 -> 72 bytes");
+ reported = true;
+ }
+ }
}
const u32 device_max = device.GetMaxVertexInputBindings();
const u32 min_binding = (std::min)(bindings.min_index, device_max);
diff --git a/src/video_core/renderer_vulkan/vk_buffer_cache.h b/src/video_core/renderer_vulkan/vk_buffer_cache.h
index fff1f8164d..b9a0225094 100644
--- a/src/video_core/renderer_vulkan/vk_buffer_cache.h
+++ b/src/video_core/renderer_vulkan/vk_buffer_cache.h
@@ -110,12 +110,22 @@ public:
void TickFrame(Common::SlotVector<Buffer>& slot_buffers) noexcept;
+ /// Returns the mask of bindings whose repacked state changed.
+ u32 SetVertexRepackLayout(const Tegra::Engines::Maxwell3D::Regs& regs, bool dynamic_input);
+
+ bool IsVertexRepacked(u32 binding) const {
+ return (vertex_repack_mask & (1U << binding)) != 0;
+ }
+
u64 CurrentTick();
bool IsFree(u64 tick);
void Wait(u64 tick);
+ /// Waits for a submitted tick without flushing: safe on threads other than the GPU thread.
+ void WaitSubmittedTick(u64 tick);
+
void Finish();
u64 GetDeviceLocalMemory() const;
@@ -159,15 +169,40 @@ public:
void BindTransformFeedbackBuffers(VideoCommon::HostBindings<Buffer>& bindings);
+ /// This and the two members below are the hooks of VideoCommon::StreamUniformReusable
+ /// (buffer_cache/uniform_stream_reuse.h).
+ struct StreamUniformRef {
+ VkBuffer buffer;
+ VkDeviceAddress device_address; // base of `buffer`, 0 without buffer device address
+ u32 offset;
+ };
+
std::span<u8> BindMappedUniformBuffer([[maybe_unused]] size_t stage,
[[maybe_unused]] u32 binding_index,
u32 size) {
const StagingBufferRef ref = staging_pool.Request(size, MemoryUsage::Upload);
- guest_descriptor_queue.AddBuffer(ref.buffer, ref.device_address,
- static_cast<u32>(ref.offset), size);
+ last_stream_uniform = StreamUniformRef{
+ .buffer = ref.buffer,
+ .device_address = ref.device_address,
+ .offset = static_cast<u32>(ref.offset),
+ };
+ RebindStreamUniform(last_stream_uniform, size);
return ref.mapped_span;
}
+ /// The allocation of the latest BindMappedUniformBuffer call.
+ [[nodiscard]] StreamUniformRef LastStreamUniformRef() const noexcept {
+ return last_stream_uniform;
+ }
+
+ /// Adds `ref` to the descriptor payload exactly as BindMappedUniformBuffer did when it made
+ /// the allocation (it is the very same call). The caller guarantees the allocation is still
+ /// live: made on the scheduler tick that is current now, so the staging pool has not recycled
+ /// it and the GPU has not even started on it.
+ void RebindStreamUniform(const StreamUniformRef& ref, u32 size) {
+ guest_descriptor_queue.AddBuffer(ref.buffer, ref.device_address, ref.offset, size);
+ }
+
[[nodiscard]] VkDeviceSize SparseAlignmentFor(bool sparse_compatible) const noexcept {
if (!sparse_compatible || !multi_range_buffers.use_sparse) {
return 0;
@@ -259,12 +294,17 @@ private:
vk::Buffer null_buffer;
std::unique_ptr<Uint8Pass> uint8_pass;
+ std::unique_ptr<VertexRepackPass> vertex_repack_pass;
+ u32 vertex_repack_mask{};
+ bool static_vertex_repack{};
QuadIndexedPass quad_index_pass;
MultiRangeBufferCache multi_range_buffers;
boost::container::small_vector<MultiRangeSource, 16> multi_range_sources;
VkDeviceSize multi_range_total{};
+ StreamUniformRef last_stream_uniform{};
+
bool limit_dynamic_storage_buffers = false;
u32 max_dynamic_storage_buffers = (std::numeric_limits<u32>::max)();
};
diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.cpp b/src/video_core/renderer_vulkan/vk_compute_pass.cpp
index 8cd8abb599..5902b7d8f5 100644
--- a/src/video_core/renderer_vulkan/vk_compute_pass.cpp
+++ b/src/video_core/renderer_vulkan/vk_compute_pass.cpp
@@ -26,8 +26,10 @@
#include "video_core/host_shaders/resolve_conditional_render_comp_spv.h"
#include "video_core/host_shaders/vulkan_quad_indexed_comp_spv.h"
#include "video_core/host_shaders/vulkan_uint8_comp_spv.h"
+#include "video_core/host_shaders/vulkan_vertex_repack_comp_spv.h"
#include "video_core/host_shaders/block_linear_unswizzle_3d_bcn_comp_spv.h"
#include "video_core/renderer_vulkan/vk_compute_pass.h"
+#include "video_core/renderer_vulkan/vertex_repack.h"
#include "video_core/surface.h"
#include "video_core/renderer_vulkan/vk_descriptor_pool.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
@@ -324,6 +326,63 @@ ComputePass::ComputePass(const Device& device_, Scheduler& scheduler, Descriptor
ComputePass::~ComputePass() = default;
+VertexRepackPass::VertexRepackPass(const Device& device_, Scheduler& scheduler_,
+ DescriptorPool& descriptor_pool, StagingBufferPool& staging_pool_,
+ ComputePassDescriptorQueue& descriptor_queue_)
+ : ComputePass(device_, scheduler_, descriptor_pool, INPUT_OUTPUT_DESCRIPTOR_SET_BINDINGS,
+ INPUT_OUTPUT_DESCRIPTOR_UPDATE_TEMPLATE, INPUT_OUTPUT_BANK_INFO,
+ COMPUTE_PUSH_CONSTANT_RANGE<12>, VULKAN_VERTEX_REPACK_COMP_SPV),
+ scheduler{scheduler_}, staging_pool{staging_pool_}, descriptor_queue{descriptor_queue_} {}
+
+std::optional<std::pair<VkBuffer, VkDeviceSize>> VertexRepackPass::Assemble(VkBuffer source,
+ u32 offset, u32 size,
+ u64 buffer_size) {
+ const auto parameters = MakeVertexRepackParameters(
+ offset, size, device.GetStorageBufferAlignment(), device.GetMaxStorageBufferRange(),
+ device.GetMaxComputeWorkGroupCount()[0], buffer_size);
+ if (!parameters) {
+ LOG_ERROR(Render_Vulkan, "Vertex repack exceeds device limits: offset={}, size={}", offset, size);
+ return std::nullopt;
+ }
+ const u32 num_vertices = parameters->vertices;
+ const u32 output_size = parameters->output_size;
+ const auto dest = staging_pool.Request(output_size, MemoryUsage::DeviceLocal);
+ const u32 base_offset = parameters->source_base;
+ const u32 bias = parameters->source_bias;
+ descriptor_queue.Acquire(scheduler, 2);
+ descriptor_queue.AddBuffer(source, base_offset, parameters->source_range);
+ descriptor_queue.AddBuffer(dest.buffer, dest.offset, output_size);
+ const void* data = descriptor_queue.UpdateData();
+ scheduler.RequestOutsideRenderPassOperationContext();
+ LOG_DEBUG(Render_Vulkan, "Vertex repack queued: source_size={}, output_size={}, groups={}",
+ size, output_size, parameters->dispatch_groups);
+ scheduler.Record([this, data, bias, size, num_vertices,
+ groups = parameters->dispatch_groups](vk::CommandBuffer cmdbuf) {
+ const VkMemoryBarrier read_barrier{
+ .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
+ .srcAccessMask = VK_ACCESS_MEMORY_WRITE_BIT,
+ .dstAccessMask = VK_ACCESS_SHADER_READ_BIT,
+ };
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT,
+ VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, read_barrier);
+ const auto set = descriptor_allocator.Commit();
+ device.GetLogical().UpdateDescriptorSet(set, *descriptor_template, data);
+ cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline);
+ cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *layout, 0, set, {});
+ const std::array<u32, 3> constants{bias, size, num_vertices * 18};
+ cmdbuf.PushConstants(*layout, VK_SHADER_STAGE_COMPUTE_BIT, 0, sizeof(constants), constants.data());
+ cmdbuf.Dispatch(groups, 1, 1);
+ const VkMemoryBarrier write_barrier{
+ .sType = VK_STRUCTURE_TYPE_MEMORY_BARRIER,
+ .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT,
+ .dstAccessMask = VK_ACCESS_VERTEX_ATTRIBUTE_READ_BIT,
+ };
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
+ VK_PIPELINE_STAGE_VERTEX_INPUT_BIT, 0, write_barrier);
+ });
+ return std::pair<VkBuffer, VkDeviceSize>{dest.buffer, dest.offset};
+}
+
Uint8Pass::Uint8Pass(const Device& device_, Scheduler& scheduler_, DescriptorPool& descriptor_pool,
StagingBufferPool& staging_buffer_pool_,
ComputePassDescriptorQueue& compute_pass_descriptor_queue_)
@@ -743,11 +802,11 @@ void ASTCDecoderPass::AssembleInto(const VideoCommon::ImageInfo& info, u32 skip,
cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT,
vk::PIPELINE_STAGE_GRAPHICS_COMPUTE, 0, image_barrier);
});
- // Each decode ends with the GPU thread waiting for the host GPU to go idle. Measured here
- // (count and time per 5 s); EDEN_NX2_ASTC_FLUSH=1 submits without waiting instead.
+ // Each decode ends with a submit to the host GPU. Measured here
+ // (count and time per 5 s); EDEN_NX2_ASTC_FLUSH=0 waits for the host GPU to go idle instead.
static const bool flush_only = [] {
const char* value = std::getenv("EDEN_NX2_ASTC_FLUSH");
- return value != nullptr && value[0] == '1';
+ return value == nullptr || value[0] != '0';
}();
if (!target.wait) {
return;
diff --git a/src/video_core/renderer_vulkan/vk_compute_pass.h b/src/video_core/renderer_vulkan/vk_compute_pass.h
index 9f2e8e9f1c..9687bfff91 100644
--- a/src/video_core/renderer_vulkan/vk_compute_pass.h
+++ b/src/video_core/renderer_vulkan/vk_compute_pass.h
@@ -56,6 +56,21 @@ private:
vk::ShaderModule module;
};
+class VertexRepackPass final : public ComputePass {
+public:
+ VertexRepackPass(const Device& device, Scheduler& scheduler, DescriptorPool& descriptor_pool,
+ StagingBufferPool& staging_pool, ComputePassDescriptorQueue& descriptor_queue);
+ /// Returns nothing when the range exceeds the device limits or the source buffer; the binding
+ /// is then left as is.
+ std::optional<std::pair<VkBuffer, VkDeviceSize>> Assemble(VkBuffer source, u32 offset, u32 size,
+ u64 buffer_size);
+
+private:
+ Scheduler& scheduler;
+ StagingBufferPool& staging_pool;
+ ComputePassDescriptorQueue& descriptor_queue;
+};
+
class Uint8Pass final : public ComputePass {
public:
explicit Uint8Pass(const Device& device_, Scheduler& scheduler_,
diff --git a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp
index 29356cfcec..3a3861930c 100644
--- a/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp
+++ b/src/video_core/renderer_vulkan/vk_compute_pipeline.cpp
@@ -18,6 +18,7 @@
#include "video_core/renderer_vulkan/vk_pipeline_cache.h"
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include "video_core/renderer_vulkan/vk_update_descriptor.h"
+#include "video_core/shader_handle_program.h"
#include "video_core/shader_notify.h"
#include "video_core/vulkan_common/vulkan_device.h"
#include "video_core/vulkan_common/vulkan_wrapper.h"
@@ -134,6 +135,9 @@ ComputePipeline::ComputePipeline(const Device& device_, Scheduler& scheduler, vk
if (pipeline_statistics) {
pipeline_statistics->Collect(device, *pipeline);
}
+ // Nothing reads the module once the pipeline exists; the driver keeps its SPIR-V while it
+ // lives.
+ spv_module.reset();
std::scoped_lock lock{build_mutex};
is_built = true;
build_condvar.notify_one();
@@ -172,6 +176,19 @@ bool ComputePipeline::Configure(Tegra::Engines::KeplerCompute& kepler_compute,
const auto& cbufs{qmd.const_buffer_config};
const bool via_header_index{qmd.linked_tsc != 0};
const auto read_handle{[&](const auto& desc, u32 index) {
+ if (desc.handle_program != ~0U) {
+ // Read by the shader from a storage buffer: computed for this dispatch.
+ return TexturePair(VideoCommon::EvaluateTextureHandle(
+ info, desc.handle_program, gpu_memory,
+ [&](u32 cbuf) -> GPUVAddr {
+ return cbuf < cbufs.size() &&
+ ((qmd.const_buffer_enable_mask >> cbuf) &
+ 1) != 0
+ ? cbufs[cbuf].Address()
+ : GPUVAddr{};
+ }),
+ via_header_index);
+ }
ASSERT(((qmd.const_buffer_enable_mask >> desc.cbuf_index) & 1) != 0);
const u32 index_offset{index << desc.size_shift};
const u32 offset{desc.cbuf_offset + index_offset};
diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp
index 94bace432c..4bd2e2735e 100644
--- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp
+++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.cpp
@@ -24,6 +24,7 @@
#include "video_core/renderer_vulkan/vk_scheduler.h"
#include "video_core/renderer_vulkan/vk_texture_cache.h"
#include "video_core/renderer_vulkan/vk_update_descriptor.h"
+#include "video_core/shader_handle_program.h"
#include "video_core/shader_notify.h"
#include "video_core/texture_cache/texture_cache.h"
#include "video_core/vulkan_common/vulkan_device.h"
@@ -54,6 +55,17 @@ using VideoCore::Surface::PixelFormatFromRenderTargetFormat;
constexpr size_t NUM_STAGES = Maxwell::MaxShaderStage;
+// The stage of a program: task and mesh programs sit in the vertex and tessellation slots.
+VkShaderStageFlagBits StageFlag(const Shader::Info& info, VkShaderStageFlagBits slot_stage) {
+ if (info.is_task) {
+ return VK_SHADER_STAGE_TASK_BIT_EXT;
+ }
+ if (info.is_mesh) {
+ return VK_SHADER_STAGE_MESH_BIT_EXT;
+ }
+ return slot_stage;
+}
+
DescriptorLayoutBuilder MakeBuilder(const Device& device, std::span<const Shader::Info> infos) {
DescriptorLayoutBuilder builder{device};
for (size_t index = 0; index < infos.size(); ++index) {
@@ -64,7 +76,7 @@ DescriptorLayoutBuilder MakeBuilder(const Device& device, std::span<const Shader
VK_SHADER_STAGE_GEOMETRY_BIT,
VK_SHADER_STAGE_FRAGMENT_BIT,
};
- builder.Add(infos[index], stages.at(index));
+ builder.Add(infos[index], StageFlag(infos[index], stages.at(index)));
}
return builder;
}
@@ -330,6 +342,11 @@ GraphicsPipeline::GraphicsPipeline(
if (pipeline_statistics) {
pipeline_statistics->Collect(device, *pipeline);
}
+ // Nothing reads the shader modules once the pipeline exists, and the driver keeps a copy
+ // of each one's SPIR-V while it lives (title 1: ~1,500 pipelines, 2-5 modules each).
+ for (vk::ShaderModule& module : spv_modules) {
+ module.reset();
+ }
std::scoped_lock lock{build_mutex};
is_built = true;
@@ -338,12 +355,13 @@ GraphicsPipeline::GraphicsPipeline(
shader_notify->MarkShaderComplete();
}
}};
+ // Before the build: it frees the modules this looks at.
+ configure_func = ConfigureFunc(spv_modules, stage_infos);
if (worker_thread) {
worker_thread->QueueWork(std::move(func));
} else {
func();
}
- configure_func = ConfigureFunc(spv_modules, stage_infos);
}
void GraphicsPipeline::AddTransition(GraphicsPipeline* transition) {
@@ -382,6 +400,17 @@ bool GraphicsPipeline::ConfigureImpl(bool is_indexed) {
}
const auto& cbufs{maxwell3d->state.shader_stages[stage].const_buffers};
const auto read_handle{[&](const auto& desc, u32 index) {
+ if (desc.handle_program != ~0U) {
+ // Read by the shader from a storage buffer: computed for this draw.
+ return TexturePair(VideoCommon::EvaluateTextureHandle(
+ info, desc.handle_program, *gpu_memory,
+ [&](u32 cbuf) -> GPUVAddr {
+ return cbuf < cbufs.size() && cbufs[cbuf].enabled
+ ? cbufs[cbuf].address
+ : GPUVAddr{};
+ }),
+ via_header_index);
+ }
ASSERT(cbufs[desc.cbuf_index].enabled);
const u32 index_offset{index << desc.size_shift};
const u32 offset{desc.cbuf_offset + index_offset};
@@ -1073,6 +1102,19 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) {
}
}
+ const bool is_mesh{IsMeshPipeline()};
+ if (is_mesh) {
+ // Mesh pipelines have no vertex input or input assembly.
+ dynamic_states.erase(std::remove_if(dynamic_states.begin(), dynamic_states.end(),
+ [](VkDynamicState state) {
+ return state == VK_DYNAMIC_STATE_PRIMITIVE_TOPOLOGY_EXT ||
+ state == VK_DYNAMIC_STATE_PRIMITIVE_RESTART_ENABLE_EXT ||
+ state == VK_DYNAMIC_STATE_VERTEX_INPUT_EXT ||
+ state == VK_DYNAMIC_STATE_VERTEX_INPUT_BINDING_STRIDE_EXT ||
+ state == VK_DYNAMIC_STATE_PATCH_CONTROL_POINTS_EXT;
+ }),
+ dynamic_states.end());
+ }
const VkPipelineDynamicStateCreateInfo dynamic_state_ci{
.sType = VK_STRUCTURE_TYPE_PIPELINE_DYNAMIC_STATE_CREATE_INFO,
.pNext = nullptr,
@@ -1095,7 +1137,8 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) {
.sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO,
.pNext = nullptr,
.flags = 0,
- .stage = MaxwellToVK::ShaderStage(Shader::StageFromIndex(stage)),
+ .stage = StageFlag(stage_infos[stage],
+ MaxwellToVK::ShaderStage(Shader::StageFromIndex(stage))),
.module = *spv_modules[stage],
.pName = "main",
.pSpecializationInfo = nullptr,
@@ -1115,9 +1158,9 @@ void GraphicsPipeline::MakePipeline(VkRenderPass render_pass) {
.flags = flags,
.stageCount = static_cast<u32>(shader_stages.size()),
.pStages = shader_stages.data(),
- .pVertexInputState = &vertex_input_ci,
- .pInputAssemblyState = &input_assembly_ci,
- .pTessellationState = &tessellation_ci,
+ .pVertexInputState = is_mesh ? nullptr : &vertex_input_ci,
+ .pInputAssemblyState = is_mesh ? nullptr : &input_assembly_ci,
+ .pTessellationState = is_mesh ? nullptr : &tessellation_ci,
.pViewportState = &viewport_ci,
.pRasterizationState = &rasterization_ci,
.pMultisampleState = &multisample_ci,
diff --git a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h
index b1ac5a3fcc..6d6e2ab12d 100644
--- a/src/video_core/renderer_vulkan/vk_graphics_pipeline.h
+++ b/src/video_core/renderer_vulkan/vk_graphics_pipeline.h
@@ -98,6 +98,11 @@ public:
bool UsesExtendedDynamicState() const noexcept {
return key.state.extended_dynamic_state != 0;
}
+
+ /// Switch 2 task/mesh pipeline: drawn with vkCmdDrawMeshTasksEXT.
+ [[nodiscard]] bool IsMeshPipeline() const noexcept {
+ return stage_infos[0].is_task || stage_infos[0].is_mesh;
+ }
GraphicsPipeline& operator=(GraphicsPipeline&&) noexcept = delete;
GraphicsPipeline(GraphicsPipeline&&) noexcept = delete;
@@ -119,6 +124,21 @@ public:
: nullptr;
}
+ /// The frame the pipeline cache last handed this pipeline out (evicts long unused ones).
+ void MarkUsed(u64 frame) noexcept {
+ last_use_frame = frame;
+ }
+
+ [[nodiscard]] u64 LastUse() const noexcept {
+ return last_use_frame;
+ }
+
+ /// Forgets the transitions, which may point to pipelines the cache evicted.
+ void ClearTransitions() noexcept {
+ transition_keys.clear();
+ transitions.clear();
+ }
+
[[nodiscard]] bool IsBuilt() const noexcept {
return is_built.load(std::memory_order::relaxed);
}
@@ -159,6 +179,7 @@ private:
std::vector<GraphicsPipelineCacheKey> transition_keys;
std::vector<GraphicsPipeline*> transitions;
+ u64 last_use_frame{};
std::array<vk::ShaderModule, NUM_STAGES> spv_modules;
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
index 2c64f3375b..b651bfc9ba 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.cpp
@@ -34,6 +34,7 @@
#include "video_core/renderer_vulkan/maxwell_to_vk.h"
#include "video_core/renderer_vulkan/pipeline_helper.h"
#include "video_core/renderer_vulkan/pipeline_statistics.h"
+#include "video_core/renderer_vulkan/vertex_repack.h"
#include "video_core/renderer_vulkan/vk_compute_pipeline.h"
#include "video_core/renderer_vulkan/vk_descriptor_pool.h"
#include "video_core/renderer_vulkan/vk_pipeline_cache.h"
@@ -65,7 +66,7 @@ using VideoCommon::FileEnvironment;
using VideoCommon::GenericEnvironment;
using VideoCommon::GraphicsEnvironment;
-constexpr u32 CACHE_VERSION = 18;
+constexpr u32 CACHE_VERSION = 19;
constexpr size_t VULKAN_CACHE_FLUSH_PIPELINES = 128;
constexpr size_t VULKAN_CACHE_FLUSH_MIN_SECONDS = 30;
constexpr std::array<char, 8> VULKAN_CACHE_MAGIC_NUMBER{'y', 'u', 'z', 'u', 'v', 'k', 'c', 'h'};
@@ -170,6 +171,14 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program
} else {
info.previous_stage_stores.mask.set();
}
+ if (previous_program && previous_program->stage == Shader::Stage::Mesh) {
+ for (const u32 line : previous_program->mesh.primitive_lines) {
+ const auto attribute{static_cast<Shader::IR::Attribute>(line)};
+ if (Shader::IR::IsGeneric(attribute)) {
+ info.per_primitive_generics.set(Shader::IR::GenericAttributeIndex(attribute));
+ }
+ }
+ }
const Shader::Stage stage{program.stage};
const bool has_geometry{key.unique_hashes[4] != 0 && !programs[4].is_geometry_passthrough};
const bool gl_ndc{key.state.ndc_minus_one_to_one != 0};
@@ -230,6 +239,9 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program
return Shader::TessSpacing::Equal;
}();
break;
+ case Shader::Stage::Mesh:
+ info.convert_depth_mode = gl_ndc;
+ break;
case Shader::Stage::Geometry:
if (program.output_topology == Shader::OutputTopology::PointList) {
info.fixed_state_point_size = point_size;
@@ -256,6 +268,10 @@ Shader::RuntimeInfo MakeRuntimeInfo(std::span<const Shader::IR::Program> program
if (device.IsMoltenVK()) {
for (size_t i = 0; i < 8; ++i) {
const auto format = static_cast<Tegra::RenderTargetFormat>(key.state.color_formats[i]);
+ if (format == Tegra::RenderTargetFormat::NONE) {
+ info.color_output_types[i] = Shader::AttributeType::Float;
+ continue;
+ }
const auto pixel_format = VideoCore::Surface::PixelFormatFromRenderTargetFormat(format);
if (VideoCore::Surface::IsPixelFormatInteger(pixel_format)) {
if (VideoCore::Surface::IsPixelFormatSignedInteger(pixel_format)) {
@@ -341,6 +357,22 @@ bool GraphicsPipelineCacheKey::operator==(const GraphicsPipelineCacheKey& rhs) c
return std::memcmp(&rhs, this, Size()) == 0;
}
+namespace {
+// NVIDIA's and AMD's drivers keep a disk cache of built pipelines of their own. Eden's copy on top
+// only kept every pipeline the title ever built in host memory (title 1: a 245 MB file, ~0.35 GB
+// of RAM for the session) and copied it whole at each save, which stalled frames; pipelines built
+// no slower without it.
+bool DriverCachesPipelines(const Device& device) {
+ switch (device.GetDriverID()) {
+ case VK_DRIVER_ID_NVIDIA_PROPRIETARY:
+ case VK_DRIVER_ID_AMD_PROPRIETARY:
+ return true;
+ default:
+ return false;
+ }
+}
+} // Anonymous namespace
+
PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
const Device& device_, Scheduler& scheduler_,
DescriptorPool& descriptor_pool_,
@@ -354,7 +386,8 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
render_pass_cache{render_pass_cache_}, buffer_cache{buffer_cache_},
texture_cache{texture_cache_}, shader_notify{shader_notify_},
use_asynchronous_shaders{Settings::values.use_asynchronous_shaders.GetValue()},
- use_vulkan_pipeline_cache{Settings::values.use_vulkan_driver_pipeline_cache.GetValue()},
+ use_vulkan_pipeline_cache{Settings::values.use_vulkan_driver_pipeline_cache.GetValue() &&
+ !DriverCachesPipelines(device_)},
workers(device.HasBrokenParallelShaderCompiling() ? 1ULL : GetTotalPipelineWorkers(),
"VkPipelineBuilder", {}, Common::ThreadPlacement::Background),
serialization_thread(1, "VkPipelineSerialization", {},
@@ -374,7 +407,9 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
Shader::Stage::TessellationEval) |
subgroup_stage_bit(VK_SHADER_STAGE_GEOMETRY_BIT, Shader::Stage::Geometry) |
subgroup_stage_bit(VK_SHADER_STAGE_FRAGMENT_BIT, Shader::Stage::Fragment) |
- subgroup_stage_bit(VK_SHADER_STAGE_COMPUTE_BIT, Shader::Stage::Compute)};
+ subgroup_stage_bit(VK_SHADER_STAGE_COMPUTE_BIT, Shader::Stage::Compute) |
+ subgroup_stage_bit(VK_SHADER_STAGE_TASK_BIT_EXT, Shader::Stage::Task) |
+ subgroup_stage_bit(VK_SHADER_STAGE_MESH_BIT_EXT, Shader::Stage::Mesh)};
if (driver_id == VK_DRIVER_ID_MESA_TURNIP) {
supported_subgroup_stages &= ~(1u << static_cast<u32>(Shader::Stage::Geometry));
}
@@ -549,6 +584,8 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
dynamic_features.has_dynamic_vertex_input =
device.IsExtVertexInputDynamicStateSupported() &&
Settings::values.vertex_input_dynamic_state.GetValue();
+ dynamic_features.has_static_vertex_repack =
+ StaticVertexRepackEnabled(device.GetMinVertexInputBindingStrideAlignment());
dynamic_features.has_provoking_vertex = device.IsExtProvokingVertexSupported();
dynamic_features.has_provoking_vertex_first_mode =
@@ -559,13 +596,104 @@ PipelineCache::PipelineCache(Tegra::MaxwellDeviceMemoryManager& device_memory_,
device.SupportsTransformFeedbackProvokingVertexPreservation();
}
+namespace {
+// <title>/vulkan_used.bin: the graphics pipelines recent sessions used, as key hash + sessions
+// since the last use.
+constexpr u32 UsedPipelinesMagic = 0x31505545; // "EUP1"
+// Pipelines the last two sessions used are built at boot. Each holds ~0.45 MB of driver memory:
+// title 1 built 4,800 from eight sessions, 1.5 GB more than building them on first use (but that
+// stalled frames 4x as often), while one session of its title screen uses ~1,500.
+// MoltenVK keeps the previous eight sessions: a pipeline built while playing goes through
+// SPIR-V -> MSL -> Metal, far more than the 2-3 ms the shorter window assumes, so each one
+// built at boot is a stutter avoided.
+constexpr u8 UsedPipelinesMaxAge = 2;
+constexpr u8 UsedPipelinesMaxAgeMoltenVK = 8;
+
+u8 MaxUsedPipelineAge(const Device& device) {
+ return device.IsMoltenVK() ? UsedPipelinesMaxAgeMoltenVK : UsedPipelinesMaxAge;
+}
+
+std::unordered_map<u64, u8> LoadUsedPipelines(const std::filesystem::path& path, u8 max_age) {
+ std::unordered_map<u64, u8> used;
+ std::ifstream file{path, std::ios::binary};
+ u32 magic{}, count{};
+ if (!file.read(reinterpret_cast<char*>(&magic), sizeof(magic)) || magic != UsedPipelinesMagic ||
+ !file.read(reinterpret_cast<char*>(&count), sizeof(count))) {
+ return used;
+ }
+ used.reserve(count);
+ for (u32 i = 0; i < count; ++i) {
+ u64 hash{};
+ u8 age{};
+ if (!file.read(reinterpret_cast<char*>(&hash), sizeof(hash)) ||
+ !file.read(reinterpret_cast<char*>(&age), sizeof(age))) {
+ break;
+ }
+ if (age < max_age) { // lists saved with a longer window
+ used.emplace(hash, age);
+ }
+ }
+ return used;
+}
+
+// Without a list of the pipelines recent sessions used (the first boot with this cache, or a session
+// that ended before saving it), this many cached graphics pipelines at most are built at boot and the
+// rest when first needed. A cache that grew over many builds held 48k pipelines, ~21 GB of driver
+// memory built before the game started; the list is only saved at exit or after two minutes, so a
+// boot that ran out of memory never saved one and the next boot did the same.
+constexpr size_t MaxPipelinesWithoutUsedList = 3000;
+
+// EDEN_NX2_PRELOAD_ALL_PIPELINES=1 builds every cached pipeline at boot, as before.
+bool PreloadAllPipelines() {
+ static const bool all = [] {
+ const char* value = std::getenv("EDEN_NX2_PRELOAD_ALL_PIPELINES");
+ return value != nullptr && value[0] == '1';
+ }();
+ return all;
+}
+} // Anonymous namespace
+
PipelineCache::~PipelineCache() {
+ SaveUsedPipelines();
if (use_vulkan_pipeline_cache && !vulkan_pipeline_cache_filename.empty()) {
SerializeVulkanPipelineCache(vulkan_pipeline_cache_filename, vulkan_pipeline_cache,
CACHE_VERSION);
}
}
+void PipelineCache::SaveUsedPipelines() {
+ used_pipelines_saved = std::chrono::steady_clock::now();
+ // A session too short to say what the game uses (a boot to the menu) keeps the old list.
+ constexpr size_t MinimumUsed = 64;
+ if (used_pipelines_filename.empty() ||
+ used_this_session.size() + used_and_evicted.size() < MinimumUsed) {
+ return;
+ }
+ std::unordered_map<u64, u8> next;
+ next.reserve(used_pipelines.size() + used_this_session.size());
+ for (const auto& [key, pipeline] : graphics_cache) {
+ if (used_this_session.contains(pipeline.get())) {
+ next.emplace(key.Hash(), u8{0});
+ }
+ }
+ for (const u64 hash : used_and_evicted) {
+ next.emplace(hash, u8{0});
+ }
+ for (const auto& [hash, age] : used_pipelines) {
+ if (age + 1 < MaxUsedPipelineAge(device)) {
+ next.try_emplace(hash, static_cast<u8>(age + 1));
+ }
+ }
+ std::ofstream file{used_pipelines_filename, std::ios::binary | std::ios::trunc};
+ const u32 count = static_cast<u32>(next.size());
+ file.write(reinterpret_cast<const char*>(&UsedPipelinesMagic), sizeof(UsedPipelinesMagic));
+ file.write(reinterpret_cast<const char*>(&count), sizeof(count));
+ for (const auto& [hash, age] : next) {
+ file.write(reinterpret_cast<const char*>(&hash), sizeof(hash));
+ file.write(reinterpret_cast<const char*>(&age), sizeof(age));
+ }
+}
+
GraphicsPipeline* PipelineCache::CurrentGraphicsPipeline() {
if (!RefreshStages(graphics_key.unique_hashes)) {
@@ -578,6 +706,7 @@ GraphicsPipeline* PipelineCache::CurrentGraphicsPipeline() {
GraphicsPipeline* const next{current_pipeline->Next(graphics_key)};
if (next) {
current_pipeline = next;
+ current_pipeline->MarkUsed(frame_count);
return BuiltPipeline(current_pipeline);
}
}
@@ -625,6 +754,11 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
return;
}
pipeline_cache_filename = base_dir / "vulkan.bin";
+ used_pipelines_filename = base_dir / "vulkan_used.bin";
+ used_pipelines = LoadUsedPipelines(used_pipelines_filename, MaxUsedPipelineAge(device));
+ used_pipelines_saved = std::chrono::steady_clock::now();
+ size_t skipped_pipelines = 0;
+ size_t queued_graphics = 0;
if (use_vulkan_pipeline_cache) {
vulkan_pipeline_cache_filename = base_dir / "vulkan_pipelines.bin";
@@ -665,6 +799,15 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
GraphicsPipelineCacheKey key;
file.read(reinterpret_cast<char*>(&key), sizeof(key));
+ // Task/mesh pipelines are not stored (their mesh registers are not in the format); skip
+ // any that were.
+ if (std::ranges::any_of(envs, [](const FileEnvironment& env) {
+ return env.ShaderStage() == Shader::Stage::Task ||
+ env.ShaderStage() == Shader::Stage::Mesh;
+ })) {
+ return;
+ }
+
if ((key.state.extended_dynamic_state != 0) !=
dynamic_features.has_extended_dynamic_state ||
(key.state.extended_dynamic_state_2 != 0) !=
@@ -693,6 +836,16 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
return;
}
+ const u64 hash = key.Hash();
+ disk_pipelines.insert(hash);
+ if (!PreloadAllPipelines() &&
+ (used_pipelines.empty() ? queued_graphics >= MaxPipelinesWithoutUsedList
+ : !used_pipelines.contains(hash))) {
+ ++skipped_pipelines;
+ return;
+ }
+ ++queued_graphics;
+
workers.QueueWork([this, key, envs_ = std::move(envs), &state, &callback]() mutable {
ShaderPools pools;
boost::container::static_vector<Shader::Environment*, 5> env_ptrs;
@@ -717,6 +870,11 @@ void PipelineCache::LoadDiskResources(u64 title_id, std::stop_token stop_loading
load_graphics);
LOG_INFO(Render_Vulkan, "Total Pipeline Count: {}", state.total);
+ if (skipped_pipelines != 0) {
+ LOG_INFO(Render_Vulkan,
+ "{} cached graphics pipelines are built when first needed",
+ skipped_pipelines);
+ }
std::unique_lock lock{state.mutex};
callback(VideoCore::LoadCallbackStage::Build, 0, state.total);
@@ -833,13 +991,71 @@ GraphicsPipeline* PipelineCache::CurrentGraphicsPipelineSlowPath() {
if (!pipeline) {
return nullptr;
}
+ if (used_this_session.insert(pipeline.get()).second &&
+ std::chrono::steady_clock::now() - used_pipelines_saved > std::chrono::minutes{2}) {
+ SaveUsedPipelines();
+ }
if (current_pipeline) {
current_pipeline->AddTransition(pipeline.get());
}
current_pipeline = pipeline.get();
+ current_pipeline->MarkUsed(frame_count);
return BuiltPipeline(current_pipeline);
}
+void PipelineCache::TickFrame() {
+ ++frame_count;
+ while (!evicted_pipelines.empty() && scheduler.IsFree(evicted_pipelines.front().first)) {
+ evicted_pipelines.pop_front();
+ }
+ constexpr u64 EvictCheckFrames = 600;
+ if (frame_count % EvictCheckFrames == 0) {
+ EvictUnusedPipelines();
+ }
+}
+
+void PipelineCache::EvictUnusedPipelines() {
+ // Building a pipeline again on MoltenVK means a Metal compile mid-game, far dearer than the
+ // 2-3 ms below, so none are evicted there.
+ if (device.IsMoltenVK()) {
+ return;
+ }
+ // Each built pipeline holds ~0.45 MB of driver memory and none was ever destroyed, so it grew
+ // with every place a session went (title 1: 1,500 on its title screen alone). One unused for
+ // this long is destroyed; it takes 2-3 ms to build again from the driver's pipeline cache.
+ constexpr u64 EvictAfterFrames = 9000;
+ if (frame_count <= EvictAfterFrames) {
+ return;
+ }
+ const u64 cutoff = frame_count - EvictAfterFrames;
+ const u64 tick = scheduler.CurrentTick();
+ size_t evicted = 0;
+ for (auto it = graphics_cache.begin(); it != graphics_cache.end();) {
+ GraphicsPipeline* const pipeline = it->second.get();
+ if (pipeline == nullptr || pipeline == current_pipeline || !pipeline->IsBuilt() ||
+ pipeline->LastUse() >= cutoff) {
+ ++it;
+ continue;
+ }
+ if (used_this_session.erase(pipeline) != 0) {
+ used_and_evicted.insert(it->first.Hash());
+ }
+ evicted_pipelines.emplace_back(tick, std::move(it->second));
+ it = graphics_cache.erase(it);
+ ++evicted;
+ }
+ if (evicted == 0) {
+ return;
+ }
+ for (auto& [key, pipeline] : graphics_cache) {
+ if (pipeline) {
+ pipeline->ClearTransitions();
+ }
+ }
+ LOG_INFO(Render_Vulkan, "Evicted {} graphics pipelines unused for {} frames, {} left", evicted,
+ EvictAfterFrames, graphics_cache.size());
+}
+
GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const noexcept {
if (pipeline->IsBuilt()) {
return pipeline;
@@ -856,7 +1072,7 @@ GraphicsPipeline* PipelineCache::BuiltPipeline(GraphicsPipeline* pipeline) const
// Usually these shaders are only used once for building textures so we can assume they
// can't be built async
const auto& draw_state = maxwell3d->draw_manager.draw_state;
- if (draw_state.index_buffer.count <= 6 || draw_state.vertex_buffer.count <= 6) {
+ if (draw_state.draw_element_count <= 6) {
return pipeline;
}
return nullptr;
@@ -981,7 +1197,12 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline(
++env_index;
const u32 cfg_offset{static_cast<u32>(env.StartAddress() + sizeof(Shader::ProgramHeader))};
- Shader::Maxwell::Flow::CFG cfg(env, pools.flow_block, cfg_offset, index == 0);
+ try {
+ // Walks the program for the dump; the one that failed may fail here again, and an
+ // exception leaving this handler would end the process.
+ Shader::Maxwell::Flow::CFG cfg(env, pools.flow_block, cfg_offset, index == 0);
+ } catch (const Shader::Exception&) {
+ }
env.Dump(hash, key.unique_hashes[index]);
}
LOG_ERROR(Render_Vulkan, "{}", exception.what());
@@ -1029,6 +1250,18 @@ std::unique_ptr<GraphicsPipeline> PipelineCache::CreateGraphicsPipeline() {
if (!pipeline || pipeline_cache_filename.empty()) {
return pipeline;
}
+ if (pipeline->IsMeshPipeline()) {
+ // Not stored: task and mesh programs are translated with the mesh registers, which the
+ // cache format does not carry. The few of them rebuild quickly from the driver cache.
+ QueueVulkanPipelineCacheFlush();
+ return pipeline;
+ }
+ if (disk_pipelines.contains(graphics_key.Hash())) {
+ // In the disk cache already, only not built at boot.
+ QueueVulkanPipelineCacheFlush();
+ return pipeline;
+ }
+ disk_pipelines.insert(graphics_key.Hash());
serialization_thread.QueueWork([this, key = graphics_key, envs = std::move(environments.envs)] {
boost::container::static_vector<const GenericEnvironment*, Maxwell::MaxShaderProgram>
env_ptrs;
@@ -1051,7 +1284,8 @@ std::unique_ptr<ComputePipeline> PipelineCache::CreateComputePipeline(
};
const GPUVAddr program_base{kepler_compute->regs.code_loc.Address()};
const auto& qmd{kepler_compute->launch_description};
- ComputeEnvironment env{*kepler_compute, *gpu_memory, program_base, qmd.program_start};
+ ComputeEnvironment env{*kepler_compute, *gpu_memory, program_base, qmd.program_start,
+ shader->lowered_native};
env.SetCachedSize(shader->size_bytes);
main_pools.ReleaseContents();
diff --git a/src/video_core/renderer_vulkan/vk_pipeline_cache.h b/src/video_core/renderer_vulkan/vk_pipeline_cache.h
index 71dfb46e3d..66fa615ea5 100644
--- a/src/video_core/renderer_vulkan/vk_pipeline_cache.h
+++ b/src/video_core/renderer_vulkan/vk_pipeline_cache.h
@@ -10,7 +10,10 @@
#include <atomic>
#include <chrono>
#include <cstddef>
+#include <deque>
#include <filesystem>
+#include <unordered_map>
+#include <unordered_set>
#include <memory>
#include <type_traits>
#include "common/container/unordered_map.h"
@@ -114,6 +117,8 @@ public:
[[nodiscard]] GraphicsPipeline* CurrentGraphicsPipeline();
+ void TickFrame();
+
[[nodiscard]] ComputePipeline* CurrentComputePipeline();
void LoadDiskResources(u64 title_id, std::stop_token stop_loading,
@@ -163,6 +168,12 @@ private:
GraphicsPipelineCacheKey graphics_key{};
GraphicsPipeline* current_pipeline{};
+ /// Destroys graphics pipelines unused for a while, once the GPU is done with them.
+ void EvictUnusedPipelines();
+ u64 frame_count{};
+ std::deque<std::pair<u64, std::unique_ptr<GraphicsPipeline>>> evicted_pipelines; ///< (tick, pipeline)
+ std::unordered_set<u64> used_and_evicted; ///< Key hashes of pipelines used this session, evicted since
+
::Common::unordered_map<ComputePipelineCacheKey, std::unique_ptr<ComputePipeline>> compute_cache;
::Common::unordered_map<GraphicsPipelineCacheKey, std::unique_ptr<GraphicsPipeline>> graphics_cache;
@@ -173,6 +184,16 @@ private:
std::filesystem::path pipeline_cache_filename;
+ /// Key hashes of the graphics pipelines in the disk cache. Only those a recent session used
+ /// (used_pipelines) are built at boot; the rest are built when first needed, and not stored
+ /// again. Each built pipeline holds driver memory (title 1: 6.5k cached, a quarter used).
+ std::unordered_set<u64> disk_pipelines;
+ std::unordered_map<u64, u8> used_pipelines; ///< Key hash -> sessions since it was last used
+ std::unordered_set<const GraphicsPipeline*> used_this_session;
+ std::filesystem::path used_pipelines_filename;
+ std::chrono::steady_clock::time_point used_pipelines_saved{};
+ void SaveUsedPipelines();
+
std::filesystem::path vulkan_pipeline_cache_filename;
vk::PipelineCache vulkan_pipeline_cache;
size_t pipelines_since_flush{};
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.cpp b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
index cb31a0e7e4..3898c571f5 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.cpp
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.cpp
@@ -9,6 +9,7 @@
#include <memory>
#include <cstdio>
#include <cstdlib>
+#include <filesystem>
#include <tuple>
#include <set>
#include <fstream>
@@ -279,9 +280,37 @@ void RasterizerVulkan::PrepareDraw(bool is_indexed, Func&& draw_func) {
report_failure("no pipeline");
return;
}
+ // Task/mesh pipelines: the draw's vertex count is the number of task workgroups.
+ mesh_draw = pipeline->IsMeshPipeline();
+ // EDEN_NX2_NO_MESH=1: skip task/mesh draws as before they were supported (A/B comparisons).
+ // EDEN_NX2_NO_MESH_FILE=<path>: skip them while that file exists (checked every 1024 draws),
+ // to compare the same scene with and without them in one run.
+ static const bool no_mesh_always{std::getenv("EDEN_NX2_NO_MESH") != nullptr};
+ static const char* const no_mesh_file{std::getenv("EDEN_NX2_NO_MESH_FILE")};
+ static bool no_mesh{no_mesh_always};
+ static u32 no_mesh_check{};
+ if (no_mesh_file && (no_mesh_check++ % 1024) == 0) {
+ no_mesh = std::filesystem::exists(no_mesh_file);
+ }
+ if (mesh_draw && (no_mesh || !device.IsExtMeshShaderSupported())) {
+ report_failure(no_mesh ? "EDEN_NX2_NO_MESH" : "no VK_EXT_mesh_shader");
+ return;
+ }
std::scoped_lock lock{buffer_cache.mutex, texture_cache.mutex};
// update engine as channel may be different.
pipeline->SetEngine(maxwell3d, gpu_memory);
+ const u32 repack_dirty = buffer_cache_runtime.SetVertexRepackLayout(
+ maxwell3d->regs, pipeline->HasDynamicVertexInput());
+ if (repack_dirty) {
+ auto& flags = maxwell3d->dirty.flags;
+ flags[VideoCommon::Dirty::VertexBuffers] = true;
+ flags[Dirty::VertexInput] = true;
+ for (u32 binding = 0; binding < 32; ++binding) {
+ if (repack_dirty & (1U << binding)) {
+ flags[VideoCommon::Dirty::VertexBuffer0 + binding] = true;
+ }
+ }
+ }
if (Profile::Scope scope{Profile::Zone::Configure}; !pipeline->Configure(is_indexed)) {
report_failure("configure failed");
return;
@@ -541,6 +570,12 @@ void RasterizerVulkan::Draw(bool is_indexed, u32 instance_count) {
const u32 num_instances{instance_count};
const DrawParams draw_params{MakeDrawParams(draw_state, num_instances, is_indexed)};
+ if (mesh_draw) {
+ scheduler.Record([groups = draw_params.num_vertices](vk::CommandBuffer cmdbuf) {
+ cmdbuf.DrawMeshTasksEXT(groups, 1, 1);
+ });
+ return;
+ }
scheduler.Record([draw_params](vk::CommandBuffer cmdbuf) {
if (draw_params.is_indexed) {
cmdbuf.DrawIndexed(draw_params.num_vertices, draw_params.num_instances,
@@ -573,6 +608,14 @@ void RasterizerVulkan::DrawIndirect() {
const auto& params = maxwell3d->draw_manager.indirect_state;
buffer_cache.SetDrawIndirect(&params);
PrepareDraw(params.is_indexed, [this, &params] {
+ if (mesh_draw) {
+ // Indirect task/mesh draws would need vkCmdDrawMeshTasksIndirectEXT's layout.
+ static bool logged{};
+ if (!std::exchange(logged, true)) {
+ LOG_WARNING(Render_Vulkan, "Indirect task/mesh draw skipped");
+ }
+ return;
+ }
const auto indirect_buffer = buffer_cache.GetDrawIndirectBuffer();
const auto& buffer = indirect_buffer.first;
const auto& offset = indirect_buffer.second;
@@ -879,6 +922,11 @@ void RasterizerVulkan::DispatchCompute() {
return;
}
std::scoped_lock lock{texture_cache.mutex, buffer_cache.mutex};
+ // Command list pages the dispatch writes are copied for the split's front right after it.
+ buffer_cache.Nx2CollectListCopies(true);
+ SCOPE_EXIT {
+ buffer_cache.Nx2CollectListCopies(false);
+ };
if (!pipeline->Configure(*kepler_compute, *gpu_memory, scheduler, buffer_cache,
texture_cache)) {
if (kepler_compute->IsNativeMode()) throw Shader::RuntimeError("Native compute resources unavailable");
@@ -901,6 +949,7 @@ void RasterizerVulkan::DispatchCompute() {
}
cmdbuf.DispatchIndirect(indirect_buffer, indirect_offset);
});
+ SubmitNx2ListCopies();
return;
}
const std::array<u32, 3> dim{qmd.grid_dim_x, qmd.grid_dim_y, qmd.grid_dim_z};
@@ -923,6 +972,7 @@ void RasterizerVulkan::DispatchCompute() {
}
cmdbuf.Dispatch(dim[0], dim[1], dim[2]);
});
+ SubmitNx2ListCopies();
// Log compute dispatch
if (GPU::Logging::IsActive() &&
@@ -964,6 +1014,29 @@ void Vulkan::RasterizerVulkan::DisableGraphicsUniformBuffer(size_t stage, u32 in
void RasterizerVulkan::FlushAll() {}
+void RasterizerVulkan::SubmitNx2ListCopies() {
+ if (buffer_cache.Nx2CopyListPages(scheduler.CurrentTick())) {
+ scheduler.Flush();
+ }
+}
+
+VideoCore::RasterizerInterface::GpuWrittenList RasterizerVulkan::ReadGpuWrittenCommandList(
+ DAddr addr, std::span<u8> out) {
+ // No cache lock: the front only reads the copies, under their own lock.
+ switch (buffer_cache.Nx2ReadListCopy(addr, out)) {
+ case BufferCache::Nx2ListRead::Copied:
+ return GpuWrittenList::Copied;
+ case BufferCache::Nx2ListRead::Synced:
+ return GpuWrittenList::Synced;
+ default:
+ return GpuWrittenList::Unknown;
+ }
+}
+
+void RasterizerVulkan::NoteGpuWrittenCommandListSynced(DAddr page) {
+ buffer_cache.Nx2NoteListSynced(page);
+}
+
void RasterizerVulkan::FlushRegion(DAddr addr, u64 size, VideoCommon::CacheType which) {
if (addr == 0 || size == 0) {
return;
@@ -1054,12 +1127,10 @@ void RasterizerVulkan::InnerInvalidation(std::span<const std::pair<DAddr, std::s
buffer_cache.WriteMemory(addr, size);
}
}
- {
- for (const auto& [addr, size] : sequences) {
- query_cache.InvalidateRegion(addr, size);
- pipeline_cache.InvalidateRegion(addr, size);
- }
+ for (const auto& [addr, size] : sequences) {
+ query_cache.InvalidateRegion(addr, size);
}
+ pipeline_cache.InvalidateRegions(sequences);
}
bool RasterizerVulkan::OnCPUWrite(DAddr addr, u64 size) {
@@ -1200,6 +1271,7 @@ void RasterizerVulkan::FlushCommands() {
void RasterizerVulkan::TickFrame() {
draw_counter = 0;
+ pipeline_cache.TickFrame();
guest_descriptor_queue.TickFrame();
compute_pass_descriptor_queue.TickFrame();
descriptor_buffer_ring.TickFrame();
@@ -1231,6 +1303,14 @@ bool RasterizerVulkan::AccelerateSurfaceCopy(const Tegra::Engines::Fermi2D::Surf
return texture_cache.BlitImage(dst, src, copy_config);
}
+bool RasterizerVulkan::AccelerateTemporalUpscale(
+ const Tegra::Engines::Fermi2D::Surface& src, const Tegra::Engines::Fermi2D::Surface& dst,
+ const Tegra::Engines::Fermi2D::Surface& motion_vectors, std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale) {
+ std::scoped_lock lock{texture_cache.mutex};
+ return texture_cache.TemporalUpscale(dst, src, motion_vectors, jitter, motion_scale);
+}
+
Tegra::Engines::AccelerateDMAInterface& RasterizerVulkan::AccessAccelerateDMA() {
return accelerate_dma;
}
@@ -1267,6 +1347,8 @@ void RasterizerVulkan::AccelerateInlineToMemory(GPUVAddr address, size_t copy_si
bool uploaded;
{
std::scoped_lock lock{buffer_cache.mutex};
+ // Guest memory has the words now: a command list copy of the page no longer does.
+ buffer_cache.Nx2NoteListHostWrite(*cpu_addr, copy_size);
uploaded = buffer_cache.InlineMemory(*cpu_addr, copy_size, memory);
if (!uploaded && !defer) {
buffer_cache.WriteMemory(*cpu_addr, copy_size);
@@ -1340,10 +1422,27 @@ void RasterizerVulkan::FlushWork() {
#endif // __ANDROID__
static_assert(DRAWS_TO_DISPATCH % (CHECK_MASK + 1) == 0);
- if ((++draw_counter & CHECK_MASK) != CHECK_MASK) {
+ if ((++draw_counter & CHECK_MASK) != 0) {
return;
}
- if (draw_counter < DRAWS_TO_DISPATCH) {
+ // Keep the host GPU working while native commands are still being recorded.
+ static const u32 native_flush_draws = [] {
+ constexpr u32 fallback = 128;
+ const char* value = std::getenv("EDEN_NX2_FLUSH_DRAWS");
+ if (value == nullptr || value[0] < '0' || value[0] > '9') {
+ return fallback;
+ }
+ char* end = nullptr;
+ const unsigned long parsed = std::strtoul(value, &end, 10);
+ if (*end != '\0' || parsed == 0) {
+ return fallback;
+ }
+ return static_cast<u32>(std::clamp(parsed, 32UL, 1'048'576UL));
+ }();
+ const u32 draws_to_dispatch = Tegra::g_nx2_ampere.load(std::memory_order_relaxed)
+ ? native_flush_draws
+ : DRAWS_TO_DISPATCH;
+ if (draw_counter < draws_to_dispatch) {
scheduler.DispatchWork();
return;
}
@@ -1362,7 +1461,13 @@ bool AccelerateDMA::BufferClear(GPUVAddr src_address, u64 amount, u32 value) {
bool AccelerateDMA::BufferCopy(GPUVAddr src_address, GPUVAddr dest_address, u64 amount) {
std::scoped_lock lock{buffer_cache.mutex};
- return buffer_cache.DMACopy(src_address, dest_address, amount);
+ buffer_cache.Nx2CollectListCopies(true);
+ const bool copied = buffer_cache.DMACopy(src_address, dest_address, amount);
+ if (buffer_cache.Nx2CopyListPages(scheduler.CurrentTick())) {
+ scheduler.Flush();
+ }
+ buffer_cache.Nx2CollectListCopies(false);
+ return copied;
}
template <bool IS_IMAGE_UPLOAD>
@@ -2261,7 +2366,8 @@ void RasterizerVulkan::UpdateVertexInput(Tegra::Engines::Maxwell3D::Regs& regs)
.location = index,
.binding = binding,
.format = MaxwellToVK::VertexFormat(device, attribute.type, attribute.size),
- .offset = attribute.offset,
+ .offset = attribute.offset +
+ (buffer_cache_runtime.IsVertexRepacked(binding) && attribute.offset >= 65 ? 3 : 0),
});
}
@@ -2272,7 +2378,7 @@ void RasterizerVulkan::UpdateVertexInput(Tegra::Engines::Maxwell3D::Regs& regs)
.sType = VK_STRUCTURE_TYPE_VERTEX_INPUT_BINDING_DESCRIPTION_2_EXT,
.pNext = nullptr,
.binding = binding,
- .stride = input_binding.stride,
+ .stride = buffer_cache_runtime.IsVertexRepacked(binding) ? 72U : input_binding.stride.Value(),
.inputRate = is_instanced ? VK_VERTEX_INPUT_RATE_INSTANCE : VK_VERTEX_INPUT_RATE_VERTEX,
.divisor = is_instanced ? input_binding.frequency : 1,
});
diff --git a/src/video_core/renderer_vulkan/vk_rasterizer.h b/src/video_core/renderer_vulkan/vk_rasterizer.h
index ae9929ca0b..dbb6ebdca8 100644
--- a/src/video_core/renderer_vulkan/vk_rasterizer.h
+++ b/src/video_core/renderer_vulkan/vk_rasterizer.h
@@ -99,6 +99,8 @@ public:
void FlushAll() override;
void FlushRegion(DAddr addr, u64 size,
VideoCommon::CacheType which = VideoCommon::CacheType::All) override;
+ GpuWrittenList ReadGpuWrittenCommandList(DAddr addr, std::span<u8> out) override;
+ void NoteGpuWrittenCommandListSynced(DAddr page) override;
bool MustFlushRegion(DAddr addr, u64 size,
VideoCommon::CacheType which = VideoCommon::CacheType::All) override;
VideoCore::RasterizerDownloadArea GetFlushArea(DAddr addr, u64 size) override;
@@ -128,6 +130,11 @@ public:
bool AccelerateSurfaceCopy(const Tegra::Engines::Fermi2D::Surface& src,
const Tegra::Engines::Fermi2D::Surface& dst,
const Tegra::Engines::Fermi2D::Config& copy_config) override;
+ bool AccelerateTemporalUpscale(const Tegra::Engines::Fermi2D::Surface& src,
+ const Tegra::Engines::Fermi2D::Surface& dst,
+ const Tegra::Engines::Fermi2D::Surface& motion_vectors,
+ std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale) override;
Tegra::Engines::AccelerateDMAInterface& AccessAccelerateDMA() override;
void AccelerateInlineToMemory(GPUVAddr address, size_t copy_size,
std::span<const u8> memory) override;
@@ -160,6 +167,10 @@ private:
void FlushWork();
+ /// Records the copies of the command list pages the work just recorded wrote and submits, so
+ /// the GPU makes them now (BufferCache::Nx2CopyListPages).
+ void SubmitNx2ListCopies();
+
void UpdateDynamicStates();
void HandleTransformFeedback();
@@ -230,6 +241,7 @@ private:
boost::container::static_vector<VkSampler, MAX_TEXTURES> sampler_handles;
u32 draw_counter = 0;
+ bool mesh_draw{}; ///< the draw being prepared uses a task/mesh pipeline
};
} // namespace Vulkan
diff --git a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp
index 7b6571d33d..98787b9add 100644
--- a/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp
+++ b/src/video_core/renderer_vulkan/vk_staging_buffer_pool.cpp
@@ -36,11 +36,25 @@ constexpr VkDeviceSize MAX_ALIGNMENT = 256;
#if defined(__FreeBSD__)
constexpr VkDeviceSize MAX_STREAM_BUFFER_SIZE = 128_MiB;
#else
-constexpr VkDeviceSize MAX_STREAM_BUFFER_SIZE = 256_MiB;
+// 128 MiB on Windows too: 256 MiB sat mostly unused in device memory on Switch 2 titles.
+constexpr VkDeviceSize MAX_STREAM_BUFFER_SIZE = 128_MiB;
#endif
size_t GetStreamBufferSize(const Device& device) {
if (!device.HasDebuggingToolAttached()) {
+ if (device.IsIntegrated()) {
+ // An integrated GPU's device-local host-visible heap can be no bigger than the stream
+ // buffer (Radeon 780M: 256 MiB, 243 MiB budget), which then sat over that budget.
+ // Leave a quarter of it free.
+ VkDeviceSize heap_size = 0;
+ ForEachDeviceLocalHostVisibleHeap(
+ device, [&heap_size](size_t, VkMemoryHeap& heap) {
+ heap_size = (std::max)(heap_size, heap.size);
+ });
+ if (heap_size != 0 && heap_size <= MAX_STREAM_BUFFER_SIZE) {
+ return Common::AlignUp(heap_size * 3 / 4, MAX_ALIGNMENT);
+ }
+ }
return MAX_STREAM_BUFFER_SIZE;
}
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.cpp b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
index 5ba36feee9..928146027d 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.cpp
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.cpp
@@ -172,7 +172,10 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
// A large image over Switch 2 sparse blocks nobody has written (title 1's terrain layer pools, 2.9
// GB of R8/RG16/R16 that stay empty in the attract demo) is all zeros: it gets no device memory
// until a write makes the texture cache replace it.
-[[nodiscard]] bool IsUnwrittenSparse(const ImageInfo& info, GPUVAddr gpu_addr) {
+[[nodiscard]] bool IsUnwrittenSparse(const Device& device, const ImageInfo& info, GPUVAddr gpu_addr) {
+ if (!device.CanUseUnbackedSparseImages()) {
+ return false;
+ }
constexpr u32 MinSize = 32U << 20;
const u32 size = VideoCommon::CalculateGuestSizeInBytes(info);
return size >= MinSize && info.type != ImageType::e1D && info.type != ImageType::Buffer &&
@@ -184,7 +187,8 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
// Image::BindLayers. Only formats uploaded layer by layer: huge ASTC arrays (GPU decode) and
// formats the host samples as they are.
[[nodiscard]] bool UsesSparseLayers(const Device& device, const ImageInfo& info, GPUVAddr gpu_addr) {
- if (!device.IsSparseResidencyImage2DSupported() || info.type != ImageType::e2D ||
+ if (!device.CanUseUnbackedSparseImages() || !device.IsSparseResidencyImage2DSupported() ||
+ info.type != ImageType::e2D ||
info.resources.layers <= 1 || info.num_samples != 1 ||
VideoCore::Surface::GetFormatType(info.format) != VideoCore::Surface::SurfaceType::ColorTexture) {
return false;
@@ -197,6 +201,78 @@ constexpr VkBorderColor ConvertBorderColor(const std::array<float, 4>& color) {
return layer_uploads && size >= (32U << 20) && Tegra::Nx2InSparseRegion(gpu_addr, size);
}
+// How SparseTiles images leave the blocks without data: unbound (reads give zero), or all bound to
+// one zeroed block where the driver can't sample unbound memory (AMD) but lets blocks share
+// memory. EDEN_NX2_SPARSE_ALIASED=1 takes the shared block wherever it is supported, =0 never.
+enum class SparseTilesMode {
+ None,
+ Unbound,
+ Aliased,
+};
+
+[[nodiscard]] SparseTilesMode SparseTilesModeOf(const Device& device) {
+ static const char force_aliased = [] {
+ const char* value = std::getenv("EDEN_NX2_SPARSE_ALIASED");
+ return value == nullptr ? '\0' : value[0];
+ }();
+ if (!device.IsSparseResidencyImage2DSupported()) {
+ return SparseTilesMode::None;
+ }
+ const bool aliased = device.IsSparseResidencyAliasedSupported() && force_aliased != '0';
+ if (aliased && force_aliased == '1') {
+ return SparseTilesMode::Aliased;
+ }
+ if (device.CanUseUnbackedSparseImages()) {
+ return SparseTilesMode::Unbound;
+ }
+ return aliased ? SparseTilesMode::Aliased : SparseTilesMode::None;
+}
+
+// A large single 2D image in a Switch 2 sparse region (title 1's world-sized R8/RG16/R16 masks,
+// 170-341 MB, of which the game maps a few MiB) gets memory by sparse block where the guest memory
+// under it can hold data: unbacked at first, then Image::BindTiles. Formats of one texel per
+// block that the host samples as they are.
+[[nodiscard]] bool UsesSparseTiles(const Device& device, const ImageInfo& info, GPUVAddr gpu_addr) {
+ if (SparseTilesModeOf(device) == SparseTilesMode::None || info.type != ImageType::e2D || info.resources.layers != 1 || info.num_samples != 1 ||
+ info.size.depth != 1 || info.block.width != 0 ||
+ VideoCore::Surface::DefaultBlockWidth(info.format) != 1 ||
+ VideoCore::Surface::DefaultBlockHeight(info.format) != 1 ||
+ VideoCore::Surface::GetFormatType(info.format) != VideoCore::Surface::SurfaceType::ColorTexture) {
+ return false;
+ }
+ const u32 size = VideoCommon::CalculateGuestSizeInBytes(info);
+ return size >= (32U << 20) && Tegra::Nx2InSparseRegion(gpu_addr, size);
+}
+
+// Vulkan's standard sparse block shape for single-sampled 2D images (64 KiB blocks), in texel
+// blocks, for a texel block of `block_bytes`. Only valid when the device reports
+// residencyStandard2DBlockShape.
+[[nodiscard]] bool StandardSparseBlockShape(const Device& device, u64 block_bytes,
+ VkExtent3D& shape) {
+ if (!device.GetPhysical().GetProperties().sparseProperties.residencyStandard2DBlockShape) {
+ return false;
+ }
+ switch (block_bytes) {
+ case 1:
+ shape = {256, 256, 1};
+ return true;
+ case 2:
+ shape = {256, 128, 1};
+ return true;
+ case 4:
+ shape = {128, 128, 1};
+ return true;
+ case 8:
+ shape = {128, 64, 1};
+ return true;
+ case 16:
+ shape = {64, 64, 1};
+ return true;
+ default:
+ return false;
+ }
+}
+
// The most device memory one SparseLayers image binds: 768 MiB, 384 MiB on GPUs with less than
// 7 GiB (title 1's BC6H arrays are 85 MB a layer).
[[nodiscard]] VkDeviceSize SparseLayersCap(const Device& device) {
@@ -257,6 +333,20 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM
info.size.depth == 1;
}
+// The most a trimmed ASTC array keeps on the host: 512 MiB, 256 MiB on GPUs with less than 7 GiB
+// (title 1's two terrain arrays took 724 MiB of a 6 GB card next to ~1.2 GB of other sparse terrain
+// textures). EDEN_NX2_ASTC_ARRAY_MIB=<MiB> sets it.
+[[nodiscard]] u64 TrimmedAstcArrayLimit(const Device& device) {
+ static const u64 forced = [] {
+ const char* value = std::getenv("EDEN_NX2_ASTC_ARRAY_MIB");
+ return value != nullptr ? std::strtoull(value, nullptr, 10) << 20 : 0ULL;
+ }();
+ if (forced != 0) {
+ return forced;
+ }
+ return device.GetDeviceLocalMemory() < (u64{7} << 30) ? (u64{256} << 20) : (u64{512} << 20);
+}
+
// The host shape of an image whose decoded copy would not fit in VRAM: a GPU-decoded ASTC array
// over 1 GiB as RGBA8 (title 1's 8192x8192x68 ASTC 8x8 virtual textures are 24 GB each) keeps only
// its mip levels from `level_offset` on, so host mip 0 is guest mip `level_offset`. Normalized
@@ -277,8 +367,9 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM
if (decoded_bytes(0) <= (u64{1} << 30)) {
return info;
}
+ const u64 limit = TrimmedAstcArrayLimit(device);
while (level_offset + 1 < static_cast<u32>(info.resources.levels) &&
- decoded_bytes(level_offset) > (u64{512} << 20)) {
+ decoded_bytes(level_offset) > limit) {
++level_offset;
}
ImageInfo host = info;
@@ -349,7 +440,7 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM
[[nodiscard]] vk::Image MakeImage(const Device& device, const MemoryAllocator& allocator,
const ImageInfo& info, std::span<const VkFormat> view_formats,
std::optional<VkFormat> format_override = {},
- bool unbacked = false) {
+ bool unbacked = false, bool aliased = false) {
if (info.type == ImageType::Buffer) {
return vk::Image{};
}
@@ -378,7 +469,7 @@ constexpr std::array<VkFormat, 2> OpaqueAstcViewFormats{VK_FORMAT_BC1_RGBA_UNORM
}
}
if (unbacked) {
- if (vk::Image image = allocator.CreateUnbackedImage(image_ci)) {
+ if (vk::Image image = allocator.CreateUnbackedImage(image_ci, aliased)) {
return image;
}
}
@@ -1444,6 +1535,154 @@ void TextureCacheRuntime::ReinterpretImage(Image& dst, Image& src,
});
}
+bool TextureCacheRuntime::TemporalUpscale(Framebuffer* dst_framebuffer, ImageView& dst,
+ ImageView& input_color, ImageView& motion_vectors,
+ std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale) {
+ if (dst.Samples() != VK_SAMPLE_COUNT_1_BIT || ImageAspectMask(dst.format) != VK_IMAGE_ASPECT_COLOR_BIT) {
+ return false;
+ }
+ const VkFormat format =
+ MaxwellToVK::SurfaceFormat(device, FormatType::Optimal, false, dst.format).format;
+ const VkExtent2D extent{dst.size.width, dst.size.height};
+ const VkImage output = dst.ImageHandle();
+ bool reset = false;
+ if (!temporal_history.image || temporal_history.format != format ||
+ temporal_history.extent.width != extent.width ||
+ temporal_history.extent.height != extent.height) {
+ if (temporal_history.image) {
+ scheduler.Finish(); // the old history may still be sampled
+ }
+ temporal_history.image = memory_allocator.CreateImage(VkImageCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .imageType = VK_IMAGE_TYPE_2D,
+ .format = format,
+ .extent = {extent.width, extent.height, 1},
+ .mipLevels = 1,
+ .arrayLayers = 1,
+ .samples = VK_SAMPLE_COUNT_1_BIT,
+ .tiling = VK_IMAGE_TILING_OPTIMAL,
+ .usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT,
+ .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
+ .queueFamilyIndexCount = 0,
+ .pQueueFamilyIndices = nullptr,
+ .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ });
+ temporal_history.view = device.GetLogical().CreateImageView(VkImageViewCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .image = *temporal_history.image,
+ .viewType = VK_IMAGE_VIEW_TYPE_2D,
+ .format = format,
+ .components{},
+ .subresourceRange{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = 0,
+ .levelCount = 1,
+ .baseArrayLayer = 0,
+ .layerCount = 1,
+ },
+ });
+ temporal_history.format = format;
+ temporal_history.extent = extent;
+ reset = true;
+ LOG_INFO(Render_Vulkan, "Temporal upscale into {}x{} (format {})", extent.width, extent.height,
+ static_cast<u32>(format));
+ }
+
+ // The history holds this pass's previous result, copied right after drawing it: the game may
+ // draw into its DLSS output afterwards (post-processing), which must not feed back. The history
+ // stays in the GENERAL layout, which the blit helper's descriptors use.
+ const VkImage history = *temporal_history.image;
+ const VkImageSubresourceLayers output_layers{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .mipLevel = static_cast<u32>(dst.range.base.level),
+ .baseArrayLayer = static_cast<u32>(dst.range.base.layer),
+ .layerCount = 1,
+ };
+ static constexpr VkImageSubresourceLayers history_layers{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .mipLevel = 0,
+ .baseArrayLayer = 0,
+ .layerCount = 1,
+ };
+ const auto barrier = [](VkImage image, VkAccessFlags src_access, VkAccessFlags dst_access,
+ VkImageLayout old_layout, const VkImageSubresourceLayers& layers) {
+ return VkImageMemoryBarrier{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+ .pNext = nullptr,
+ .srcAccessMask = src_access,
+ .dstAccessMask = dst_access,
+ .oldLayout = old_layout,
+ .newLayout = VK_IMAGE_LAYOUT_GENERAL,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = image,
+ .subresourceRange{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = layers.mipLevel,
+ .levelCount = 1,
+ .baseArrayLayer = layers.baseArrayLayer,
+ .layerCount = 1,
+ },
+ };
+ };
+ if (reset) {
+ // A new history image has no contents yet; the first frame does not read it.
+ scheduler.RequestOutsideRenderPassOperationContext();
+ scheduler.Record([history, barrier](vk::CommandBuffer cmdbuf) {
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
+ 0, barrier(history, 0, VK_ACCESS_SHADER_READ_BIT,
+ VK_IMAGE_LAYOUT_UNDEFINED, history_layers));
+ });
+ }
+ const Region2D dst_region{
+ .start = {0, 0},
+ .end = {static_cast<s32>(extent.width), static_cast<s32>(extent.height)},
+ };
+ // Each frame weighs at most 0.1 against the history (scaled by how close its samples land to
+ // the pixel; the neighbourhood clip limits what older samples contribute).
+ constexpr f32 current_weight = 0.1f;
+ blit_image_helper.TemporalUpscale(dst_framebuffer, input_color, motion_vectors,
+ *temporal_history.view, dst_region, jitter, motion_scale,
+ reset ? 1.0f : current_weight);
+ scheduler.RequestOutsideRenderPassOperationContext();
+ scheduler.Record([output, history, extent, output_layers, barrier](vk::CommandBuffer cmdbuf) {
+ constexpr VkAccessFlags written = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
+ const std::array before{
+ barrier(output, written, VK_ACCESS_TRANSFER_READ_BIT, VK_IMAGE_LAYOUT_GENERAL,
+ output_layers),
+ barrier(history, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT,
+ VK_IMAGE_LAYOUT_GENERAL, history_layers),
+ };
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT |
+ VK_PIPELINE_STAGE_FRAGMENT_SHADER_BIT,
+ VK_PIPELINE_STAGE_TRANSFER_BIT, 0, {}, {}, before);
+ const VkImageCopy copy{
+ .srcSubresource = output_layers,
+ .srcOffset = {0, 0, 0},
+ .dstSubresource = history_layers,
+ .dstOffset = {0, 0, 0},
+ .extent = {extent.width, extent.height, 1},
+ };
+ cmdbuf.CopyImage(output, VK_IMAGE_LAYOUT_GENERAL, history, VK_IMAGE_LAYOUT_GENERAL, copy);
+ const std::array after{
+ barrier(output, VK_ACCESS_TRANSFER_READ_BIT,
+ VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
+ VK_ACCESS_TRANSFER_READ_BIT,
+ VK_IMAGE_LAYOUT_GENERAL, output_layers),
+ barrier(history, VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT,
+ VK_IMAGE_LAYOUT_GENERAL, history_layers),
+ };
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0,
+ {}, {}, after);
+ });
+ return true;
+}
+
void TextureCacheRuntime::BlitImage(Framebuffer* dst_framebuffer, ImageView& dst, ImageView& src,
const Region2D& dst_region, const Region2D& src_region,
Tegra::Engines::Fermi2D::Filter filter,
@@ -1979,10 +2218,22 @@ u64 TextureCacheRuntime::GetDeviceMemoryUsage() const {
return device.GetDeviceMemoryUsage();
}
+u64 TextureCacheRuntime::CurrentTick() const noexcept {
+ return scheduler.CurrentTick();
+}
+
+bool TextureCacheRuntime::IsFree(u64 tick) const noexcept {
+ return scheduler.IsFree(tick);
+}
+
u64 TextureCacheRuntime::GetDeviceMemoryBudget() const {
return device.GetDeviceMemoryBudget();
}
+u64 TextureCacheRuntime::GetDeviceMemorySlack() const {
+ return device.GetDeviceMemorySlack();
+}
+
bool TextureCacheRuntime::CanDownloadMsaa(const VideoCommon::ImageInfo& info) const {
const VkImageAspectFlags aspect_mask = ImageAspectMask(info.format);
if ((aspect_mask & VK_IMAGE_ASPECT_COLOR_BIT) != 0) {
@@ -2060,10 +2311,33 @@ void TextureCacheRuntime::ReleaseMsaaScratchImage(VkImage image) {
}
void TextureCacheRuntime::TickFrame() {
+ static constexpr u32 MAX_UNUSED_SCRATCH_FRAMES = 60;
+ // Finished decode images wait for the next texture of their shape, up to this much memory.
+ static constexpr u64 MaxFreeTranscodeBytes = u64{256} << 20;
+ const auto decode_bytes = [](const PendingTranscode& t) {
+ u64 bytes{};
+ for (u32 level = 0; level < t.levels; ++level) {
+ bytes += u64{(std::max)(t.width >> level, 1U)} * (std::max)(t.height >> level, 1U) * 4;
+ }
+ return bytes * t.layers;
+ };
+ u64 free_bytes{};
+ for (const PendingTranscode& t : free_transcodes) {
+ free_bytes += decode_bytes(t);
+ }
while (!pending_transcodes.empty() && scheduler.IsFree(pending_transcodes.front().tick)) {
+ PendingTranscode& done = pending_transcodes.front();
+ const u64 bytes = decode_bytes(done);
+ if (free_bytes + bytes <= MaxFreeTranscodeBytes) {
+ done.unused_frames = 0;
+ free_bytes += bytes;
+ free_transcodes.push_back(std::move(done));
+ }
pending_transcodes.pop_front();
}
- static constexpr u32 MAX_UNUSED_SCRATCH_FRAMES = 60;
+ std::erase_if(free_transcodes, [](PendingTranscode& t) {
+ return ++t.unused_frames > MAX_UNUSED_SCRATCH_FRAMES;
+ });
std::erase_if(msaa_scratch_images, [this](MsaaScratchImage& scratch) {
if (!scheduler.IsFree(scratch.tick)) {
scratch.unused_frames = 0;
@@ -2076,20 +2350,31 @@ void TextureCacheRuntime::TickFrame() {
});
}
+namespace {
+
+// The host image of a texture; unbacked images are created without memory (see MakeImage).
+[[nodiscard]] vk::Image MakeHostImage(TextureCacheRuntime& runtime, const ImageInfo& info,
+ u32 host_level_offset, GPUVAddr gpu_addr, bool backed) {
+ const Device& device = runtime.device;
+ const bool sparse_tiles = !backed && UsesSparseTiles(device, info, gpu_addr);
+ return MakeImage(
+ device, runtime.memory_allocator, TrimmedHostInfo(device, info, host_level_offset),
+ IsHugeAstcArray(device, info) ? std::span<const VkFormat>{HugeAstcViewFormats}
+ : IsOpaqueBc1Astc(device, info) ? std::span<const VkFormat>{OpaqueAstcViewFormats}
+ : runtime.ViewFormats(info.format),
+ AstcHostFormat(device, info),
+ !backed && (UsesSparseLayers(device, info, gpu_addr) || sparse_tiles ||
+ IsUnwrittenSparse(device, info, gpu_addr)),
+ sparse_tiles && SparseTilesModeOf(device) == SparseTilesMode::Aliased);
+}
+
+} // Anonymous namespace
+
Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu_addr_,
VAddr cpu_addr_)
: VideoCommon::ImageBase(info_, gpu_addr_, cpu_addr_), scheduler{&runtime_.scheduler},
runtime{&runtime_},
- original_image(MakeImage(runtime_.device, runtime_.memory_allocator,
- TrimmedHostInfo(runtime_.device, info, host_level_offset),
- IsHugeAstcArray(runtime_.device, info)
- ? std::span<const VkFormat>{HugeAstcViewFormats}
- : IsOpaqueBc1Astc(runtime_.device, info)
- ? std::span<const VkFormat>{OpaqueAstcViewFormats}
- : runtime->ViewFormats(info.format),
- AstcHostFormat(runtime_.device, info),
- UsesSparseLayers(runtime_.device, info, gpu_addr_) ||
- IsUnwrittenSparse(info, gpu_addr_))),
+ original_image(MakeHostImage(runtime_, info, host_level_offset, gpu_addr_, false)),
aspect_mask(ImageAspectMask(info.format)) {
if (original_image.IsUnbacked()) {
flags |= VideoCommon::ImageFlagBits::Unbacked;
@@ -2098,6 +2383,19 @@ Image::Image(TextureCacheRuntime& runtime_, const ImageInfo& info_, GPUVAddr gpu
host_info.size.depth * static_cast<u64>(host_info.resources.layers);
if (UsesSparseLayers(runtime_.device, info, gpu_addr_) && InitSparseLayers()) {
flags |= VideoCommon::ImageFlagBits::SparseLayers;
+ } else if (UsesSparseTiles(runtime_.device, info, gpu_addr_) && InitSparseTiles()) {
+ flags |= VideoCommon::ImageFlagBits::SparseTiles;
+ } else if (UsesSparseTiles(runtime_.device, info, gpu_addr_) &&
+ SparseTilesModeOf(runtime_.device) == SparseTilesMode::Aliased) {
+ // The driver can't sample unbound memory (that is why the blocks were to share one),
+ // so an image whose tiles could not be set up is backed whole like any other.
+ LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: backing it whole", gpu_addr_);
+ original_image = MakeHostImage(runtime_, info, host_level_offset, gpu_addr_, true);
+ if (!original_image.IsUnbacked()) {
+ flags &= ~VideoCommon::ImageFlagBits::Unbacked;
+ } else if (texels <= (u64{1} << 28)) {
+ flags |= VideoCommon::ImageFlagBits::UnwrittenSparse;
+ }
} else if (texels <= (u64{1} << 28)) {
// Not too big to back (see MemoryAllocator::CreateImage): unbacked because nothing
// was written there yet, so the first write gets it replaced.
@@ -2598,7 +2896,24 @@ bool Image::InitSparseLayers() {
const u32 block_width = huge_astc ? 1 : VideoCore::Surface::DefaultBlockWidth(info.format);
const u32 block_height = huge_astc ? 1 : VideoCore::Surface::DefaultBlockHeight(info.format);
const u64 block_bytes = huge_astc ? 4 : VideoCore::Surface::BytesPerBlock(info.format);
- const VkExtent3D granularity = it->formatProperties.imageGranularity;
+ VkExtent3D granularity = it->formatProperties.imageGranularity;
+ if (granularity.width == 0 || granularity.height == 0) {
+ // Seen with AMD's driver: the sparse block size comes back as 0, and the size computation
+ // below divided by it. Use the standard 2D block shape when the device promises it.
+ if (!StandardSparseBlockShape(device, block_bytes, granularity)) {
+ LOG_WARNING(Render_Vulkan,
+ "Sparse layered image {:#x}: the driver reports no sparse block size and "
+ "no standard block shape; not using sparse layers",
+ gpu_addr);
+ return false;
+ }
+ granularity.width *= block_width;
+ granularity.height *= block_height;
+ LOG_INFO(Render_Vulkan,
+ "Sparse layered image {:#x}: no block size from the driver, using the standard "
+ "{}x{} texel block",
+ gpu_addr, granularity.width, granularity.height);
+ }
s.tile_width = granularity.width;
s.tile_height = granularity.height;
const u64 tile = s.requirements.alignment;
@@ -2629,6 +2944,381 @@ bool Image::InitSparseLayers() {
return true;
}
+bool Image::InitSparseTiles() {
+ const Device& device = runtime->device;
+ const VkImage handle = *original_image;
+ const auto requirements = device.GetLogical().GetImageSparseMemoryRequirements(handle);
+ const auto it = std::ranges::find_if(requirements, [](const auto& r) {
+ return (r.formatProperties.aspectMask & VK_IMAGE_ASPECT_COLOR_BIT) != 0;
+ });
+ if (it == requirements.end()) {
+ return false;
+ }
+ SparseLayerMemory& s = sparse_layers;
+ s.requirements = device.GetLogical().GetImageMemoryRequirements(handle);
+ const VkExtent3D granularity = it->formatProperties.imageGranularity;
+ const u32 bytes = VideoCore::Surface::BytesPerBlock(info.format);
+ // Rectangles are uploaded by whole GOB columns (64 bytes across).
+ if (granularity.width == 0 || granularity.height == 0 || (granularity.width * bytes) % 64 != 0 ||
+ u64{granularity.width} * granularity.height * bytes != s.requirements.alignment) {
+ return false;
+ }
+ s.allocator = &runtime->memory_allocator;
+ s.sparse = *it;
+ s.width = info.size.width;
+ s.height = info.size.height;
+ s.levels = static_cast<u32>(info.resources.levels);
+ s.tile_width = granularity.width;
+ s.tile_height = granularity.height;
+ s.tail_level = (std::min)(it->imageMipTailFirstLod, s.levels);
+ s.level_tiles.resize(s.tail_level);
+ for (u32 level = 0; level < s.tail_level; ++level) {
+ const u32 width = (std::max)(s.width >> level, 1U);
+ const u32 height = (std::max)(s.height >> level, 1U);
+ s.level_tiles[level].assign(
+ size_t{Common::DivCeil(width, s.tile_width)} * Common::DivCeil(height, s.tile_height),
+ VK_NULL_HANDLE);
+ }
+ if (SparseTilesModeOf(device) == SparseTilesMode::Aliased && !BindZeroBlock()) {
+ LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: no zeroed block to share", gpu_addr);
+ s.Release();
+ s.allocator = nullptr;
+ s.bound_bytes = 0;
+ return false;
+ }
+ LOG_INFO(Render_Vulkan, "Sparse tiled image {:#x}: {}x{} {} levels, block {}x{}, mip tail from {}{}",
+ gpu_addr, s.width, s.height, s.levels, s.tile_width, s.tile_height, s.tail_level,
+ s.zero_block != VK_NULL_HANDLE ? ", empty blocks share a zeroed one" : "");
+ return true;
+}
+
+bool Image::BindZeroBlock() {
+ SparseLayerMemory& s = sparse_layers;
+ const Device& device = runtime->device;
+ const u64 tile = s.requirements.alignment;
+ VkDeviceMemory memory{};
+ VkDeviceSize offset{};
+ s.zero_block = s.allocator->AllocateSparseMemory(
+ VkMemoryRequirements{
+ .size = tile,
+ .alignment = tile,
+ .memoryTypeBits = s.requirements.memoryTypeBits,
+ },
+ memory, offset);
+ if (s.zero_block == VK_NULL_HANDLE) {
+ return false;
+ }
+ s.bound_bytes += tile;
+ std::vector<VkSparseImageMemoryBind> image_binds;
+ for (u32 level = 0; level < s.tail_level; ++level) {
+ const u32 width = (std::max)(s.width >> level, 1U);
+ const u32 height = (std::max)(s.height >> level, 1U);
+ for (u32 y = 0; y < height; y += s.tile_height) {
+ for (u32 x = 0; x < width; x += s.tile_width) {
+ image_binds.push_back(VkSparseImageMemoryBind{
+ .subresource{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .mipLevel = level,
+ .arrayLayer = 0,
+ },
+ .offset{static_cast<s32>(x), static_cast<s32>(y), 0},
+ .extent{(std::min)(s.tile_width, width - x), (std::min)(s.tile_height, height - y),
+ 1},
+ .memory = memory,
+ .memoryOffset = offset,
+ .flags = 0,
+ });
+ }
+ }
+ }
+ // The mip tail gets memory of its own: it is small, and the clear below zeroes it too.
+ std::vector<VkSparseMemoryBind> opaque_binds;
+ if (s.tail_level < s.levels) {
+ const VkMemoryRequirements requirements{
+ .size = Common::AlignUp(s.sparse.imageMipTailSize, tile),
+ .alignment = tile,
+ .memoryTypeBits = s.requirements.memoryTypeBits,
+ };
+ VkDeviceMemory tail_memory{};
+ VkDeviceSize tail_offset{};
+ s.tail_memory = s.allocator->AllocateSparseMemory(requirements, tail_memory, tail_offset);
+ if (s.tail_memory == VK_NULL_HANDLE) {
+ return false;
+ }
+ s.bound_bytes += requirements.size;
+ opaque_binds.push_back(VkSparseMemoryBind{
+ .resourceOffset = s.sparse.imageMipTailOffset,
+ .size = s.sparse.imageMipTailSize,
+ .memory = tail_memory,
+ .memoryOffset = tail_offset,
+ .flags = 0,
+ });
+ }
+ const VkSparseImageMemoryBindInfo image_bind_info{
+ .image = *original_image,
+ .bindCount = static_cast<u32>(image_binds.size()),
+ .pBinds = image_binds.data(),
+ };
+ const VkSparseImageOpaqueMemoryBindInfo opaque_bind_info{
+ .image = *original_image,
+ .bindCount = static_cast<u32>(opaque_binds.size()),
+ .pBinds = opaque_binds.data(),
+ };
+ const VkBindSparseInfo bind_info{
+ .sType = VK_STRUCTURE_TYPE_BIND_SPARSE_INFO,
+ .pNext = nullptr,
+ .waitSemaphoreCount = 0,
+ .pWaitSemaphores = nullptr,
+ .bufferBindCount = 0,
+ .pBufferBinds = nullptr,
+ .imageOpaqueBindCount = opaque_binds.empty() ? 0U : 1U,
+ .pImageOpaqueBinds = opaque_binds.empty() ? nullptr : &opaque_bind_info,
+ .imageBindCount = image_binds.empty() ? 0U : 1U,
+ .pImageBinds = image_binds.empty() ? nullptr : &image_bind_info,
+ .signalSemaphoreCount = 0,
+ .pSignalSemaphores = nullptr,
+ };
+ vk::Fence fence = device.GetLogical().CreateFence(VkFenceCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ });
+ VkResult result;
+ {
+ std::scoped_lock lock{scheduler->submit_mutex};
+ result = device.GetGraphicsQueue().BindSparse(bind_info, *fence);
+ }
+ if (result != VK_SUCCESS) {
+ LOG_ERROR(Render_Vulkan, "Sparse tiled image {:#x}: binding the zeroed block failed ({})",
+ gpu_addr, static_cast<int>(result));
+ return false;
+ }
+ fence.Wait();
+ // Every block now reads the shared block (and the tail its own memory): zero them all.
+ ClearTiles();
+ return true;
+}
+
+void Image::ClearTiles() {
+ scheduler->RequestOutsideRenderPassOperationContext();
+ const VkImage vk_image = *original_image;
+ const bool was_initialized = std::exchange(initialized, true);
+ scheduler->Record([vk_image, was_initialized](vk::CommandBuffer cmdbuf) {
+ static constexpr VkImageSubresourceRange range{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .baseMipLevel = 0,
+ .levelCount = VK_REMAINING_MIP_LEVELS,
+ .baseArrayLayer = 0,
+ .layerCount = VK_REMAINING_ARRAY_LAYERS,
+ };
+ const VkImageMemoryBarrier to_transfer{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+ .pNext = nullptr,
+ .srcAccessMask = VK_ACCESS_SHADER_WRITE_BIT | VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT |
+ VK_ACCESS_TRANSFER_WRITE_BIT,
+ .dstAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
+ .oldLayout = was_initialized ? VK_IMAGE_LAYOUT_GENERAL : VK_IMAGE_LAYOUT_UNDEFINED,
+ .newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = vk_image,
+ .subresourceRange = range,
+ };
+ const VkImageMemoryBarrier to_general{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER,
+ .pNext = nullptr,
+ .srcAccessMask = VK_ACCESS_TRANSFER_WRITE_BIT,
+ .dstAccessMask = VK_ACCESS_SHADER_READ_BIT | VK_ACCESS_SHADER_WRITE_BIT |
+ VK_ACCESS_COLOR_ATTACHMENT_READ_BIT |
+ VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT |
+ VK_ACCESS_TRANSFER_WRITE_BIT,
+ .oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL,
+ .newLayout = VK_IMAGE_LAYOUT_GENERAL,
+ .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED,
+ .image = vk_image,
+ .subresourceRange = range,
+ };
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0,
+ to_transfer);
+ cmdbuf.ClearColorImage(vk_image, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VkClearColorValue{},
+ range);
+ cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0,
+ to_general);
+ });
+}
+
+bool Image::BackTilesWhole() {
+ SparseLayerMemory& s = sparse_layers;
+ if (s.zero_block == VK_NULL_HANDLE) {
+ return true;
+ }
+ std::vector<VideoCommon::LevelRect> rects;
+ for (u32 level = 0; level < s.levels; ++level) {
+ rects.push_back({static_cast<s32>(level), 0, 0, (std::max)(s.width >> level, 1U),
+ (std::max)(s.height >> level, 1U)});
+ }
+ if (!BindTiles(rects)) {
+ return false;
+ }
+ // No block reads the shared one any more.
+ s.allocator->FreeSparseMemory(s.zero_block);
+ s.zero_block = VK_NULL_HANDLE;
+ s.bound_bytes -= s.requirements.alignment;
+ ClearTiles();
+ return true;
+}
+
+bool Image::BindTiles(std::span<const VideoCommon::LevelRect> rects) {
+ SparseLayerMemory& s = sparse_layers;
+ if (s.allocator == nullptr) {
+ return false;
+ }
+ const Device& device = runtime->device;
+ const u64 tile = s.requirements.alignment;
+ const VkMemoryRequirements tile_requirements{
+ .size = tile,
+ .alignment = tile,
+ .memoryTypeBits = s.requirements.memoryTypeBits,
+ };
+ std::vector<VkSparseImageMemoryBind> image_binds;
+ std::vector<VkSparseMemoryBind> opaque_binds;
+ std::vector<VmaAllocation*> fresh; // Blocks given memory by this call
+ bool fresh_tail = false;
+ // A failed bind leaves the blocks as they were, so the next call tries them again.
+ const auto roll_back = [&] {
+ for (VmaAllocation* const allocation : fresh) {
+ s.allocator->FreeSparseMemory(*allocation);
+ *allocation = VK_NULL_HANDLE;
+ s.bound_bytes -= tile;
+ }
+ if (fresh_tail) {
+ s.allocator->FreeSparseMemory(s.tail_memory);
+ s.tail_memory = VK_NULL_HANDLE;
+ s.bound_bytes -= Common::AlignUp(s.sparse.imageMipTailSize, tile);
+ }
+ };
+ bool needs_tail = false;
+ for (const VideoCommon::LevelRect& rect : rects) {
+ const u32 level = static_cast<u32>(rect.level);
+ if (level >= s.tail_level) {
+ needs_tail = true;
+ continue;
+ }
+ const u32 width = (std::max)(s.width >> level, 1U);
+ const u32 height = (std::max)(s.height >> level, 1U);
+ const u32 tiles_x = Common::DivCeil(width, s.tile_width);
+ for (u32 ty = rect.y / s.tile_height; ty <= (rect.y + rect.height - 1) / s.tile_height;
+ ++ty) {
+ for (u32 tx = rect.x / s.tile_width; tx <= (rect.x + rect.width - 1) / s.tile_width;
+ ++tx) {
+ VmaAllocation& allocation = s.level_tiles[level][size_t{ty} * tiles_x + tx];
+ if (allocation != VK_NULL_HANDLE) {
+ continue;
+ }
+ VkDeviceMemory memory{};
+ VkDeviceSize offset{};
+ allocation = s.allocator->AllocateSparseMemory(tile_requirements, memory, offset);
+ if (allocation == VK_NULL_HANDLE) {
+ LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: out of device memory",
+ gpu_addr);
+ roll_back();
+ return false;
+ }
+ fresh.push_back(&allocation);
+ s.bound_bytes += tile;
+ const u32 x = tx * s.tile_width;
+ const u32 y = ty * s.tile_height;
+ image_binds.push_back(VkSparseImageMemoryBind{
+ .subresource{
+ .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT,
+ .mipLevel = level,
+ .arrayLayer = 0,
+ },
+ .offset{static_cast<s32>(x), static_cast<s32>(y), 0},
+ .extent{(std::min)(s.tile_width, width - x), (std::min)(s.tile_height, height - y),
+ 1},
+ .memory = memory,
+ .memoryOffset = offset,
+ .flags = 0,
+ });
+ }
+ }
+ }
+ if (needs_tail && s.tail_level < s.levels && s.tail_memory == VK_NULL_HANDLE) {
+ const VkMemoryRequirements requirements{
+ .size = Common::AlignUp(s.sparse.imageMipTailSize, tile),
+ .alignment = tile,
+ .memoryTypeBits = s.requirements.memoryTypeBits,
+ };
+ VkDeviceMemory memory{};
+ VkDeviceSize offset{};
+ s.tail_memory = s.allocator->AllocateSparseMemory(requirements, memory, offset);
+ if (s.tail_memory == VK_NULL_HANDLE) {
+ LOG_WARNING(Render_Vulkan, "Sparse tiled image {:#x}: out of device memory for the mip "
+ "tail",
+ gpu_addr);
+ roll_back();
+ return false;
+ }
+ fresh_tail = true;
+ s.bound_bytes += requirements.size;
+ opaque_binds.push_back(VkSparseMemoryBind{
+ .resourceOffset = s.sparse.imageMipTailOffset,
+ .size = s.sparse.imageMipTailSize,
+ .memory = memory,
+ .memoryOffset = offset,
+ .flags = 0,
+ });
+ }
+ if (image_binds.empty() && opaque_binds.empty()) {
+ return true;
+ }
+ const VkSparseImageMemoryBindInfo image_bind_info{
+ .image = *original_image,
+ .bindCount = static_cast<u32>(image_binds.size()),
+ .pBinds = image_binds.data(),
+ };
+ const VkSparseImageOpaqueMemoryBindInfo opaque_bind_info{
+ .image = *original_image,
+ .bindCount = static_cast<u32>(opaque_binds.size()),
+ .pBinds = opaque_binds.data(),
+ };
+ const VkBindSparseInfo bind_info{
+ .sType = VK_STRUCTURE_TYPE_BIND_SPARSE_INFO,
+ .pNext = nullptr,
+ .waitSemaphoreCount = 0,
+ .pWaitSemaphores = nullptr,
+ .bufferBindCount = 0,
+ .pBufferBinds = nullptr,
+ .imageOpaqueBindCount = opaque_binds.empty() ? 0U : 1U,
+ .pImageOpaqueBinds = opaque_binds.empty() ? nullptr : &opaque_bind_info,
+ .imageBindCount = image_binds.empty() ? 0U : 1U,
+ .pImageBinds = image_binds.empty() ? nullptr : &image_bind_info,
+ .signalSemaphoreCount = 0,
+ .pSignalSemaphores = nullptr,
+ };
+ // Bound before the command buffer that uploads the blocks is submitted (as BindLayers).
+ vk::Fence fence = device.GetLogical().CreateFence(VkFenceCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_FENCE_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ });
+ VkResult result;
+ {
+ std::scoped_lock lock{scheduler->submit_mutex};
+ result = device.GetGraphicsQueue().BindSparse(bind_info, *fence);
+ }
+ if (result != VK_SUCCESS) {
+ LOG_ERROR(Render_Vulkan, "Sparse tiled image {:#x}: binding failed ({})", gpu_addr,
+ static_cast<int>(result));
+ roll_back();
+ return false;
+ }
+ fence.Wait();
+ return true;
+}
+
std::vector<u32> Image::BindLayers(std::span<const u32> layers) {
SparseLayerMemory& s = sparse_layers;
std::vector<u32> bound;
@@ -3660,30 +4350,47 @@ void TextureCacheRuntime::TranscodeAstcToBcn(
const u32 levels = static_cast<u32>(info.resources.levels);
const u32 layers = static_cast<u32>(info.resources.layers);
constexpr VkFormat DecodeFormat = VK_FORMAT_A8B8G8R8_UNORM_PACK32;
- PendingTranscode pending{
- .tick = 0,
- .image = memory_allocator.CreateImage(VkImageCreateInfo{
- .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
- .pNext = nullptr,
- .flags = 0,
- .imageType = VK_IMAGE_TYPE_2D,
- .format = DecodeFormat,
- .extent = {width, height, 1},
- .mipLevels = levels,
- .arrayLayers = layers,
- .samples = VK_SAMPLE_COUNT_1_BIT,
- .tiling = VK_IMAGE_TILING_OPTIMAL,
- .usage = VK_IMAGE_USAGE_STORAGE_BIT,
- .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
- .queueFamilyIndexCount = 0,
- .pQueueFamilyIndices = nullptr,
- .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
- }, true),
- .views = {},
- };
- for (u32 level = 0; level < levels; ++level) {
- pending.views.push_back(
- MakeStorageView(device.GetLogical(), level, *pending.image, DecodeFormat));
+ // A decode image of the same shape the GPU is done with: title 1's title screen decodes
+ // dozens of textures a frame, mostly of a few shapes. Making one took 263 us; from VMA's
+ // shared blocks 90 us, but freeing them there left ~1 GB of those blocks unused.
+ const auto reusable = std::ranges::find_if(free_transcodes, [&](const PendingTranscode& t) {
+ return t.width == width && t.height == height && t.levels == levels && t.layers == layers;
+ });
+ PendingTranscode pending;
+ if (reusable != free_transcodes.end()) {
+ pending = std::move(*reusable);
+ free_transcodes.erase(reusable);
+ } else {
+ pending = PendingTranscode{
+ .tick = 0,
+ .image = memory_allocator.CreateImage(VkImageCreateInfo{
+ .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO,
+ .pNext = nullptr,
+ .flags = 0,
+ .imageType = VK_IMAGE_TYPE_2D,
+ .format = DecodeFormat,
+ .extent = {width, height, 1},
+ .mipLevels = levels,
+ .arrayLayers = layers,
+ .samples = VK_SAMPLE_COUNT_1_BIT,
+ .tiling = VK_IMAGE_TILING_OPTIMAL,
+ .usage = VK_IMAGE_USAGE_STORAGE_BIT,
+ .sharingMode = VK_SHARING_MODE_EXCLUSIVE,
+ .queueFamilyIndexCount = 0,
+ .pQueueFamilyIndices = nullptr,
+ .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED,
+ }, true),
+ .views = {},
+ .width = width,
+ .height = height,
+ .levels = levels,
+ .layers = layers,
+ .unused_frames = 0,
+ };
+ for (u32 level = 0; level < levels; ++level) {
+ pending.views.push_back(
+ MakeStorageView(device.GetLogical(), level, *pending.image, DecodeFormat));
+ }
}
astc_decoder_pass->AssembleInto(
info, 0, image.guest_size_bytes,
@@ -3692,7 +4399,7 @@ void TextureCacheRuntime::TranscodeAstcToBcn(
.aspect_mask = VK_IMAGE_ASPECT_COLOR_BIT,
.initialized = false,
.level_view = [&pending](s32 level) { return *pending.views[level]; },
- .wait = false,
+ .wait = true,
},
map, swizzles);
diff --git a/src/video_core/renderer_vulkan/vk_texture_cache.h b/src/video_core/renderer_vulkan/vk_texture_cache.h
index 3944e183fd..564b2cbf9e 100644
--- a/src/video_core/renderer_vulkan/vk_texture_cache.h
+++ b/src/video_core/renderer_vulkan/vk_texture_cache.h
@@ -67,10 +67,22 @@ public:
u64 GetDeviceMemoryUsage() const;
+ /// GPU progress: the texture cache frees a deleted image once the GPU is past its last use.
+ [[nodiscard]] u64 CurrentTick() const noexcept;
+
+ [[nodiscard]] bool IsFree(u64 tick) const noexcept;
+
u64 GetDeviceMemoryBudget() const;
+ u64 GetDeviceMemorySlack() const;
+
bool CanReportMemoryUsage() const;
+ /// Images that did not fit the device memory budget so far (see MemoryAllocator).
+ u32 MemoryPressureEvents() const noexcept {
+ return memory_allocator.MemoryPressureEvents();
+ }
+
void LogMemoryStatistics() const;
/// True when ASTC is re-compressed to BC3 and opaque images may use BC1 instead.
@@ -87,6 +99,12 @@ public:
Tegra::Engines::Fermi2D::Filter filter,
Tegra::Engines::Fermi2D::Operation operation);
+ /// Stands in for a DLSS evaluation: temporal upscale of input_color into dst, accumulated with
+ /// a copy of dst's previous contents (the last output). False when dst cannot take it.
+ bool TemporalUpscale(Framebuffer* dst_framebuffer, ImageView& dst, ImageView& input_color,
+ ImageView& motion_vectors, std::array<f32, 2> jitter,
+ std::array<f32, 2> motion_scale);
+
void CopyImage(Image& dst, Image& src, std::span<const VideoCommon::ImageCopy> copies);
void CopyImageMSAA(Image& dst, Image& src, std::span<const VideoCommon::ImageCopy> copies);
@@ -169,6 +187,15 @@ public:
StagingBufferPool& staging_buffer_pool;
BlitImageHelper& blit_image_helper;
RenderPassCache& render_pass_cache;
+
+ /// TemporalUpscale's history: its previous result, copied right after drawing it.
+ struct TemporalHistory {
+ vk::Image image;
+ vk::ImageView view;
+ VkFormat format{};
+ VkExtent2D extent{};
+ } temporal_history;
+
std::optional<ASTCDecoderPass> astc_decoder_pass;
std::optional<BcEncoderPass> bc_encoder_pass;
@@ -177,13 +204,20 @@ public:
void TranscodeAstcToBcn(Image& image, const StagingBufferRef& map,
std::span<const VideoCommon::SwizzleParameters> swizzles);
- /// Temporary decode images, freed once the GPU is past `tick`.
+ /// Temporary decode images, reusable once the GPU is past `tick`.
struct PendingTranscode {
u64 tick;
vk::Image image;
std::vector<vk::ImageView> views;
+ u32 width;
+ u32 height;
+ u32 levels;
+ u32 layers;
+ u32 unused_frames;
};
std::deque<PendingTranscode> pending_transcodes;
+ /// Decode images the GPU is done with, for the next texture of the same shape.
+ std::vector<PendingTranscode> free_transcodes;
std::optional<BlockLinearUnswizzle3DPass> bl3d_unswizzle_pass;
const Settings::ResolutionScalingInfo& resolution;
@@ -375,6 +409,11 @@ public:
return (this->*current_image).UsageFlags();
}
+ /// Where the image's memory lies, for the collector (TextureCache::RunGarbageCollector).
+ [[nodiscard]] vk::Image::MemoryBlock MemoryBlock() const noexcept {
+ return original_image.GetMemoryBlock();
+ }
+
/// Returns true when the image is already initialized and mark it as initialized
[[nodiscard]] bool ExchangeInitialization() noexcept {
return std::exchange(initialized, true);
@@ -389,6 +428,30 @@ public:
/// layers that have memory afterwards (a cap per image can leave some out).
std::vector<u32> BindLayers(std::span<const u32> layers);
+ /// SparseTiles images: gives the host sparse blocks under `rects` (and the mip tail, for
+ /// levels in it) device memory where they have none yet. False if that failed.
+ bool BindTiles(std::span<const VideoCommon::LevelRect> rects);
+
+ /// SparseTiles images: the host sparse block, in texels, and the first level of the mip tail.
+ u32 SparseTileWidth() const noexcept {
+ return sparse_layers.tile_width;
+ }
+ u32 SparseTileHeight() const noexcept {
+ return sparse_layers.tile_height;
+ }
+ s32 SparseTailLevel() const noexcept {
+ return static_cast<s32>(sparse_layers.tail_level);
+ }
+
+ /// SparseTiles images whose blocks without memory of their own share one zeroed block.
+ bool SparseTilesShareZeroBlock() const noexcept {
+ return sparse_layers.zero_block != VK_NULL_HANDLE;
+ }
+
+ /// SparseTiles images sharing a zeroed block, before the GPU renders into them: every block
+ /// gets memory of its own, all cleared (the caller loads the data again). False on failure.
+ bool BackTilesWhole();
+
/// Device memory BindLayers has bound to the image's layers (an unbacked image has no other).
u64 SparseBoundBytes() const noexcept {
return sparse_layers.bound_bytes;
@@ -434,6 +497,10 @@ private:
layer_bytes = other.layer_bytes;
layer_memory = std::move(other.layer_memory);
other.layer_memory.clear();
+ level_tiles = std::move(other.level_tiles);
+ other.level_tiles.clear();
+ tail_level = other.tail_level;
+ zero_block = std::exchange(other.zero_block, VK_NULL_HANDLE);
tail_memory = std::exchange(other.tail_memory, VK_NULL_HANDLE);
bound_bytes = other.bound_bytes;
warned = other.warned;
@@ -449,8 +516,16 @@ private:
for (const VmaAllocation allocation : layer_memory) {
allocator->FreeSparseMemory(allocation);
}
+ for (const auto& tiles : level_tiles) {
+ for (const VmaAllocation allocation : tiles) {
+ allocator->FreeSparseMemory(allocation);
+ }
+ }
allocator->FreeSparseMemory(tail_memory);
+ allocator->FreeSparseMemory(zero_block);
+ zero_block = VK_NULL_HANDLE;
layer_memory.clear();
+ level_tiles.clear();
tail_memory = VK_NULL_HANDLE;
}
@@ -464,6 +539,12 @@ private:
u32 tile_height{};
VkDeviceSize layer_bytes{}; ///< One layer's levels before the mip tail, and its own tail
std::vector<VmaAllocation> layer_memory; ///< Per layer, null until bound
+ /// SparseTiles: per level before the mip tail, per sparse block (rows), null until bound
+ std::vector<std::vector<VmaAllocation>> level_tiles;
+ u32 tail_level{}; ///< SparseTiles: first level of the mip tail
+ /// SparseTiles: the zeroed block every block without memory of its own is bound to, where
+ /// the driver can't sample unbound memory
+ VmaAllocation zero_block{};
VmaAllocation tail_memory{}; ///< The mip tail all layers share, if one
VkDeviceSize bound_bytes{};
bool warned{};
@@ -473,6 +554,15 @@ private:
/// Reads the image's sparse requirements; false when it can't be bound by layer.
bool InitSparseLayers();
+ /// Reads the image's sparse requirements; false when it can't be bound by sparse block.
+ bool InitSparseTiles();
+
+ /// Binds one zeroed block to every block of a SparseTiles image (and memory to its mip tail).
+ bool BindZeroBlock();
+
+ /// Records a clear of the whole image to zero.
+ void ClearTiles();
+
vk::Image original_image;
vk::Image scaled_image;
diff --git a/src/video_core/renderer_vulkan/vk_update_descriptor.cpp b/src/video_core/renderer_vulkan/vk_update_descriptor.cpp
index 742a8d9969..29f3ee71a1 100644
--- a/src/video_core/renderer_vulkan/vk_update_descriptor.cpp
+++ b/src/video_core/renderer_vulkan/vk_update_descriptor.cpp
@@ -20,10 +20,10 @@ UpdateDescriptorQueue::UpdateDescriptorQueue(const Device& device_, size_t frame
bool supports_descriptor_buffer_)
: device{device_}, frame_payload_size{frame_payload_size_},
supports_descriptor_buffer{supports_descriptor_buffer_},
- payload(frame_payload_size_ * FRAMES_IN_FLIGHT)
+ payload(std::make_unique_for_overwrite<DescriptorUpdateEntry[]>(frame_payload_size_ * FRAMES_IN_FLIGHT))
{
- payload_start = payload.data();
- payload_cursor = payload.data();
+ payload_start = payload.get();
+ payload_cursor = payload.get();
}
UpdateDescriptorQueue::~UpdateDescriptorQueue() = default;
@@ -32,7 +32,7 @@ void UpdateDescriptorQueue::TickFrame() {
if (++frame_index >= FRAMES_IN_FLIGHT) {
frame_index = 0;
}
- payload_start = payload.data() + frame_index * frame_payload_size;
+ payload_start = payload.get() + frame_index * frame_payload_size;
payload_cursor = payload_start;
}
diff --git a/src/video_core/renderer_vulkan/vk_update_descriptor.h b/src/video_core/renderer_vulkan/vk_update_descriptor.h
index 9f30e65dbd..4d393376b4 100644
--- a/src/video_core/renderer_vulkan/vk_update_descriptor.h
+++ b/src/video_core/renderer_vulkan/vk_update_descriptor.h
@@ -6,6 +6,7 @@
#pragma once
+#include <memory>
#include <variant>
#include <vector>
#include "video_core/vulkan_common/vulkan_wrapper.h"
@@ -122,7 +123,7 @@ private:
DescriptorUpdateEntry* payload_cursor = nullptr;
DescriptorUpdateEntry* payload_start = nullptr;
const DescriptorUpdateEntry* upload_start = nullptr;
- std::vector<DescriptorUpdateEntry> payload;
+ std::unique_ptr<DescriptorUpdateEntry[]> payload;
};
// TODO: should these be separate classes instead?
diff --git a/src/video_core/shader_cache.cpp b/src/video_core/shader_cache.cpp
index dfe80abac3..5d41dc9697 100644
--- a/src/video_core/shader_cache.cpp
+++ b/src/video_core/shader_cache.cpp
@@ -6,6 +6,7 @@
#include <algorithm>
#include <cstring>
+#include <functional>
#include <string>
#include <unordered_set>
#include <fmt/ranges.h>
@@ -14,6 +15,7 @@
#include <boost/container/small_vector.hpp>
#include "common/assert.h"
+#include "common/cityhash.h"
#include "shader_recompiler/frontend/maxwell/control_flow.h"
#include "shader_recompiler/exception.h"
#include "shader_recompiler/object_pool.h"
@@ -35,6 +37,17 @@ void ShaderCache::InvalidateRegion(VAddr addr, size_t size) {
RemovePendingShaders();
}
+void ShaderCache::InvalidateRegions(std::span<const std::pair<DAddr, size_t>> sequences) {
+ if (sequences.empty()) {
+ return;
+ }
+ std::scoped_lock lock{invalidation_mutex};
+ for (const auto& [addr, size] : sequences) {
+ InvalidatePagesInRegion(addr, size);
+ }
+ RemovePendingShaders();
+}
+
void ShaderCache::OnCacheInvalidation(VAddr addr, size_t size) {
std::scoped_lock lock{invalidation_mutex};
InvalidatePagesInRegion(addr, size);
@@ -48,6 +61,22 @@ void ShaderCache::SyncGuestHost() {
ShaderCache::ShaderCache(Tegra::MaxwellDeviceMemoryManager& device_memory_)
: device_memory{device_memory_} {}
+// Task and mesh programs are translated with the mesh registers and the header of the pass-through
+// geometry program (per-primitive outputs), so pipelines are keyed by them too.
+u64 ShaderCache::MeshStateHash() const {
+ using Regs = Tegra::Engines::Maxwell3D::Regs;
+ const auto& regs{maxwell3d->regs};
+ std::array<u32, 3 + 20> state{regs.reg_array[Regs::MeshShaderA], regs.reg_array[Regs::MeshShaderB],
+ regs.reg_array[Regs::MeshInitShader]};
+ if (regs.IsShaderConfigEnabled(Regs::ShaderType::Geometry)) {
+ const auto& geometry{regs.pipelines[static_cast<size_t>(Regs::ShaderType::Geometry)]};
+ const GPUVAddr address{(static_cast<u64>(geometry.reserved[0] & 0xff) << 32) |
+ geometry.reserved[1]};
+ gpu_memory->ReadBlockUnsafe(address, state.data() + 3, 20 * sizeof(u32));
+ }
+ return Common::CityHash64(reinterpret_cast<const char*>(state.data()), sizeof(state)) | 1;
+}
+
bool ShaderCache::RefreshStages(std::array<u64, 6>& unique_hashes) try {
auto& dirty{maxwell3d->dirty.flags};
if (!dirty[VideoCommon::Dirty::Shaders]) {
@@ -57,6 +86,7 @@ bool ShaderCache::RefreshStages(std::array<u64, 6>& unique_hashes) try {
const GPUVAddr base_addr{maxwell3d->regs.program_region.Address()};
const bool native = maxwell3d->GetNativeMacroEngine().native_turing;
+ const bool mesh{native && maxwell3d->regs.IsMeshEnabled()};
for (size_t index = 0; index < Tegra::Engines::Maxwell3D::Regs::MaxShaderProgram; ++index) {
if (!maxwell3d->regs.IsShaderConfigEnabled(index)) {
unique_hashes[index] = 0;
@@ -69,6 +99,11 @@ bool ShaderCache::RefreshStages(std::array<u64, 6>& unique_hashes) try {
unique_hashes[index] = 0;
continue;
}
+ if (mesh && program == Tegra::Engines::Maxwell3D::Regs::ShaderType::Geometry) {
+ // A header without code: the mesh program's environment reads its output map.
+ unique_hashes[index] = 0;
+ continue;
+ }
const GPUVAddr shader_addr{native
? (static_cast<u64>(shader_config.reserved[0] & 0xff) << 32) | shader_config.reserved[1]
: base_addr + shader_config.offset};
@@ -89,7 +124,8 @@ bool ShaderCache::RefreshStages(std::array<u64, 6>& unique_hashes) try {
if (!shader_info) {
const u32 start_address{native ? 0 : shader_config.offset};
try {
- // Native programs are lowered while the environment is built.
+ // Native programs are lowered while the environment is built (once: the
+ // ShaderInfo keeps the result for the pipeline's environments).
RethrowIfRejected(*cpu_shader_addr);
GraphicsEnvironment env{*maxwell3d, *gpu_memory, program, native ? shader_addr : base_addr, start_address};
shader_info = MakeShaderInfo(env, *cpu_shader_addr);
@@ -111,6 +147,10 @@ bool ShaderCache::RefreshStages(std::array<u64, 6>& unique_hashes) try {
}
shader_infos[index] = shader_info;
unique_hashes[index] = shader_info->unique_hash;
+ if (mesh && (program == Tegra::Engines::Maxwell3D::Regs::ShaderType::VertexB ||
+ program == Tegra::Engines::Maxwell3D::Regs::ShaderType::Tessellation)) {
+ unique_hashes[index] ^= MeshStateHash();
+ }
}
last_shaders_valid = true;
return true;
@@ -202,7 +242,8 @@ void ShaderCache::GetGraphicsEnvironments(GraphicsEnvironments& result,
const GPUVAddr address = native
? (static_cast<u64>(config.reserved[0] & 0xff) << 32) | config.reserved[1]
: base_addr;
- env = GraphicsEnvironment{*maxwell3d, *gpu_memory, program, address, start_address};
+ env = GraphicsEnvironment{*maxwell3d, *gpu_memory, program, address, start_address,
+ shader_infos[index]->lowered_native};
env.SetCachedSize(shader_infos[index]->size_bytes);
result.env_ptrs[env_index++] = &env;
}
@@ -275,10 +316,17 @@ void ShaderCache::RemovePendingShaders() {
marked_for_removal.clear();
if (!removed_shaders.empty()) {
- // Remove the given shaders from the cache
- std::erase_if(storage, [&removed_shaders](const std::unique_ptr<ShaderInfo>& shader) {
- return std::ranges::find(removed_shaders, shader.get()) != removed_shaders.end();
- });
+ if (removed_shaders.size() > 16) {
+ std::ranges::sort(removed_shaders, std::less<ShaderInfo*>{});
+ std::erase_if(storage, [&removed_shaders](const std::unique_ptr<ShaderInfo>& shader) {
+ return std::binary_search(removed_shaders.begin(), removed_shaders.end(),
+ shader.get(), std::less<ShaderInfo*>{});
+ });
+ } else {
+ std::erase_if(storage, [&removed_shaders](const std::unique_ptr<ShaderInfo>& shader) {
+ return std::ranges::find(removed_shaders, shader.get()) != removed_shaders.end();
+ });
+ }
}
}
@@ -342,6 +390,7 @@ const ShaderInfo* ShaderCache::MakeShaderInfo(GenericEnvironment& env, VAddr cpu
info->unique_hash = env.CalculateHash();
info->size_bytes = env.ReadSizeBytes();
}
+ info->lowered_native = env.LoweredNative();
const size_t size_bytes{env.SourceSizeBytes() ? env.SourceSizeBytes() : info->size_bytes};
const ShaderInfo* const result{info.get()};
Register(std::move(info), cpu_addr, size_bytes);
diff --git a/src/video_core/shader_cache.h b/src/video_core/shader_cache.h
index f4179dd435..7c370e217c 100644
--- a/src/video_core/shader_cache.h
+++ b/src/video_core/shader_cache.h
@@ -38,6 +38,8 @@ class GenericEnvironment;
struct ShaderInfo {
u64 unique_hash{};
size_t size_bytes{};
+ /// Lowered code of a native program, reused when its pipelines are built.
+ std::shared_ptr<const LoweredNativeCode> lowered_native;
};
class ShaderCache : public VideoCommon::ChannelSetupCaches<VideoCommon::ChannelInfo> {
@@ -65,6 +67,8 @@ public:
/// @param size Number of bytes of the invalidation
void InvalidateRegion(VAddr addr, size_t size);
+ void InvalidateRegions(std::span<const std::pair<DAddr, size_t>> sequences);
+
/// @brief Unmarks a memory region as cached and marks it for removal
/// @param addr Start address of the CPU write operation
/// @param size Number of bytes of the CPU write operation
@@ -102,6 +106,8 @@ protected:
bool last_shaders_valid = false;
private:
+ friend struct ShaderCacheTestAccess;
+
/// @brief Tries to obtain a cached shader starting in a given address
/// @note Doesn't check for ranges, the given address has to be the start of the shader
/// @param addr Start address of the shader, this doesn't cache for region
@@ -151,6 +157,8 @@ private:
bool IsRejected(VAddr addr);
/// Remembers the exception being handled as this address's rejection.
void RememberRejection(VAddr addr);
+ /// Hash of the state task and mesh programs are translated with.
+ u64 MeshStateHash() const;
Tegra::MaxwellDeviceMemoryManager& device_memory;
diff --git a/src/video_core/shader_environment.cpp b/src/video_core/shader_environment.cpp
index 9d189ad4f0..0c7d8a0d42 100644
--- a/src/video_core/shader_environment.cpp
+++ b/src/video_core/shader_environment.cpp
@@ -5,6 +5,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
#include <algorithm>
+#include <array>
#include <chrono>
#include <cstdlib>
#include <thread>
@@ -315,29 +316,108 @@ std::optional<u64> GenericEnvironment::TryFindSize() {
Tegra::Texture::TICEntry GenericEnvironment::ReadTextureInfo(GPUVAddr tic_addr, u32 tic_limit,
bool via_header_index, u32 raw) {
const auto handle{Tegra::Texture::TexturePair(raw, via_header_index)};
- ASSERT(handle.first <= tic_limit);
- const GPUVAddr descriptor_addr{tic_addr + handle.first * sizeof(Tegra::Texture::TICEntry)};
+ const u32 index{Tegra::Texture::TextureHeaderIndex(handle.first, tic_limit,
+ gpu_memory->UsesNativeTiling())};
+ ASSERT(index <= tic_limit);
+ const GPUVAddr descriptor_addr{tic_addr + index * sizeof(Tegra::Texture::TICEntry)};
Tegra::Texture::TICEntry entry;
gpu_memory->ReadBlock(descriptor_addr, &entry, sizeof(entry));
return entry;
}
-void GenericEnvironment::InitializeNativeCode(u32 source_header_size) {
+namespace {
+
+// Reads a native program a 4 KiB page at a time instead of one 16-byte instruction at a time.
+// Each page is read with ReadBlock, so GPU-written code is flushed first, once per page instead
+// of once per instruction. An instruction that is not inside a fully mapped page is left to the
+// caller.
+class NativeInstructionReader {
+public:
+ using Instruction = Shader::Ampere::Instruction;
+ static constexpr GPUVAddr PageSize = 0x1000;
+
+ explicit NativeInstructionReader(Tegra::MemoryManager& gpu_memory_)
+ : gpu_memory{gpu_memory_} {}
+
+ /// Copies the instruction at `address` into `out`, loading its page first if needed.
+ /// Returns false when the instruction is not inside a fully mapped page.
+ bool TryRead(GPUVAddr address, Instruction& out) {
+ if (!Holds(address)) {
+ LoadFrom(address);
+ }
+ if (!Holds(address)) {
+ return false;
+ }
+ out = page[(address - page_begin) / sizeof(Instruction)];
+ return true;
+ }
+
+private:
+ bool Holds(GPUVAddr address) const {
+ return address >= page_begin && address + sizeof(Instruction) <= page_end;
+ }
+
+ // Loads from `address` up to the next page boundary; loads nothing if that range is not
+ // fully mapped.
+ void LoadFrom(GPUVAddr address) {
+ const GPUVAddr next_page = (address | (PageSize - 1)) + 1;
+ const GPUVAddr length = next_page - address;
+ page_begin = address;
+ page_end = address;
+ if (length >= sizeof(Instruction) && gpu_memory.IsFullyMappedRange(address, length)) {
+ gpu_memory.ReadBlock(address, page.data(), length);
+ page_end = next_page;
+ }
+ }
+
+ Tegra::MemoryManager& gpu_memory;
+ std::array<Instruction, PageSize / sizeof(Instruction)> page;
+ GPUVAddr page_begin{};
+ GPUVAddr page_end{};
+};
+
+} // Anonymous namespace
+
+// A native program is captured and lowered once, when the shader cache first meets it. The
+// environments built later for its pipelines receive that result instead of lowering again.
+void GenericEnvironment::InitializeNativeCode(std::shared_ptr<const LoweredNativeCode> lowered,
+ u32 source_header_size) {
+ if (!lowered) {
+ lowered = LowerNativeCode(source_header_size);
+ }
+ if (lowered->texture_bound != ~0U) {
+ // Native TEX carries its cbuf index explicitly; TEXS obtains it from the environment.
+ texture_bound = lowered->texture_bound;
+ }
+ native_source_size = lowered->source_size;
+ code = lowered->code;
+ cached_lowest = 0;
+ cached_highest = static_cast<u32>((code.size() - 1) * sizeof(u64));
+ lowered_native = std::move(lowered);
+}
+
+std::shared_ptr<const LoweredNativeCode> GenericEnvironment::LowerNativeCode(u32 source_header_size) {
using Shader::Ampere::Instruction;
const u32 native_header_size = source_header_size;
constexpr u32 maximum_instructions = Shader::Ampere::ProgramCaptureState::MaximumInstructions;
std::vector<Instruction> native;
Shader::Ampere::ProgramCaptureState capture;
+ NativeInstructionReader reader{*gpu_memory};
for (u32 index = 0; index < maximum_instructions; ++index) {
const GPUVAddr address = program_base + native_header_size + index * sizeof(Instruction);
- if (!gpu_memory->IsFullyMappedRange(address, sizeof(Instruction))) {
- // The program may end before a branch target past its last EXIT (the capture can't
- // tell); stop at the mapping's end and let the lowering judge what was captured.
- if (!native.empty()) break;
- throw Shader::RuntimeError("Unmapped native shader instruction at {:#x}", address);
+ Instruction instruction;
+ if (!reader.TryRead(address, instruction)) {
+ // Not inside a fully mapped page: check and read this one instruction on its own.
+ if (!gpu_memory->IsFullyMappedRange(address, sizeof(Instruction))) {
+ // The program may end before a branch target past its last EXIT (the capture
+ // can't tell); stop at the mapping's end and let the lowering judge what was
+ // captured.
+ if (!native.empty()) break;
+ throw Shader::RuntimeError("Unmapped native shader instruction at {:#x}", address);
+ }
+ gpu_memory->ReadBlock(address, &instruction, sizeof(instruction));
}
- auto& instruction = native.emplace_back();
- gpu_memory->ReadBlock(address, &instruction, sizeof(instruction));
+ native.push_back(instruction);
const auto status = capture.Observe(instruction, index);
if (status == Shader::Ampere::ProgramCaptureState::Result::InvalidTarget) {
if (const char* dir = std::getenv("EDEN_NX2_SHADER_DUMP")) {
@@ -439,39 +519,46 @@ void GenericEnvironment::InitializeNativeCode(u32 source_header_size) {
program_base, lowered.error_offset, lowered.error, words[0] & 0xfff, words[0],
words[1], words[2], words[3]);
}
- if (lowered.required_texture_buffer != ~0U) {
- // Native TEX carries its cbuf index explicitly; TEXS obtains it from the environment.
- texture_bound = lowered.required_texture_buffer;
- }
- native_source_size = native_header_size + lowered.source_size;
- code.resize(sizeof(sph) / sizeof(u64));
- std::memcpy(code.data(), &sph, sizeof(sph));
+ auto result = std::make_shared<LoweredNativeCode>();
+ result->texture_bound = lowered.required_texture_buffer;
+ result->source_size = native_header_size + lowered.source_size;
+ auto& lowered_code = result->code;
+ lowered_code.resize(sizeof(sph) / sizeof(u64));
+ std::memcpy(lowered_code.data(), &sph, sizeof(sph));
for (const u64 instruction : lowered.instructions) {
- if ((code.size() * sizeof(u64)) % 32 == 0) {
- code.push_back(0); // Maxwell scheduling word, absent from native instructions.
+ if ((lowered_code.size() * sizeof(u64)) % 32 == 0) {
+ lowered_code.push_back(0); // Maxwell scheduling word, absent from native instructions.
}
- code.push_back(instruction);
+ lowered_code.push_back(instruction);
}
// Keep the original padding ABI, then retain the native address map as
// unreachable data. Existing disk-cache serialization carries it with code.
- code.push_back(0);
- Shader::Ampere::AppendNativeAddressMap(code, lowered.source_instruction_offsets);
- cached_lowest = 0;
- cached_highest = static_cast<u32>((code.size() - 1) * sizeof(u64));
+ lowered_code.push_back(0);
+ Shader::Ampere::AppendNativeAddressMap(lowered_code, lowered.source_instruction_offsets);
LOG_DEBUG(Shader, "Lowered native shader at {:#x}: {} source bytes, {} instructions",
- program_base, native_source_size, lowered.instructions.size());
+ program_base, result->source_size, lowered.instructions.size());
+ return result;
}
GraphicsEnvironment::GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_,
Tegra::MemoryManager& gpu_memory_,
Maxwell::ShaderType program, GPUVAddr program_base_,
- u32 start_address_)
+ u32 start_address_,
+ std::shared_ptr<const LoweredNativeCode> lowered_native_)
: GenericEnvironment{gpu_memory_, program_base_, start_address_}, maxwell3d{&maxwell3d_} {
gpu_memory->ReadBlock(program_base + start_address, &sph, sizeof(sph));
initial_offset = sizeof(sph);
texture_bound = maxwell3d->regs.bindless_texture_const_buffer_slot;
- if (maxwell3d->GetNativeMacroEngine().native_turing) {
- if (program != Maxwell::ShaderType::VertexB && program != Maxwell::ShaderType::Pixel) {
+ const auto& regs{maxwell3d->regs};
+ const bool native{maxwell3d->GetNativeMacroEngine().native_turing};
+ // Task and mesh programs: the task in the vertex slot, its mesh program in the tessellation
+ // slot (without a task, the mesh program is in the vertex slot).
+ const bool mesh_slot{native && regs.IsMeshEnabled() &&
+ (program == Maxwell::ShaderType::VertexB ||
+ program == Maxwell::ShaderType::Tessellation)};
+ if (native) {
+ if (program != Maxwell::ShaderType::VertexB && program != Maxwell::ShaderType::Pixel &&
+ !mesh_slot) {
throw Shader::RuntimeError("Unsupported native graphics shader stage {}", static_cast<u32>(program));
}
if (std::getenv("EDEN_NATIVE_DRAW_DIAGNOSTICS")) {
@@ -484,7 +571,7 @@ GraphicsEnvironment::GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_,
dump.write(reinterpret_cast<const char*>(bytes.data()), bytes.size());
}
}
- InitializeNativeCode();
+ InitializeNativeCode(std::move(lowered_native_));
}
gp_passthrough_mask = maxwell3d->regs.post_vtg_shader_attrib_skip_mask;
switch (program) {
@@ -516,6 +603,19 @@ GraphicsEnvironment::GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_,
ASSERT_MSG(false, "Invalid program={}", program);
break;
}
+ if (mesh_slot) {
+ const bool has_task{regs.IsShaderConfigEnabled(Maxwell::ShaderType::Tessellation)};
+ stage = program == Maxwell::ShaderType::VertexB && has_task ? Shader::Stage::Task
+ : Shader::Stage::Mesh;
+ mesh_registers = {regs.reg_array[Maxwell::MeshShaderA], regs.reg_array[Maxwell::MeshShaderB],
+ regs.reg_array[Maxwell::MeshInitShader]};
+ if (stage == Shader::Stage::Mesh && regs.IsShaderConfigEnabled(Maxwell::ShaderType::Geometry)) {
+ const auto& geometry{regs.pipelines[static_cast<size_t>(Maxwell::ShaderType::Geometry)]};
+ const GPUVAddr address{(static_cast<u64>(geometry.reserved[0] & 0xff) << 32) |
+ geometry.reserved[1]};
+ gpu_memory->ReadBlock(address, &mesh_primitive_header, sizeof(mesh_primitive_header));
+ }
+ }
const u64 local_size{sph.LocalMemorySize()};
ASSERT(local_size <= (std::numeric_limits<u32>::max)());
local_memory_size = static_cast<u32>(local_size) + sph.common3.shader_local_memory_crs_size;
@@ -594,7 +694,8 @@ u32 GraphicsEnvironment::ReadViewportTransformState() {
ComputeEnvironment::ComputeEnvironment(Tegra::Engines::KeplerCompute& kepler_compute_,
Tegra::MemoryManager& gpu_memory_, GPUVAddr program_base_,
- u32 start_address_)
+ u32 start_address_,
+ std::shared_ptr<const LoweredNativeCode> lowered_native_)
: GenericEnvironment{gpu_memory_, program_base_, start_address_}, kepler_compute{
&kepler_compute_} {
const auto& qmd{kepler_compute->launch_description};
@@ -607,7 +708,7 @@ ComputeEnvironment::ComputeEnvironment(Tegra::Engines::KeplerCompute& kepler_com
if (kepler_compute->IsNativeMode()) {
initial_offset = sizeof(sph);
is_proprietary_driver = true;
- InitializeNativeCode(0);
+ InitializeNativeCode(std::move(lowered_native_), 0);
start_address = sizeof(sph);
}
}
@@ -712,7 +813,8 @@ void FileEnvironment::Deserialize(std::ifstream& file) {
file.read(reinterpret_cast<char*>(&gp_passthrough_mask), sizeof(gp_passthrough_mask));
}
}
- is_proprietary_driver = texture_bound == 2;
+ // Native programs fold bank 1 reads as they did when they were built.
+ is_proprietary_driver = texture_bound == 2 || Shader::Ampere::HasNativeAddressMap(code);
}
void FileEnvironment::Dump(u64 pipeline_hash, u64 shader_hash) {
diff --git a/src/video_core/shader_environment.h b/src/video_core/shader_environment.h
index e49f01d95f..30d59955af 100644
--- a/src/video_core/shader_environment.h
+++ b/src/video_core/shader_environment.h
@@ -29,6 +29,19 @@ class Memorymanager;
namespace VideoCommon {
+/// A native (Switch 2) program after capture and lowering to Maxwell code. The shader cache keeps
+/// it so the environments built for the program's pipelines don't capture and lower it again.
+struct LoweredNativeCode {
+ /// Shader header, lowered instructions with Maxwell scheduling words, then the native
+ /// address map (what the environment's `code` used to be filled with).
+ std::vector<u64> code;
+ /// Bytes of the native program that were read, header included.
+ size_t source_size{};
+ /// Constant buffer holding the texture handles if the program requires one, otherwise ~0U
+ /// (the engine's register value is used).
+ u32 texture_bound = ~0U;
+};
+
class GenericEnvironment : public Shader::Environment {
public:
explicit GenericEnvironment() = default;
@@ -63,6 +76,10 @@ public:
return native_source_size;
}
+ [[nodiscard]] const std::shared_ptr<const LoweredNativeCode>& LoweredNative() const noexcept {
+ return lowered_native;
+ }
+
[[nodiscard]] bool CanBeSerialized() const noexcept;
[[nodiscard]] u64 CalculateHash() const;
@@ -78,8 +95,12 @@ public:
protected:
std::optional<u64> TryFindSize();
- void InitializeNativeCode(u32 source_header_size = 128);
+ /// Adopts an already lowered native program, or captures and lowers it when there is none.
+ void InitializeNativeCode(std::shared_ptr<const LoweredNativeCode> lowered,
+ u32 source_header_size = 128);
+ std::shared_ptr<const LoweredNativeCode> LowerNativeCode(u32 source_header_size);
size_t native_source_size{};
+ std::shared_ptr<const LoweredNativeCode> lowered_native;
Tegra::Texture::TICEntry ReadTextureInfo(GPUVAddr tic_addr, u32 tic_limit,
bool via_header_index, u32 raw);
@@ -117,7 +138,8 @@ public:
explicit GraphicsEnvironment(Tegra::Engines::Maxwell3D& maxwell3d_,
Tegra::MemoryManager& gpu_memory_,
Tegra::Engines::Maxwell3D::Regs::ShaderType program,
- GPUVAddr program_base_, u32 start_address_);
+ GPUVAddr program_base_, u32 start_address_,
+ std::shared_ptr<const LoweredNativeCode> lowered_native_ = {});
~GraphicsEnvironment() override = default;
@@ -143,7 +165,8 @@ public:
explicit ComputeEnvironment() = default;
explicit ComputeEnvironment(Tegra::Engines::KeplerCompute& kepler_compute_,
Tegra::MemoryManager& gpu_memory_, GPUVAddr program_base_,
- u32 start_address_);
+ u32 start_address_,
+ std::shared_ptr<const LoweredNativeCode> lowered_native_ = {});
~ComputeEnvironment() override = default;
diff --git a/src/video_core/shader_handle_program.h b/src/video_core/shader_handle_program.h
new file mode 100644
index 0000000000..3d35524cc8
--- /dev/null
+++ b/src/video_core/shader_handle_program.h
@@ -0,0 +1,39 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include "common/common_types.h"
+#include "shader_recompiler/shader_info.h"
+#include "video_core/memory_manager.h"
+
+namespace VideoCommon {
+
+/// The texture handle a shader reads from a storage buffer, computed from the current constant
+/// buffer and storage buffer words (Shader::Info::handle_programs). cbuf_address(index) is the
+/// bound constant buffer's address, 0 when none is bound.
+template <typename CbufAddress>
+u32 EvaluateTextureHandle(const Shader::Info& info, u32 program, Tegra::MemoryManager& gpu_memory,
+ CbufAddress&& cbuf_address) {
+ return Shader::EvaluateHandleProgram(
+ info.handle_programs[program],
+ [&](u32 index, u32 offset) -> u32 {
+ const GPUVAddr base{cbuf_address(index)};
+ return base != 0 ? gpu_memory.Read<u32>(base + offset) : 0U;
+ },
+ [&](u32 binding, u32 offset) -> u32 {
+ if (binding >= info.storage_buffers_descriptors.size()) {
+ return 0U;
+ }
+ const auto& descriptor{info.storage_buffers_descriptors[binding]};
+ const GPUVAddr cbuf{cbuf_address(descriptor.cbuf_index)};
+ if (cbuf == 0) {
+ return 0U;
+ }
+ const GPUVAddr base{gpu_memory.Read<u64>(cbuf + descriptor.cbuf_offset)};
+ return base != 0 ? gpu_memory.Read<u32>(base + offset) : 0U;
+ })
+ .value_or(0U);
+}
+
+} // namespace VideoCommon
diff --git a/src/video_core/surface.cpp b/src/video_core/surface.cpp
index 66b9577cb5..ef4859ea3e 100644
--- a/src/video_core/surface.cpp
+++ b/src/video_core/surface.cpp
@@ -214,6 +214,8 @@ PixelFormat PixelFormatFromGPUPixelFormat(Service::android::PixelFormat format)
return PixelFormat::R5G6B5_UNORM;
case Service::android::PixelFormat::Bgra8888:
return PixelFormat::B8G8R8A8_UNORM;
+ case Service::android::PixelFormat::Rgba1010102:
+ return PixelFormat::A2B10G10R10_UNORM;
default:
UNIMPLEMENTED_MSG("Unimplemented format={}", format);
return PixelFormat::A8B8G8R8_UNORM;
diff --git a/src/video_core/texture_cache/descriptor_table.h b/src/video_core/texture_cache/descriptor_table.h
index e40c128ab5..a441e572de 100644
--- a/src/video_core/texture_cache/descriptor_table.h
+++ b/src/video_core/texture_cache/descriptor_table.h
@@ -30,11 +30,18 @@ public:
}
void Invalidate() noexcept {
- std::ranges::fill(read_descriptors, 0);
+ // Cleared on the next read: the texture cache invalidates every channel's tables for
+ // each image it deletes, dozens of times a frame while its collector runs.
+ invalidated = true;
}
- [[nodiscard]] std::pair<T, bool> Read(Tegra::MemoryManager const& gpu_memory, u32 index) noexcept {
+ template <class MemoryManager>
+ [[nodiscard]] std::pair<T, bool> Read(const MemoryManager& gpu_memory, u32 index) noexcept {
DEBUG_ASSERT(index <= current_limit);
+ if (invalidated) {
+ ClearReadDescriptors();
+ invalidated = false;
+ }
const GPUVAddr gpu_addr = current_gpu_addr + index * sizeof(T);
std::pair<T, bool> result;
gpu_memory.ReadBlockUnsafe(gpu_addr, std::addressof(result.first), sizeof(T));
@@ -56,10 +63,9 @@ public:
// Mario Brothership reallocates a lot of times, so use aggressive pre-alloc sizes
// std::vector<T> by default uses quadratic growth, but that isn't even enough to satisfy brothership
const size_t num_descriptors = ((limit + 0x80000) & (~0x7ffff)) + 1;
- size_t old_size = read_descriptors.size();
read_descriptors.resize(Common::DivCeil(num_descriptors, 64U));
- old_size = (std::min)(old_size, read_descriptors.size());
- std::fill(read_descriptors.begin(), read_descriptors.begin() + old_size, 0);
+ ClearReadDescriptors();
+ invalidated = false;
//
descriptors.resize(num_descriptors);
}
@@ -68,6 +74,14 @@ public:
std::vector<T> descriptors;
GPUVAddr current_gpu_addr{};
u32 current_limit{};
+ bool invalidated{}; ///< read_descriptors is to be cleared before the next read
+
+private:
+ void ClearReadDescriptors() noexcept {
+ // Reserved capacity is not addressable. A later limit increase clears its new range.
+ const size_t words = (std::min)(read_descriptors.size(), size_t{current_limit} / 64 + 1);
+ std::fill_n(read_descriptors.begin(), words, u64{0});
+ }
};
} // namespace VideoCommon
diff --git a/src/video_core/texture_cache/format_lookup_table.cpp b/src/video_core/texture_cache/format_lookup_table.cpp
index 20bb96aae3..18be2dcbf3 100644
--- a/src/video_core/texture_cache/format_lookup_table.cpp
+++ b/src/video_core/texture_cache/format_lookup_table.cpp
@@ -150,6 +150,7 @@ PixelFormat PixelFormatFromTextureInfo(TextureFormat format, ComponentType red,
case Hash(TextureFormat::X8Z24, UNORM):
return PixelFormat::X8_D24_UNORM;
case Hash(TextureFormat::X8Z24, UNORM, UINT, UINT, UINT, LINEAR):
+ case Hash(TextureFormat::X8Z24, UNORM, UINT, UNORM, UNORM, LINEAR):
return PixelFormat::X8_D24_UNORM;
case Hash(TextureFormat::Z24S8, UINT, UNORM, UNORM, UNORM, LINEAR):
return PixelFormat::S8_UINT_D24_UNORM;
diff --git a/src/video_core/texture_cache/image_base.h b/src/video_core/texture_cache/image_base.h
index 04254fb417..878c229953 100644
--- a/src/video_core/texture_cache/image_base.h
+++ b/src/video_core/texture_cache/image_base.h
@@ -56,6 +56,10 @@ enum class ImageFlagBits : u32 {
/// A large array over Switch 2 sparse blocks: unbacked, each layer gets device memory when
/// something is written to it (Image::BindLayers) and only those layers are uploaded.
SparseLayers = 1 << 22,
+ /// A large single image over Switch 2 sparse blocks: unbacked, each host sparse block gets
+ /// device memory when the guest memory under it can hold data (Image::BindTiles), and only
+ /// the parts written are uploaded.
+ SparseTiles = 1 << 23,
};
DECLARE_ENUM_FLAG_OPERATORS(ImageFlagBits)
@@ -108,6 +112,9 @@ struct ImageBase {
/// Large layered images uploaded by layer: one bit per layer written since the last upload
/// while CpuModified. Empty while CpuModified means the whole image.
std::vector<u64> dirty_layers;
+ /// SparseTiles images: the guest byte ranges (offset, size) written since the last upload while
+ /// CpuModified. Empty while CpuModified means all of it that can hold data.
+ std::vector<std::pair<u64, u64>> dirty_ranges;
u64 scale_tick = 0;
bool has_scaled = false;
diff --git a/src/video_core/texture_cache/texture_cache.h b/src/video_core/texture_cache/texture_cache.h
index c8aaf8dff5..4929c2df44 100644
--- a/src/video_core/texture_cache/texture_cache.h
+++ b/src/video_core/texture_cache/texture_cache.h
@@ -125,6 +125,7 @@ template <class P>
void TextureCache<P>::RunGarbageCollector() {
bool high_priority_mode = false;
bool aggressive_mode = false;
+ bool evict_old_costly = false;
u64 ticks_to_destroy = 0;
size_t num_iterations = 0;
const auto Configure = [&](bool allow_aggressive) {
@@ -139,7 +140,8 @@ void TextureCache<P>::RunGarbageCollector() {
ticks_to_destroy = (std::max)(ticks_to_destroy, min_age);
num_iterations = aggressive_mode ? 40 : (high_priority_mode ? 20 : 10);
};
- const auto Cleanup = [this, &num_iterations, &high_priority_mode, &aggressive_mode](ImageId image_id) {
+ const auto Cleanup = [this, &num_iterations, &high_priority_mode, &aggressive_mode,
+ &evict_old_costly](ImageId image_id) {
if (num_iterations == 0) {
return true;
}
@@ -149,7 +151,8 @@ void TextureCache<P>::RunGarbageCollector() {
return false;
}
const bool must_download = IsDownloadable(image) && False(image.flags & ImageFlagBits::BadOverlap);
- if ((!aggressive_mode && True(image.flags & ImageFlagBits::CostlyLoad)) || (!high_priority_mode && must_download)) {
+ if ((!aggressive_mode && !evict_old_costly && True(image.flags & ImageFlagBits::CostlyLoad)) ||
+ (!high_priority_mode && must_download)) {
return false;
}
if (must_download) {
@@ -175,16 +178,84 @@ void TextureCache<P>::RunGarbageCollector() {
}
return false;
};
+ // Deleting by age alone left every device memory block partly used: the collector deletes
+ // thousands of images a minute and new ones land in the holes, so blocks seldom empty (title
+ // 1: ~0.8 GB of holes in ~190 32 MiB blocks after ten minutes, which the driver counts as
+ // used). Among the oldest candidates (8 per image this pass may delete), those in the
+ // emptiest blocks go first, so blocks drain and go back to the driver; placement already
+ // prefers the fullest blocks (best fit). Images with memory of their own sit in the middle.
+ const auto Collect = [&] {
+ if constexpr (requires(const Image& image) { image.MemoryBlock(); }) {
+ std::vector<ImageId> candidates;
+ const size_t limit = num_iterations * 8;
+ lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, [&](ImageId image_id) {
+ candidates.push_back(image_id);
+ return candidates.size() >= limit;
+ });
+ if (candidates.empty()) {
+ return;
+ }
+ if (block_image_bytes_frame == ~0ULL || frame_tick - block_image_bytes_frame >= 60) {
+ block_image_bytes_frame = frame_tick;
+ block_image_bytes.clear();
+ for (const auto [image_id, image] : slot_images) {
+ const auto block = image->MemoryBlock();
+ if (block.block != 0) {
+ block_image_bytes[block.block] += block.size;
+ }
+ }
+ }
+ std::vector<std::pair<double, ImageId>> ordered;
+ ordered.reserve(candidates.size());
+ for (const ImageId image_id : candidates) {
+ const auto block = slot_images[image_id].MemoryBlock();
+ double fill = 0.5;
+ if (block.block != 0 && block.block_size != 0) {
+ const auto it = block_image_bytes.find(block.block);
+ fill = it != block_image_bytes.end()
+ ? static_cast<double>(it->second) / static_cast<double>(block.block_size)
+ : 0.0;
+ }
+ ordered.emplace_back(fill, image_id);
+ }
+ std::stable_sort(ordered.begin(), ordered.end(),
+ [](const auto& a, const auto& b) { return a.first < b.first; });
+ for (const auto& [fill, image_id] : ordered) {
+ if (Cleanup(image_id)) {
+ break;
+ }
+ }
+ } else {
+ lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
+ }
+ };
Configure(false);
- lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
+ static const u64 old_age = [] {
+ const char* value = std::getenv("EDEN_NX2_TC_OLD_AGE");
+ return value != nullptr ? std::strtoull(value, nullptr, 10) : 1800ULL;
+ }();
+ if (high_priority_mode && old_age != 0 && frame_tick > old_age) {
+ evict_old_costly = true;
+ lru_cache.ForEachItemBelow(frame_tick - old_age, Cleanup);
+ evict_old_costly = false;
+ }
+ Collect();
if (total_used_memory >= critical_memory) {
Configure(true);
- lru_cache.ForEachItemBelow(frame_tick - ticks_to_destroy, Cleanup);
+ Collect();
}
}
template <class P>
void TextureCache<P>::ApplyThresholdOverrides(bool log) {
+ // On every GPU: past what a frame needs, more device memory only keeps the textures of earlier
+ // scenes (title 1 filled 6.7 GB of an 8 GB card and draws its races in ~5 GB), so collect from
+ // MaxCriticalMemory on rather than near the card's budget.
+ critical_memory = (std::min)(critical_memory, MaxCriticalMemory);
+ expected_memory =
+ (std::min)(expected_memory, critical_memory - (std::min)(critical_memory, u64{768_MiB}));
+ minimum_memory =
+ (std::min)(minimum_memory, expected_memory - (std::min)(expected_memory, u64{1_GiB}));
// EDEN_NX2_TC_EXPECTED_MIB / EDEN_NX2_TC_CRITICAL_MIB override the collector's thresholds.
for (const auto& [name, target] : {std::pair{"EDEN_NX2_TC_EXPECTED_MIB", &expected_memory},
std::pair{"EDEN_NX2_TC_CRITICAL_MIB", &critical_memory}}) {
@@ -230,26 +301,78 @@ void TextureCache<P>::TickFrame() {
// budget as the card fills (16 GB card: 13.5 GB left mid-race), hence the re-read every 30
// frames. EDEN_NX2_TC_*_MIB overrides still win.
if constexpr (requires { runtime.GetDeviceMemoryBudget(); }) {
+ if constexpr (requires { runtime.MemoryPressureEvents(); }) {
+ // Images that did not fit (made in host memory or with nothing bound) mean the
+ // thresholds sit too high for this card and driver: leave more room until the
+ // pressure has been gone for a while.
+ if (native_thresholds_applied) {
+ const u32 events = runtime.MemoryPressureEvents();
+ if (events != seen_pressure_events) {
+ seen_pressure_events = events;
+ pressure_margin = (std::min)(pressure_margin + 512_MiB, 3_GiB);
+ pressure_quiet_frames = 0;
+ budget_check_frames = 0; // thresholds are recomputed below
+ LOG_WARNING(HW_GPU, "Out of device memory: keeping {} MiB more free",
+ pressure_margin >> 20);
+ } else if (pressure_margin != 0 && ++pressure_quiet_frames % 600 == 0) {
+ pressure_margin -= (std::min)(pressure_margin, 128_MiB);
+ budget_check_frames = 0;
+ }
+ }
+ }
if (native_thresholds_applied && budget_check_frames++ % 30 == 0) {
const u64 budget = runtime.GetDeviceMemoryBudget();
if (budget > 2_GiB) {
const u64 old_critical = critical_memory;
const u64 margin = (std::min)(budget / 8, 1_GiB);
- critical_memory = budget - margin;
- expected_memory = budget - 2 * margin;
+ if constexpr (requires { runtime.GetDeviceMemorySlack(); }) {
+ // The usage counted here leaves out the unused part of the allocator's
+ // blocks, which the driver counts and a full heap cannot hand to a new
+ // image (RX 7800 XT: 1.06 GiB of it, the whole margin, with the heap at
+ // its budget for a minute before images stopped fitting). Reserve it
+ // too: at once when it grows, slowly when it shrinks, so evicting images
+ // (which leaves partly used blocks) does not chase the threshold down.
+ const u64 slack = (std::min)(runtime.GetDeviceMemorySlack(), 1536_MiB);
+ slack_margin = slack >= slack_margin
+ ? slack
+ : slack_margin - (std::min)(slack_margin - slack, 16_MiB);
+ }
+ critical_memory =
+ budget - (std::min)(margin + pressure_margin + slack_margin, budget / 2);
+ expected_memory = critical_memory - (std::min)(margin, critical_memory / 2);
// Log the first values and real changes (the budget moves by a few MiB all
// the time).
const u64 change = critical_memory > old_critical
? critical_memory - old_critical
: old_critical - critical_memory;
- ApplyThresholdOverrides(budget_check_frames == 1 || change >= 256_MiB);
+ const bool log = budget_check_frames == 1 || change >= 256_MiB;
+ ApplyThresholdOverrides(log);
+ if (log) {
+ LOG_INFO(HW_GPU,
+ "Memory margins: base {} MiB, out of memory {} MiB, block slack {} MiB",
+ margin >> 20, pressure_margin >> 20, slack_margin >> 20);
+ }
}
}
}
}
+ if constexpr (requires { runtime.CurrentTick(); }) {
+ while (!gpu_sentenced_images.empty() &&
+ runtime.IsFree(gpu_sentenced_images.front().tick)) {
+ gpu_sentenced_bytes -= gpu_sentenced_images.front().bytes;
+ gpu_sentenced_images.pop_front();
+ }
+ }
// If we can obtain the memory info, use it instead of the estimate.
if (runtime.CanReportMemoryUsage()) {
total_used_memory = runtime.GetDeviceMemoryUsage();
+ // Images the collector deleted keep their memory for TICKS_TO_DESTROY frames: counting
+ // it kept the collector in aggressive mode, deleting 40+ images a frame for frames after
+ // it had freed enough (title 1: frames over 100 ms for seconds at a time).
+ for (const u64 bytes : sentenced_image_bytes) {
+ total_used_memory -= (std::min)(total_used_memory, bytes);
+ }
+ total_used_memory -= (std::min)(total_used_memory, gpu_sentenced_bytes);
}
// Every 600 frames: device memory in use against the collector's thresholds, and what the
// images themselves account for (VRAM pressure makes the collector thrash).
@@ -316,10 +439,18 @@ void TextureCache<P>::TickFrame() {
RunGarbageCollector();
}
sentenced_images.Tick();
+ sentenced_image_index = (sentenced_image_index + 1) % TICKS_TO_DESTROY;
+ sentenced_image_bytes[sentenced_image_index] = 0;
sentenced_framebuffers.Tick();
sentenced_image_view.Tick();
TickAsyncDecode();
TickAsyncUnswizzle();
+ if (frame_tick % 600 == 0 && !uploaded_ranges.empty()) {
+ for (const auto& [address, size] : uploaded_ranges) {
+ device_memory.HintCold(address, size);
+ }
+ uploaded_ranges.clear();
+ }
runtime.TickFrame();
++frame_tick;
@@ -678,6 +809,8 @@ typename P::Framebuffer* TextureCache<P>::GetFramebuffer() {
template <class P>
ImageViewId TextureCache<P>::VisitImageView(u32 index, bool compute) {
auto& table = compute ? channel_state->compute_image_table : channel_state->graphics_image_table;
+ index = Tegra::Texture::TextureHeaderIndex(index, table.current_limit,
+ gpu_memory->UsesNativeTiling());
if (index > table.current_limit) {
LOG_DEBUG(HW_GPU, "Invalid image view index={}", index);
return NULL_IMAGE_VIEW_ID;
@@ -689,8 +822,13 @@ ImageViewId TextureCache<P>::VisitImageView(u32 index, bool compute) {
if (IsValidEntry(*gpu_memory, descriptor)) {
// Is new (registered view) on the texture cache side?
const auto [pair, is_new_tc] = channel_state->image_views.try_emplace(descriptor);
- if (is_new_tc)
+ if (is_new_tc) {
pair->second = CreateImageView(descriptor);
+ if (pair->second != NULL_IMAGE_VIEW_ID) {
+ image_view_descriptors[pair->second].emplace_back(current_channel_id,
+ descriptor);
+ }
+ }
PrepareImageView(pair->second, false, false);
channel_state->image_view_ids.insert_or_assign(map_index, pair->second);
return pair->second;
@@ -729,6 +867,10 @@ void TextureCache<P>::WriteMemory(DAddr cpu_addr, size_t size) {
image.flags |= ImageFlagBits::CpuModified | ImageFlagBits::Remapped;
return;
}
+ if (True(image.flags & ImageFlagBits::SparseTiles)) {
+ MarkRangeWritten(image, cpu_addr, size);
+ return;
+ }
if (UploadsByLayer(image)) {
MarkLayersWritten(image, cpu_addr, size);
return;
@@ -953,6 +1095,73 @@ bool TextureCache<P>::BlitImage(const Tegra::Engines::Fermi2D::Surface& dst,
}
template <class P>
+bool TextureCache<P>::TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& dst,
+ const Tegra::Engines::Fermi2D::Surface& src,
+ const Tegra::Engines::Fermi2D::Surface& motion_vectors,
+ std::array<f32, 2> jitter, std::array<f32, 2> motion_scale) {
+ if constexpr (FRAMEBUFFER_BLITS) {
+ return false;
+ } else {
+ const Tegra::Engines::Fermi2D::Config copy{
+ .operation = Tegra::Engines::Fermi2D::Operation::SrcCopy,
+ .filter = Tegra::Engines::Fermi2D::Filter::Bilinear,
+ .must_accelerate = true,
+ .dst_x0 = 0,
+ .dst_y0 = 0,
+ .dst_x1 = static_cast<s32>(dst.width),
+ .dst_y1 = static_cast<s32>(dst.height),
+ .src_x0 = 0,
+ .src_y0 = 0,
+ .src_x1 = static_cast<s32>(src.width),
+ .src_y1 = static_cast<s32>(src.height),
+ };
+ const auto result = GetBlitImages(dst, src, copy);
+ if (!result) {
+ return false;
+ }
+ const ImageInfo motion_info(motion_vectors);
+ has_deleted_images = false;
+ const ImageId motion_id =
+ FindOrInsertImage(motion_info, motion_vectors.Address(), RelaxedOptions{});
+ if (has_deleted_images || !motion_id) {
+ return false; // the colour and output ids may be stale; the caller blits instead
+ }
+ const ImageId dst_id = result->dst_id;
+ const ImageId src_id = result->src_id;
+ PrepareImage(src_id, false, false);
+ PrepareImage(motion_id, false, false);
+ PrepareImage(dst_id, true, false);
+ for (const ImageId id : {src_id, motion_id, dst_id}) {
+ if (True(slot_images[id].flags & ImageFlagBits::Rescaled) ||
+ slot_images[id].info.num_samples != 1) {
+ return false;
+ }
+ }
+ const auto view = [&](ImageId id, const Tegra::Engines::Fermi2D::Surface& surface,
+ PixelFormat format) -> std::optional<std::pair<FramebufferId, ImageViewId>> {
+ const std::optional base = slot_images[id].TryFindBase(surface.Address());
+ if (!base) {
+ return std::nullopt;
+ }
+ const SubresourceRange range{.base = *base, .extent = {1, 1}};
+ return RenderTargetFromImage(id, ImageViewInfo(ImageViewType::e2D, format, range));
+ };
+ const auto src_view = view(src_id, src, result->src_format);
+ const auto motion_view = view(motion_id, motion_vectors, motion_info.format);
+ const auto dst_view = view(dst_id, dst, result->dst_format);
+ if (!src_view || !motion_view || !dst_view) {
+ return false;
+ }
+ // After the views: creating them may reallocate the framebuffer slots.
+ Framebuffer* const dst_framebuffer = &slot_framebuffers[dst_view->first];
+ return runtime.TemporalUpscale(dst_framebuffer, slot_image_views[dst_view->second],
+ slot_image_views[src_view->second],
+ slot_image_views[motion_view->second], jitter,
+ motion_scale);
+ }
+}
+
+template <class P>
std::pair<typename P::ImageView*, bool> TextureCache<P>::TryFindFramebufferImageView(
const Tegra::FramebufferConfig& config, DAddr cpu_addr) {
// TODO: Properly implement this
@@ -1316,6 +1525,10 @@ void TextureCache<P>::RefreshContents(Image& image, ImageId image_id) {
RefreshSparseLayers(image);
return;
}
+ if (True(image.flags & ImageFlagBits::SparseTiles)) {
+ RefreshSparseTiles(image);
+ return;
+ }
if (!image.dirty_layers.empty() && UploadWrittenLayers(image)) {
runtime.InsertUploadMemoryBarrier();
return;
@@ -1357,6 +1570,21 @@ void TextureCache<P>::RefreshContents(Image& image, ImageId image_id) {
auto staging = runtime.UploadStagingBuffer(MapSizeBytes(image));
UploadImageContents(image, staging);
runtime.InsertUploadMemoryBarrier();
+ // The host GPU has its own copy now and the guest rarely reads this memory again: TickFrame
+ // marks it cold, so the OS pages it out before anything else when it needs RAM (title 1
+ // streams several GB of textures through it).
+ constexpr u64 ColdHintMinBytes = 64_KiB;
+ if (image.guest_size_bytes >= ColdHintMinBytes &&
+ False(image.flags & ImageFlagBits::GpuModified)) {
+ if (True(image.flags & ImageFlagBits::Sparse)) {
+ // Non-contiguous: each mapped piece (title 1 maps its own memory into these).
+ ForEachSparseSegment(image, [&](GPUVAddr, DAddr address, size_t size) {
+ uploaded_ranges.emplace_back(address, size);
+ });
+ } else {
+ uploaded_ranges.emplace_back(image.cpu_addr, image.guest_size_bytes);
+ }
+ }
}
template <class P>
@@ -1401,6 +1629,77 @@ void TextureCache<P>::MarkLayersWritten(Image& image, DAddr cpu_addr, size_t siz
}
template <class P>
+void TextureCache<P>::MarkRangeWritten(Image& image, DAddr cpu_addr, size_t size) {
+ if (True(image.flags & ImageFlagBits::CpuModified) && image.dirty_ranges.empty()) {
+ return; // all of it is dirty already
+ }
+ const DAddr begin = (std::max<DAddr>)(cpu_addr, image.cpu_addr);
+ const DAddr end = (std::min<DAddr>)(cpu_addr + size, image.cpu_addr + image.guest_size_bytes);
+ if (end <= begin) {
+ return;
+ }
+ image.flags |= ImageFlagBits::CpuModified;
+ image.dirty_ranges.emplace_back(begin - image.cpu_addr, end - begin);
+}
+
+template <class P>
+void TextureCache<P>::RefreshSparseTiles(Image& image) {
+ if constexpr (requires { &Image::BindTiles; }) {
+ // The ranges written since the last upload, or, for a new image, all of it that can hold
+ // data (the tiles the game maps there, or the blocks written).
+ std::vector<std::pair<u64, u64>> ranges = std::move(image.dirty_ranges);
+ image.dirty_ranges.clear();
+ if (ranges.empty()) {
+ ranges = Tegra::Nx2SparseDataRanges(image.gpu_addr, image.guest_size_bytes);
+ }
+ const std::vector<LevelRect> rects =
+ SparseTileRects(image.info, ranges, image.SparseTileWidth(), image.SparseTileHeight(),
+ image.SparseTailLevel());
+ if (rects.empty() || !BindSparseTiles(image, rects)) {
+ runtime.TransitionImageLayout(image);
+ return;
+ }
+ const size_t bytes = UnswizzledRectsBytes(image.info, rects);
+ auto staging = runtime.UploadStagingBuffer(bytes);
+ const std::vector<BufferImageCopy> copies =
+ UnswizzleImageRects(*gpu_memory, image.gpu_addr, image.info, rects, staging.mapped_span);
+ image.UploadMemory(staging, copies);
+ runtime.InsertUploadMemoryBarrier();
+ ++images_uploaded;
+ uploaded_bytes += bytes;
+ }
+}
+
+template <class P>
+bool TextureCache<P>::BindSparseTiles(Image& image, std::span<const LevelRect> rects) {
+ if constexpr (requires { &Image::BindTiles; }) {
+ const u64 bound_bytes = ImageMemoryBytes(image);
+ const bool bound = image.BindTiles(rects);
+ // RegisterImage counts what an image has bound by then; later binds are added here.
+ if (True(image.flags & ImageFlagBits::Registered)) {
+ total_used_memory += ImageMemoryBytes(image) - bound_bytes;
+ }
+ return bound;
+ } else {
+ return false;
+ }
+}
+
+template <class P>
+bool TextureCache<P>::BackSparseTilesWhole(Image& image) {
+ if constexpr (requires { &Image::BackTilesWhole; }) {
+ const u64 bound_bytes = ImageMemoryBytes(image);
+ const bool backed = image.BackTilesWhole();
+ if (True(image.flags & ImageFlagBits::Registered)) {
+ total_used_memory += ImageMemoryBytes(image) - bound_bytes;
+ }
+ return backed;
+ } else {
+ return false;
+ }
+}
+
+template <class P>
bool TextureCache<P>::UploadWrittenLayers(Image& image) {
if constexpr (requires { &Runtime::AccelerateImageUploadLayers; }) {
const u32 num_layers = static_cast<u32>(image.info.resources.layers);
@@ -1712,9 +2011,14 @@ void TextureCache<P>::InvalidateScale(Image& image) {
for (size_t c : active_channel_ids) {
auto& channel_info = channel_storage[c];
+#ifndef NDEBUG
+ // A walk over every entry (title 1: ~5,400 in 5 channels) for each deleted image: debug
+ // builds only. The tables are invalidated below, so each entry is written again before
+ // it is read.
if constexpr (ENABLE_VALIDATION)
for (auto& e : channel_info.image_view_ids)
e.second = CORRUPT_ID;
+#endif
channel_info.graphics_image_table.Invalidate();
channel_info.compute_image_table.Invalidate();
}
@@ -2832,6 +3136,8 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
}
// TickFrame sets the total from the driver, which can be under the sum of these estimates.
total_used_memory -= (std::min)(total_used_memory, ImageMemoryBytes(slot_images[image_id]));
+ const u64 freed_bytes = (image.HasScaled() ? GetScaledImageSizeBytes(image) : 0) +
+ ImageMemoryBytes(slot_images[image_id]);
const GPUVAddr gpu_addr = image.gpu_addr;
const auto alloc_it = image_allocs_table.find(gpu_addr);
if (alloc_it == image_allocs_table.end()) {
@@ -2896,19 +3202,32 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
if (retained) {
LOG_INFO(Render_Vulkan, "Retained image for queued download: id={}", image_id.Value());
} else if (!immediate_delete) {
- sentenced_images.Push(std::move(slot_images[image_id]));
+ if constexpr (requires { runtime.CurrentTick(); }) {
+ gpu_sentenced_images.push_back(
+ {runtime.CurrentTick(), freed_bytes, std::move(slot_images[image_id])});
+ gpu_sentenced_bytes += freed_bytes;
+ } else {
+ sentenced_images.Push(std::move(slot_images[image_id]));
+ sentenced_image_bytes[sentenced_image_index] += freed_bytes;
+ }
}
slot_images.erase(image_id);
alloc_images.erase(alloc_image_it);
if (alloc_images.empty()) {
image_allocs_table.erase(alloc_it);
+ slot_image_allocs.erase(alloc_id);
}
for (size_t c : active_channel_ids) {
auto& channel_info = channel_storage[c];
+#ifndef NDEBUG
+ // A walk over every entry (title 1: ~5,400 in 5 channels) for each deleted image: debug
+ // builds only. The tables are invalidated below, so each entry is written again before
+ // it is read.
if constexpr (ENABLE_VALIDATION)
for (auto& e : channel_info.image_view_ids)
e.second = CORRUPT_ID;
+#endif
channel_info.graphics_image_table.Invalidate();
channel_info.compute_image_table.Invalidate();
}
@@ -2917,17 +3236,23 @@ void TextureCache<P>::DeleteImage(ImageId image_id, bool immediate_delete) {
template <class P>
void TextureCache<P>::RemoveImageViewReferences(std::span<const ImageViewId> removed_views) {
- for (size_t c : active_channel_ids) {
- auto& channel_info = channel_storage[c];
- auto it = channel_info.image_views.begin();
- while (it != channel_info.image_views.end()) {
- const auto found = std::ranges::find(removed_views, it->second);
- if (found != removed_views.end()) {
- it = channel_info.image_views.erase(it);
- } else {
- ++it;
+ for (const ImageViewId removed : removed_views) {
+ const auto refs = image_view_descriptors.find(removed);
+ if (refs == image_view_descriptors.end()) {
+ continue;
+ }
+ for (const auto& [channel, descriptor] : refs->second) {
+ // The channel may be gone, or its entry point to another view since.
+ if (std::ranges::find(active_channel_ids, channel) == active_channel_ids.end()) {
+ continue;
+ }
+ auto& image_views = channel_storage[channel].image_views;
+ if (const auto it = image_views.find(descriptor);
+ it != image_views.end() && it->second == removed) {
+ image_views.erase(it);
}
}
+ image_view_descriptors.erase(refs);
}
}
@@ -2944,6 +3269,7 @@ void TextureCache<P>::RemoveFramebuffers(std::span<const ImageViewId> removed_vi
last_framebuffer_serial = 0;
}
sentenced_framebuffers.Push(std::move(slot_framebuffers[framebuffer_id]));
+ slot_framebuffers.erase(framebuffer_id);
it = framebuffers.erase(it);
} else {
++it;
@@ -3025,6 +3351,23 @@ void TextureCache<P>::PrepareImage(ImageId image_id, bool is_modification, bool
}
}
if (is_modification) {
+ if constexpr (requires { &Image::BackTilesWhole; }) {
+ if (True(image.flags & ImageFlagBits::SparseTiles) && image.SparseTilesShareZeroBlock()) {
+ // The render would write the zeroed block all empty blocks share: each gets memory
+ // of its own first, and the parts that hold data are loaded again.
+ LOG_INFO(HW_GPU, "GPU write into sparse tiled image {:#x}; backing it whole",
+ image.gpu_addr);
+ if (BackSparseTilesWhole(image)) {
+ image.dirty_ranges.clear();
+ RefreshSparseTiles(image);
+ } else {
+ LOG_ERROR(HW_GPU,
+ "Sparse tiled image {:#x}: out of memory backing it whole, the GPU "
+ "write lands in the shared zeroed block",
+ image.gpu_addr);
+ }
+ }
+ }
if constexpr (requires { &Image::BindLayers; }) {
if (True(image.flags & ImageFlagBits::SparseLayers)) {
std::vector<u32> layers(static_cast<size_t>(image.info.resources.layers));
@@ -3063,6 +3406,10 @@ template <class P>
void TextureCache<P>::CopyImage(ImageId dst_id, ImageId src_id, std::vector<ImageCopy> copies) {
Image& dst = slot_images[dst_id];
Image& src = slot_images[src_id];
+ if (True(dst.flags & ImageFlagBits::UnwrittenSparse) ||
+ True(src.flags & ImageFlagBits::UnwrittenSparse)) {
+ return;
+ }
if (dst.info.astc_opaque != src.info.astc_opaque) {
// BC1 and BC3 blocks differ in size, so the host can't copy between them; both hold the
// same guest data, which is uploaded from guest memory instead.
diff --git a/src/video_core/texture_cache/texture_cache_base.h b/src/video_core/texture_cache/texture_cache_base.h
index 94102cd261..09deb8da02 100644
--- a/src/video_core/texture_cache/texture_cache_base.h
+++ b/src/video_core/texture_cache/texture_cache_base.h
@@ -136,6 +136,10 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI
u64 native_critical_memory{};
u64 native_expected_memory{};
u64 budget_check_frames{};
+ u32 seen_pressure_events{}; ///< Allocator pressure events already answered
+ u64 pressure_margin{}; ///< Extra room under the budget after images did not fit
+ u64 pressure_quiet_frames{}; ///< Frames since the last such image
+ u64 slack_margin{}; ///< Extra room for allocator block space the driver counts as used
u64 astc_opaque_images{};
u64 astc_alpha_images{};
std::vector<u8> astc_scan_buffer;
@@ -144,6 +148,9 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI
static constexpr s64 DEFAULT_EXPECTED_MEMORY = 1_GiB + 125_MiB;
static constexpr s64 DEFAULT_CRITICAL_MEMORY = 1_GiB + 625_MiB;
+ /// The most device memory the collector lets the cache fill before it collects in earnest,
+ /// on every GPU (ApplyThresholdOverrides).
+ static constexpr u64 MaxCriticalMemory = 5_GiB;
static constexpr size_t GC_EMERGENCY_COUNTS = 2;
using Runtime = typename P::Runtime;
@@ -164,6 +171,18 @@ class TextureCache : public VideoCommon::ChannelSetupCaches<TextureCacheChannelI
/// Records the layers a write of [cpu_addr, +size) touches.
void MarkLayersWritten(Image& image, DAddr cpu_addr, size_t size);
+ /// SparseTiles images: notes a written range, uploaded with the next refresh.
+ void MarkRangeWritten(Image& image, DAddr cpu_addr, size_t size);
+
+ /// SparseTiles images: binds and uploads the parts written since the last upload.
+ void RefreshSparseTiles(Image& image);
+
+ /// Image::BindTiles, counting what it binds in total_used_memory.
+ bool BindSparseTiles(Image& image, std::span<const LevelRect> rects);
+
+ /// Image::BackTilesWhole, counting what it binds in total_used_memory.
+ bool BackSparseTilesWhole(Image& image);
+
/// Uploads the image's dirty layers. False when it needs a full upload instead.
bool UploadWrittenLayers(Image& image);
@@ -264,6 +283,13 @@ public:
const Tegra::Engines::Fermi2D::Surface& src,
const Tegra::Engines::Fermi2D::Config& copy);
+ /// Stand-in for a DLSS evaluation: temporal upscale of src into dst (see the runtime's
+ /// TemporalUpscale). False when the images cannot take it; the caller then blits.
+ bool TemporalUpscale(const Tegra::Engines::Fermi2D::Surface& dst,
+ const Tegra::Engines::Fermi2D::Surface& src,
+ const Tegra::Engines::Fermi2D::Surface& motion_vectors,
+ std::array<f32, 2> jitter, std::array<f32, 2> motion_scale);
+
/// Try to find a cached image view in the given CPU address
[[nodiscard]] std::pair<ImageView*, bool> TryFindFramebufferImageView(
const Tegra::FramebufferConfig& config, DAddr cpu_addr);
@@ -433,6 +459,12 @@ private:
/// Remove image views references from the cache
void RemoveImageViewReferences(std::span<const ImageViewId> removed_views);
+ /// Per image view, the (channel, descriptor) entries of the channels' image_views maps that
+ /// point to it. Deleting an image erased them by scanning every map (title 1: ~5,000 entries,
+ /// 50-130 us for each deleted image, dozens a frame while the collector runs).
+ std::unordered_map<ImageViewId, boost::container::small_vector<std::pair<size_t, TICEntry>, 2>>
+ image_view_descriptors;
+
/// Remove framebuffers using the given image views from the cache
void RemoveFramebuffers(std::span<const ImageViewId> removed_views);
@@ -552,6 +584,25 @@ private:
static constexpr size_t TICKS_TO_DESTROY = 8;
#endif
DelayedDestructionRing<Image, TICKS_TO_DESTROY> sentenced_images;
+ /// Device memory of the images in each slot of sentenced_images (same index): deleted, but
+ /// counted in the driver's usage until the ring destroys them.
+ std::array<u64, TICKS_TO_DESTROY> sentenced_image_bytes{};
+ size_t sentenced_image_index = 0;
+ /// Deleted images on runtimes that report GPU progress (CurrentTick, IsFree): destroyed, with
+ /// their device memory, as soon as the GPU is past them rather than TICKS_TO_DESTROY frames
+ /// later. While the collector deletes dozens a frame, eight frames of them held 1-1.5 GB of
+ /// VRAM above its threshold (title 1 on an 8 GB card: 6.6-7.0 GB in use against 5 GiB).
+ struct GpuSentencedImage {
+ u64 tick;
+ u64 bytes;
+ Image image;
+ };
+ std::deque<GpuSentencedImage> gpu_sentenced_images;
+ u64 gpu_sentenced_bytes{};
+
+ /// Bytes of images per device memory block, for the collector (refreshed every 60 frames).
+ std::unordered_map<u64, u64> block_image_bytes;
+ u64 block_image_bytes_frame{~0ULL};
DelayedDestructionRing<ImageView, TICKS_TO_DESTROY> sentenced_image_view;
DelayedDestructionRing<Framebuffer, TICKS_TO_DESTROY> sentenced_framebuffers;
@@ -566,6 +617,8 @@ private:
Common::ThreadWorker texture_decode_worker{1, "TextureDecoder", {},
Common::ThreadPlacement::Efficiency};
std::vector<std::unique_ptr<AsyncDecodeContext>> async_decodes;
+ /// Guest memory images were uploaded from since the last cold hint (device address, size).
+ std::vector<std::pair<DAddr, u64>> uploaded_ranges;
std::deque<PendingUnswizzle> unswizzle_queue;
u8 current_unswizzle_frame;
diff --git a/src/video_core/texture_cache/types.h b/src/video_core/texture_cache/types.h
index ecacd8c6bd..9e541d6afc 100644
--- a/src/video_core/texture_cache/types.h
+++ b/src/video_core/texture_cache/types.h
@@ -132,6 +132,15 @@ struct ImageCopy {
Extent3D extent;
};
+/// A rectangle of one mip level, in texels.
+struct LevelRect {
+ s32 level;
+ u32 x;
+ u32 y;
+ u32 width;
+ u32 height;
+};
+
struct BufferImageCopy {
size_t buffer_offset;
size_t buffer_size;
diff --git a/src/video_core/texture_cache/util.cpp b/src/video_core/texture_cache/util.cpp
index ef34019a08..0e1acba813 100644
--- a/src/video_core/texture_cache/util.cpp
+++ b/src/video_core/texture_cache/util.cpp
@@ -929,6 +929,210 @@ boost::container::small_vector<BufferImageCopy, 16> UnswizzleImage(Tegra::Memory
return copies;
}
+namespace {
+// One level of a single-layer 2D block-linear image of 1x1-texel blocks, laid out as
+// UnswizzleImage reads it: rows of blocks, each block one GOB wide and 2^block_height GOBs tall.
+struct BlockLinearLevel {
+ u32 offset; ///< Into the image
+ u32 width; ///< Texels
+ u32 height;
+ u32 block_height; ///< Log2 of the GOBs a block is tall
+ u32 block_texels; ///< Texels across a block
+ u32 block_rows; ///< Texel rows in a block
+ u32 block_bytes;
+ u32 blocks_per_row;
+};
+
+std::vector<BlockLinearLevel> BlockLinearLevels(const ImageInfo& info) {
+ const LevelInfo level_info = MakeLevelInfo(info);
+ const u32 bpp_log2 = BytesPerBlockLog2(info.format);
+ const Extent2D gob = GobSize(bpp_log2, info.block.height, info.tile_width_spacing);
+ std::vector<BlockLinearLevel> levels;
+ u32 offset = 0;
+ for (s32 level = 0; level < info.resources.levels; ++level) {
+ const Extent3D level_size = AdjustMipSize(info.size, level);
+ const Extent3D num_tiles = AdjustTileSize(level_size, level_info.tile_size);
+ const Extent3D block =
+ AdjustMipBlockSize(num_tiles, level_info.block, level, level_info.num_levels);
+ const u32 stride_alignment = StrideAlignment(num_tiles, info.block, gob, bpp_log2);
+ const u32 stride = Common::AlignUpLog2(num_tiles.width, stride_alignment) << bpp_log2;
+ levels.push_back({
+ .offset = offset,
+ .width = level_size.width,
+ .height = level_size.height,
+ .block_height = block.height,
+ .block_texels = GOB_SIZE_X >> bpp_log2,
+ .block_rows = GOB_SIZE_Y << block.height,
+ .block_bytes = GOB_SIZE << block.height,
+ .blocks_per_row = Common::DivCeilLog2(stride, GOB_SIZE_X_SHIFT),
+ });
+ offset += CalculateLevelSize(level_info, level);
+ }
+ return levels;
+}
+
+// Rows of blocks a rectangle covers, and its blocks across.
+struct RectBlocks {
+ u32 first_row;
+ u32 last_row;
+ u32 first_block;
+ u32 num_blocks;
+};
+
+RectBlocks BlocksOf(const BlockLinearLevel& level, const LevelRect& rect) {
+ const u32 first_block = rect.x / level.block_texels;
+ return {
+ .first_row = rect.y / level.block_rows,
+ .last_row = (rect.y + rect.height - 1) / level.block_rows,
+ .first_block = first_block,
+ .num_blocks = (std::min)(Common::DivCeil(rect.width, level.block_texels),
+ level.blocks_per_row - first_block),
+ };
+}
+
+u32 BlockRowTexelRows(const BlockLinearLevel& level, u32 row) {
+ return (std::min)(level.block_rows, level.height - row * level.block_rows);
+}
+} // Anonymous namespace
+
+std::vector<LevelRect> SparseTileRects(const ImageInfo& info,
+ std::span<const std::pair<u64, u64>> ranges, u32 tile_width,
+ u32 tile_height, s32 tail_level) {
+ std::vector<LevelRect> rects;
+ const std::vector<BlockLinearLevel> levels = BlockLinearLevels(info);
+ const u64 image_end = CalculateLevelBytes(
+ CalculateLevelSizes(MakeLevelInfo(info), info.resources.levels), info.resources.levels);
+ for (s32 index = 0; index < static_cast<s32>(levels.size()); ++index) {
+ const BlockLinearLevel& level = levels[index];
+ const u64 level_end =
+ index + 1 < static_cast<s32>(levels.size()) ? levels[index + 1].offset : image_end;
+ const auto overlaps = [&](const std::pair<u64, u64>& range) {
+ return range.first < level_end && range.first + range.second > level.offset;
+ };
+ if (index >= tail_level) {
+ if (std::ranges::any_of(ranges, overlaps)) {
+ rects.push_back({index, 0, 0, level.width, level.height});
+ }
+ continue;
+ }
+ const u32 tiles_x = Common::DivCeil(level.width, tile_width);
+ const u32 tiles_y = Common::DivCeil(level.height, tile_height);
+ std::vector<bool> touched(size_t{tiles_x} * tiles_y);
+ for (const auto& range : ranges) {
+ if (!overlaps(range)) {
+ continue;
+ }
+ const u64 begin = (std::max<u64>)(range.first, level.offset) - level.offset;
+ const u64 end = (std::min<u64>)(range.first + range.second, level_end) - level.offset;
+ const u64 first = begin / level.block_bytes;
+ const u64 last = (end - 1) / level.block_bytes;
+ for (u64 row = first / level.blocks_per_row; row <= last / level.blocks_per_row; ++row) {
+ const u64 row_start = row * level.blocks_per_row;
+ const u64 low = (std::max)(first, row_start) - row_start;
+ const u64 high = (std::min)(last, row_start + level.blocks_per_row - 1) - row_start;
+ const u64 x0 = low * level.block_texels;
+ const u64 y0 = row * level.block_rows;
+ if (x0 >= level.width || y0 >= level.height) {
+ continue;
+ }
+ const u64 x1 = (std::min<u64>)((high + 1) * level.block_texels, level.width);
+ const u64 y1 = (std::min<u64>)(y0 + level.block_rows, level.height);
+ for (u64 ty = y0 / tile_height; ty <= (y1 - 1) / tile_height; ++ty) {
+ for (u64 tx = x0 / tile_width; tx <= (x1 - 1) / tile_width; ++tx) {
+ touched[ty * tiles_x + tx] = true;
+ }
+ }
+ }
+ }
+ for (u32 ty = 0; ty < tiles_y; ++ty) {
+ for (u32 tx = 0; tx < tiles_x;) {
+ if (!touched[size_t{ty} * tiles_x + tx]) {
+ ++tx;
+ continue;
+ }
+ const u32 first_tile = tx;
+ while (tx < tiles_x && touched[size_t{ty} * tiles_x + tx]) {
+ ++tx;
+ }
+ const u32 x = first_tile * tile_width;
+ const u32 y = ty * tile_height;
+ rects.push_back({
+ .level = index,
+ .x = x,
+ .y = y,
+ .width = (std::min)(tx * tile_width, level.width) - x,
+ .height = (std::min)(y + tile_height, level.height) - y,
+ });
+ }
+ }
+ }
+ return rects;
+}
+
+size_t UnswizzledRectsBytes(const ImageInfo& info, std::span<const LevelRect> rects) {
+ const std::vector<BlockLinearLevel> levels = BlockLinearLevels(info);
+ const u32 bpp_log2 = BytesPerBlockLog2(info.format);
+ size_t bytes = 0;
+ for (const LevelRect& rect : rects) {
+ const BlockLinearLevel& level = levels[rect.level];
+ const RectBlocks blocks = BlocksOf(level, rect);
+ for (u32 row = blocks.first_row; row <= blocks.last_row; ++row) {
+ bytes += (size_t{blocks.num_blocks * level.block_texels} << bpp_log2) *
+ BlockRowTexelRows(level, row);
+ }
+ }
+ return bytes;
+}
+
+std::vector<BufferImageCopy> UnswizzleImageRects(Tegra::MemoryManager& gpu_memory,
+ GPUVAddr gpu_addr, const ImageInfo& info,
+ std::span<const LevelRect> rects,
+ std::span<u8> output) {
+ const std::vector<BlockLinearLevel> levels = BlockLinearLevels(info);
+ const u32 bpp_log2 = BytesPerBlockLog2(info.format);
+ std::vector<BufferImageCopy> copies;
+ std::vector<u8> swizzled;
+ size_t output_offset = 0;
+ for (const LevelRect& rect : rects) {
+ const BlockLinearLevel& level = levels[rect.level];
+ const RectBlocks blocks = BlocksOf(level, rect);
+ const u32 width = blocks.num_blocks * level.block_texels;
+ const size_t row_bytes = size_t{blocks.num_blocks} * level.block_bytes;
+ for (u32 row = blocks.first_row; row <= blocks.last_row; ++row) {
+ // A row of whole blocks is laid out like an image of that width on its own.
+ const u32 texel_rows = BlockRowTexelRows(level, row);
+ const size_t bytes = (size_t{width} << bpp_log2) * texel_rows;
+ const GPUVAddr row_addr = gpu_addr + level.offset +
+ u64{row} * level.blocks_per_row * level.block_bytes +
+ u64{blocks.first_block} * level.block_bytes;
+ swizzled.assign(row_bytes, 0);
+ gpu_memory.ReadBlockUnsafe(row_addr, swizzled.data(), row_bytes);
+ UnswizzleTexture(output.subspan(output_offset, bytes), swizzled, 1U << bpp_log2, width,
+ texel_rows, 1, level.block_height, 0, 0,
+ gpu_memory.UsesNativeTiling());
+ const u32 row_y = row * level.block_rows;
+ const u32 y0 = (std::max)(rect.y, row_y);
+ const u32 y1 = (std::min)(rect.y + rect.height, row_y + texel_rows);
+ copies.push_back(BufferImageCopy{
+ .buffer_offset = output_offset + ((size_t{y0 - row_y} * width) << bpp_log2),
+ .buffer_size = bytes,
+ .buffer_row_length = width,
+ .buffer_image_height = texel_rows,
+ .image_subresource =
+ {
+ .base_level = rect.level,
+ .base_layer = 0,
+ .num_layers = 1,
+ },
+ .image_offset = {static_cast<s32>(rect.x), static_cast<s32>(y0), 0},
+ .image_extent = {rect.width, y1 - y0, 1},
+ });
+ output_offset += bytes;
+ }
+ }
+ return copies;
+}
+
void ConvertedLayout(const ImageInfo& info, std::span<BufferImageCopy> copies) {
// Same offsets and sizes as ConvertImage below.
const auto recompression = Settings::GetAstcRecompression();
diff --git a/src/video_core/texture_cache/util.h b/src/video_core/texture_cache/util.h
index 0381d2d24b..74da0f90f9 100644
--- a/src/video_core/texture_cache/util.h
+++ b/src/video_core/texture_cache/util.h
@@ -8,6 +8,8 @@
#include <optional>
#include <span>
+#include <utility>
+#include <vector>
#include <boost/container/small_vector.hpp>
#include "common/common_types.h"
@@ -72,6 +74,25 @@ struct OverlapResult {
Tegra::MemoryManager& gpu_memory, GPUVAddr gpu_addr, const ImageInfo& info,
std::span<const u8> input, std::span<u8> output);
+/// For a single-layer 2D block-linear image of 1x1-texel blocks: the host sparse blocks
+/// (tile_width x tile_height texels) that the guest byte ranges (offset, size) touch, merged into
+/// runs along x. Levels from tail_level on (the host's mip tail) come whole.
+[[nodiscard]] std::vector<LevelRect> SparseTileRects(const ImageInfo& info,
+ std::span<const std::pair<u64, u64>> ranges,
+ u32 tile_width, u32 tile_height,
+ s32 tail_level);
+
+/// Bytes UnswizzleImageRects writes for `rects`.
+[[nodiscard]] size_t UnswizzledRectsBytes(const ImageInfo& info, std::span<const LevelRect> rects);
+
+/// Unswizzles `rects` (from SparseTileRects: on GOB columns) of a single-layer 2D block-linear
+/// image into output, reading only the guest blocks under them.
+[[nodiscard]] std::vector<BufferImageCopy> UnswizzleImageRects(Tegra::MemoryManager& gpu_memory,
+ GPUVAddr gpu_addr,
+ const ImageInfo& info,
+ std::span<const LevelRect> rects,
+ std::span<u8> output);
+
/// Rewrites `copies` (from UnswizzleImage) to the layout ConvertImage produces, without converting.
void ConvertedLayout(const ImageInfo& info, std::span<BufferImageCopy> copies);
diff --git a/src/video_core/textures/astc.cpp b/src/video_core/textures/astc.cpp
index 950ce28d6b..a0d7e6e455 100644
--- a/src/video_core/textures/astc.cpp
+++ b/src/video_core/textures/astc.cpp
@@ -2038,38 +2038,74 @@ void Decompress(std::span<const uint8_t> data, uint32_t width, uint32_t height,
}
}
+// What IsOpaque needs from a block's 11 mode bits, which are all DecodeBlockInfo reads (a void
+// extent block also needs bits 10 and 11 set, checked per block).
+struct BlockModeInfo {
+ bool error;
+ bool void_extent;
+ bool void_extent_hdr;
+ bool dual_plane;
+ u16 weight_bits;
+};
+
+static std::array<BlockModeInfo, 2048> MakeBlockModeInfos() {
+ std::array<BlockModeInfo, 2048> infos{};
+ for (u32 mode = 0; mode < infos.size(); ++mode) {
+ // Bit 11 set, so a void extent mode decodes without its reserved-bit error.
+ const std::array<u8, 16> block{static_cast<u8>(mode), static_cast<u8>((mode >> 8) | 0x8)};
+ InputBitStream strm(block);
+ const TexelWeightParams params = DecodeBlockInfo(strm);
+ BlockModeInfo& info = infos[mode];
+ info.void_extent = params.m_bVoidExtentLDR || params.m_bVoidExtentHDR;
+ info.void_extent_hdr = params.m_bVoidExtentHDR;
+ info.error = params.m_bError && !info.void_extent;
+ info.dual_plane = params.m_bDualPlane;
+ info.weight_bits = info.error || info.void_extent
+ ? u16{0}
+ : static_cast<u16>(params.GetPackedBitSize());
+ }
+ return infos;
+}
+
bool IsOpaque(std::span<const uint8_t> data) {
// Endpoint modes with an alpha channel: LDR luminance+alpha (4, 5), RGB scale + two alpha
// (10), LDR RGBA (12, 13) and HDR RGBA (14, 15).
constexpr u32 AlphaModes = (1U << 4) | (1U << 5) | (1U << 10) | (1U << 12) | (1U << 13) |
(1U << 14) | (1U << 15);
+ // Decoding each block's mode bit by bit took ~12% of title 1's long frames, where every
+ // new ASTC texture is scanned (its title screen streams them in by the thousand).
+ static const std::array<BlockModeInfo, 2048> mode_infos = MakeBlockModeInfos();
for (size_t offset = 0; offset + 16 <= data.size(); offset += 16) {
const std::span<const u8> block = data.subspan(offset, 16);
- InputBitStream strm(block);
- const TexelWeightParams params = DecodeBlockInfo(strm);
- if (params.m_bError) {
+ u32 low;
+ std::memcpy(&low, block.data(), sizeof(low));
+ const BlockModeInfo& info = mode_infos[low & 0x7FF];
+ if (info.error) {
continue; // decodes to the opaque error colour
}
- if (params.m_bVoidExtentLDR || params.m_bVoidExtentHDR) {
+ if (info.void_extent) {
+ if ((low & 0xC00) != 0xC00) {
+ continue; // reserved bits clear: the error colour too
+ }
// One RGBA16 colour in bits 64-127: UNORM16, or FP16 for HDR.
const u32 alpha = block[14] | (u32{block[15]} << 8);
- if (alpha != (params.m_bVoidExtentHDR ? 0x3C00U : 0xFFFFU)) {
+ if (alpha != (info.void_extent_hdr ? 0x3C00U : 0xFFFFU)) {
return false;
}
continue;
}
- const u32 partitions = strm.ReadBits<2>() + 1;
- if (partitions == 4 && params.m_bDualPlane) {
+ const u32 partitions = ((low >> 11) & 3) + 1;
+ if (partitions == 4 && info.dual_plane) {
continue;
}
if (partitions == 1) {
- if ((AlphaModes >> strm.ReadBits<4>()) & 1) {
+ if ((AlphaModes >> ((low >> 13) & 0xF)) & 1) {
return false;
}
continue;
}
- strm.ReadBits<10>(); // partition index
- const u32 base_cem = strm.ReadBits<6>();
+ // Bits 13-22 are the partition index.
+ const u32 base_cem = (low >> 23) & 0x3F;
const u32 base_mode = base_cem & 3;
if (base_mode == 0) {
if ((AlphaModes >> (base_cem >> 2)) & 1) {
@@ -2078,7 +2114,7 @@ bool IsOpaque(std::span<const uint8_t> data) {
continue;
}
// The rest of the per-partition modes sit just below the weights (DecompressBlock).
- const u32 weight_bits = params.GetPackedBitSize();
+ const u32 weight_bits = info.weight_bits;
if (weight_bits < 24 || weight_bits > 96) {
continue;
}
diff --git a/src/video_core/textures/texture.h b/src/video_core/textures/texture.h
index 9fc6a631bd..35a4c8eecf 100644
--- a/src/video_core/textures/texture.h
+++ b/src/video_core/textures/texture.h
@@ -165,6 +165,17 @@ static_assert(sizeof(TextureHandle) == 4, "TextureHandle has wrong size");
}
}
+/// Some Switch 2 bindless handles (Switch Sports' scenery, read from a uniform register) hold
+/// the texture header's byte offset in the pool, index * 32, where the handle normally has the
+/// index. Read it as an offset only on Switch 2 (native tiling), when the plain index falls past
+/// the pool and the offset lands inside it, so handles that already resolve are left alone.
+[[nodiscard]] constexpr u32 TextureHeaderIndex(u32 index, u32 limit, bool switch2) {
+ if (switch2 && index > limit && index % 32 == 0 && index / 32 <= limit) {
+ return index / 32;
+ }
+ return index;
+}
+
struct TICEntry {
union {
struct {
diff --git a/src/video_core/vulkan_common/vulkan_device.cpp b/src/video_core/vulkan_common/vulkan_device.cpp
index 623ab7c30d..01e13cb384 100644
--- a/src/video_core/vulkan_common/vulkan_device.cpp
+++ b/src/video_core/vulkan_common/vulkan_device.cpp
@@ -620,12 +620,16 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
}
sets_per_pool = 64;
+ // AMD GCN4 and older are told apart by their lack of float16, read before float16 is turned
+ // off for newer AMD drivers below: checked after that, every AMD GPU looked like GCN4 and lost
+ // VK_EXT_sampler_filter_minmax (an RDNA3 Radeon 780M logged it).
+ const bool amd_gcn4_or_older = is_amd && !features.shader_float16_int8.shaderFloat16;
if (is_amd_driver) {
// AMD drivers need a higher amount of Sets per Pool in certain circumstances like in XC2.
sets_per_pool = 96;
// Disable VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT on AMD GCN4 and lower as it is broken.
- if (!features.shader_float16_int8.shaderFloat16) {
+ if (amd_gcn4_or_older) {
LOG_WARNING(Render_Vulkan,
"AMD GCN4 and earlier have broken VK_IMAGE_CREATE_CUBE_COMPATIBLE_BIT");
has_broken_cube_compatibility = true;
@@ -641,7 +645,7 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
if (extensions.sampler_filter_minmax && is_amd) {
// Disable ext_sampler_filter_minmax on AMD GCN4 and lower as it is broken.
- if (!features.shader_float16_int8.shaderFloat16) {
+ if (amd_gcn4_or_older) {
LOG_WARNING(Render_Vulkan,
"AMD GCN4 and earlier have broken VK_EXT_sampler_filter_minmax");
RemoveExtension(extensions.sampler_filter_minmax,
@@ -775,10 +779,14 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
.flags = flags,
.physicalDevice = physical,
.device = *logical,
- // 64 MiB blocks on discrete GPUs too: with 256 MiB ones, Switch 2 titles' mix of
+ // 16 MiB blocks on discrete GPUs too: with 256 MiB ones, Switch 2 titles' mix of
// texture sizes left ~38% of the image blocks empty (2 GB of an 8 GB card in title
- // 1), which the texture cache counted as used and kept evicting textures for.
- .preferredLargeHeapBlockSize = 64u * 1024u * 1024u,
+ // 1), which the texture cache counted as used and kept evicting textures for. With
+ // 64 MiB ones textures created and deleted by the thousand still left ~1.7 GB of holes
+ // after ten minutes, with 32 MiB ones ~0.5 GB: a block drains only once all of its
+ // images are gone, and fewer per block drain sooner. VMA gives allocations over half a
+ // block (8 MiB) their own memory, which goes back to the driver when they are freed.
+ .preferredLargeHeapBlockSize = 16u * 1024u * 1024u,
.pAllocationCallbacks = nullptr,
.pDeviceMemoryCallbacks = nullptr,
.pHeapSizeLimit = heap_size_limit != 0 ? heap_size_limits.data() : nullptr,
@@ -800,9 +808,37 @@ Device::Device(VkInstance instance_, vk::PhysicalDevice physical_, VkSurfaceKHR
Device::~Device() {
SaveStaticPipelineCache();
ShutdownGPULogging();
+ for (auto& pools : separate_pools) {
+ for (VmaPool pool : pools) {
+ if (pool != VK_NULL_HANDLE) {
+ vmaDestroyPool(allocator, pool);
+ }
+ }
+ }
vmaDestroyAllocator(allocator);
}
+VmaPool Device::GetSeparatePool(SeparatePool kind, u32 memory_type) const {
+ std::scoped_lock lk{separate_pools_mutex};
+ VmaPool& pool = separate_pools[static_cast<u32>(kind)][memory_type];
+ if (pool == VK_NULL_HANDLE) {
+ const VmaPoolCreateInfo ci{
+ .memoryTypeIndex = memory_type,
+ .flags = 0,
+ .blockSize = SeparatePoolBlockSize,
+ .minBlockCount = 0,
+ .maxBlockCount = 0,
+ .priority = 0.f,
+ .minAllocationAlignment = 0,
+ .pMemoryAllocateNext = nullptr,
+ };
+ if (vmaCreatePool(allocator, &ci, &pool) != VK_SUCCESS) {
+ pool = VK_NULL_HANDLE;
+ }
+ }
+ return pool;
+}
+
void Device::LoadStaticPipelineCache() {
const auto create = [this](size_t size, const void* data) {
static_pipeline_cache = logical.CreatePipelineCache({
@@ -1511,6 +1547,15 @@ void Device::RemoveUnsuitableExtensions() {
VK_KHR_PIPELINE_EXECUTABLE_PROPERTIES_EXTENSION_NAME);
}
+ // VK_EXT_mesh_shader: Switch 2 titles draw some geometry with task and mesh programs. Only
+ // the two stages are used; the multiview, shading rate and query features need more.
+ features.mesh_shader.multiviewMeshShader = VK_FALSE;
+ features.mesh_shader.primitiveFragmentShadingRateMeshShader = VK_FALSE;
+ features.mesh_shader.meshShaderQueries = VK_FALSE;
+ extensions.mesh_shader = features.mesh_shader.taskShader && features.mesh_shader.meshShader;
+ RemoveExtensionFeatureIfUnsuitable(extensions.mesh_shader, features.mesh_shader,
+ VK_EXT_MESH_SHADER_EXTENSION_NAME);
+
// VK_KHR_shader_quad_control
extensions.shader_quad_control = features.shader_quad_control.shaderQuadControl;
RemoveExtensionFeatureIfUnsuitable(extensions.shader_quad_control, features.shader_quad_control,
@@ -1639,6 +1684,16 @@ u64 Device::GetDeviceMemoryUsage() const {
return result;
}
+u64 Device::GetDeviceMemorySlack() const {
+ if (allocator == nullptr) {
+ return 0;
+ }
+ std::array<VmaBudget, VK_MAX_MEMORY_HEAPS> heap_budgets{};
+ vmaGetHeapBudgets(allocator, heap_budgets.data());
+ const VmaStatistics& statistics = heap_budgets[main_heap].statistics;
+ return statistics.blockBytes - (std::min)(statistics.blockBytes, statistics.allocationBytes);
+}
+
u64 Device::GetDeviceMemoryBudget() const {
if (!extensions.memory_budget) {
return 0;
@@ -1663,6 +1718,13 @@ u64 Device::GetDeviceMemoryBudget() const {
result += AllocatedUsage(budget.heapUsage[heap], heap_budgets[heap]);
}
}
+ if (is_integrated) {
+ // An integrated GPU's device-local heap is system memory (a Radeon 780M reported a 21 GB
+ // budget): following it let the texture cache fill that much RAM before collecting, and
+ // kept ASTC uncompressed. Its budget is at most the share CollectPhysicalMemoryInfo
+ // gives it (4 GiB, 6 GiB with aggressive VRAM use).
+ result = (std::min)(result, device_access_memory);
+ }
return result;
}
diff --git a/src/video_core/vulkan_common/vulkan_device.h b/src/video_core/vulkan_common/vulkan_device.h
index d8524f6575..5f75bcc18e 100644
--- a/src/video_core/vulkan_common/vulkan_device.h
+++ b/src/video_core/vulkan_common/vulkan_device.h
@@ -6,7 +6,10 @@
#pragma once
+#include <array>
#include <atomic>
+#include <cstdlib>
+#include <mutex>
#include <optional>
#include <set>
#include <span>
@@ -20,6 +23,7 @@
#include "video_core/vulkan_common/vulkan_wrapper.h"
VK_DEFINE_HANDLE(VmaAllocator)
+VK_DEFINE_HANDLE(VmaPool)
// Define all features which may be used by the implementation here.
// Vulkan version in the macro describes the minimum version required for feature availability.
@@ -65,6 +69,7 @@ VK_DEFINE_HANDLE(VmaAllocator)
FEATURE(EXT, 4444Formats, 4444_FORMATS, format_a4b4g4r4) \
FEATURE(EXT, IndexTypeUint8, INDEX_TYPE_UINT8, index_type_uint8) \
FEATURE(EXT, LineRasterization, LINE_RASTERIZATION, line_rasterization) \
+ FEATURE(EXT, MeshShader, MESH_SHADER, mesh_shader) \
FEATURE(EXT, PrimitiveTopologyListRestart, PRIMITIVE_TOPOLOGY_LIST_RESTART, \
primitive_topology_list_restart) \
FEATURE(EXT, ProvokingVertex, PROVOKING_VERTEX, provoking_vertex) \
@@ -263,6 +268,17 @@ public:
return allocator;
}
+ /// Allocations kept out of the default blocks, which then hold images only: churned buffers
+ /// and long-lived sparse pages left holes the texture cache's collector couldn't drain.
+ enum class SeparatePool : u32 {
+ SparsePages,
+ DeviceBuffers,
+ };
+ static constexpr VkDeviceSize SeparatePoolBlockSize = 16ULL << 20;
+
+ /// The pool of that kind for a memory type, created on first use (null if that failed).
+ VmaPool GetSeparatePool(SeparatePool kind, u32 memory_type) const;
+
/// Returns the logical device.
const vk::Device& GetLogical() const {
return logical;
@@ -326,6 +342,28 @@ public:
return IsSparseBindingSupported() && features.features.sparseResidencyImage2D;
}
+ /// Whether a sparse image may bind the same memory to several of its blocks.
+ bool IsSparseResidencyAliasedSupported() const {
+ return IsSparseResidencyImage2DSupported() && features.features.sparseResidencyAliased;
+ }
+
+ /// Whether images with no memory bound may be sampled and used as attachments. AMD's drivers
+ /// fault on it (the GPU hangs shortly after the first such draw), so large images there are
+ /// backed normally. RADV is counted in until someone shows it doesn't need it.
+ /// EDEN_NX2_SPARSE_UNBACKED=1 allows unbound images on any driver, =0 on none.
+ bool CanUseUnbackedSparseImages() const {
+ static const char forced = [] {
+ const char* value = std::getenv("EDEN_NX2_SPARSE_UNBACKED");
+ return value == nullptr ? '\0' : value[0];
+ }();
+ if (forced == '0' || forced == '1') {
+ return forced == '1';
+ }
+ const VkDriverIdKHR id = GetDriverID();
+ return id != VK_DRIVER_ID_AMD_PROPRIETARY && id != VK_DRIVER_ID_AMD_OPEN_SOURCE &&
+ id != VK_DRIVER_ID_MESA_RADV;
+ }
+
/// Returns true for tile-based deferred renderers.
bool IsTiler() const {
switch (GetDriverID()) {
@@ -610,6 +648,12 @@ FN_MAX_LIMIT_LIST
return extensions.shader_quad_control && features.shader_quad_control.shaderQuadControl;
}
+ /// Returns true if the device supports task and mesh shaders (VK_EXT_mesh_shader).
+ bool IsExtMeshShaderSupported() const {
+ return extensions.mesh_shader && features.mesh_shader.taskShader &&
+ features.mesh_shader.meshShader;
+ }
+
/// Returns true if the device supports VK_KHR_image_format_list.
bool IsKhrImageFormatListSupported() const {
return extensions.image_format_list || instance_version >= VK_API_VERSION_1_2;
@@ -928,6 +972,11 @@ FN_MAX_LIMIT_LIST
return 0x00010300U;
}
+ /// Returns true for an integrated GPU (its device-local memory is system memory).
+ bool IsIntegrated() const {
+ return is_integrated;
+ }
+
/// Returns true when a known debugging tool is attached.
bool HasDebuggingToolAttached() const {
return has_renderdoc || has_nsight_graphics || has_radeon_gpu_profiler;
@@ -970,9 +1019,15 @@ FN_MAX_LIMIT_LIST
/// The driver's current memory budget for this process (VK_EXT_memory_budget), in
/// GetDeviceMemoryUsage() terms: the usage at which the main device-local heap reaches its
- /// budget. 0 without the extension.
+ /// budget. 0 without the extension. The unused part of the allocator's blocks is not
+ /// subtracted: see GetDeviceMemorySlack().
u64 GetDeviceMemoryBudget() const;
+ /// Memory the allocator holds from the driver in the main device-local heap but has no
+ /// allocation in (the unused part of its blocks). GetDeviceMemoryUsage() does not count it,
+ /// yet the driver does: with the heap at its budget it is not available to new images.
+ u64 GetDeviceMemorySlack() const;
+
u32 GetSetsPerPool() const {
return sets_per_pool;
}
@@ -1106,6 +1161,12 @@ FN_MAX_LIMIT_LIST
return properties.driver.driverID == VK_DRIVER_ID_MOLTENVK;
}
+ /// The multiple vertex input binding strides must be. Metal needs 4 bytes, which MoltenVK
+ /// reports as minVertexInputBindingStrideAlignment through VK_KHR_portability_subset.
+ u32 GetMinVertexInputBindingStrideAlignment() const noexcept {
+ return IsMoltenVK() ? 4 : 1;
+ }
+
NvidiaArchitecture GetNvidiaArch() const noexcept {
return nvidia_arch;
}
@@ -1156,6 +1217,8 @@ private:
private:
VkInstance instance; ///< Vulkan instance.
VmaAllocator allocator{}; ///< VMA allocator.
+ mutable std::mutex separate_pools_mutex;
+ mutable std::array<std::array<VmaPool, VK_MAX_MEMORY_TYPES>, 2> separate_pools{};
vk::DeviceDispatch dld; ///< Device function pointers.
vk::PhysicalDevice physical; ///< Physical device.
vk::Device logical; ///< Logical device.
diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
index a0c96d1b45..a0a39abe39 100644
--- a/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
+++ b/src/video_core/vulkan_common/vulkan_memory_allocator.cpp
@@ -209,7 +209,10 @@ MemoryAllocator::~MemoryAllocator() = default;
vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci, bool dedicated) const
{
const VmaAllocationCreateInfo alloc_ci = {
- .flags = static_cast<VmaAllocationCreateFlags>(VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT) |
+ // Best fit: images freed by the thousand leave holes in the blocks, which new ones
+ // should fill before VMA takes another block.
+ .flags = static_cast<VmaAllocationCreateFlags>(VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT |
+ VMA_ALLOCATION_CREATE_STRATEGY_MIN_MEMORY_BIT) |
(dedicated ? VmaAllocationCreateFlags{VMA_ALLOCATION_CREATE_DEDICATED_MEMORY_BIT}
: VmaAllocationCreateFlags{0}),
.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE,
@@ -242,11 +245,32 @@ vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci, bool dedicat
ci.extent.width, ci.extent.height, ci.extent.depth, ci.mipLevels,
ci.arrayLayers, static_cast<u32>(ci.format));
}
+ pressure_events.fetch_add(1, std::memory_order_relaxed);
VmaAllocationCreateInfo relaxed = alloc_ci;
relaxed.flags &= ~VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
result = vmaCreateImage(allocator, &ci, &relaxed, &handle, &allocation, &alloc_info);
}
+ if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY && !huge) {
+ // Still out of device memory: a real image in host memory (slow, but valid) beats one
+ // with nothing bound, which AMD's drivers do not survive being sampled.
+ VmaAllocationCreateInfo host = alloc_ci;
+ host.flags &= ~VMA_ALLOCATION_CREATE_WITHIN_BUDGET_BIT;
+ host.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST;
+ host.preferredFlags = 0;
+ result = vmaCreateImage(allocator, &ci, &host, &handle, &allocation, &alloc_info);
+ static u32 host_logged{};
+ if (result == VK_SUCCESS && host_logged++ < 16) {
+ LOG_WARNING(Render_Vulkan,
+ "Image {}x{}x{} ({} levels, {} layers, format {}) is out of device memory; "
+ "placed in host memory",
+ ci.extent.width, ci.extent.height, ci.extent.depth, ci.mipLevels,
+ ci.arrayLayers, static_cast<u32>(ci.format));
+ }
+ }
if (result == VK_ERROR_OUT_OF_DEVICE_MEMORY && ci.imageType != VK_IMAGE_TYPE_1D) {
+ if (!huge) {
+ pressure_events.fetch_add(1, std::memory_order_relaxed);
+ }
// Too big to back at all, e.g. title 1's sparse virtual texture arrays (8192x8192,
// 68 layers, ~23 GB): on the console only resident tiles have memory. Use an image with
// nothing bound, so views and copies still work, instead of aborting.
@@ -280,12 +304,15 @@ vk::Image MemoryAllocator::CreateImage(const VkImageCreateInfo &ci, bool dedicat
device.GetDispatchLoader());
}
-vk::Image MemoryAllocator::CreateUnbackedImage(const VkImageCreateInfo &ci) const {
+vk::Image MemoryAllocator::CreateUnbackedImage(const VkImageCreateInfo &ci, bool aliased) const {
if (ci.imageType == VK_IMAGE_TYPE_1D) {
return vk::Image{};
}
VkImageCreateInfo sparse_ci = ci;
sparse_ci.flags |= VK_IMAGE_CREATE_SPARSE_BINDING_BIT | VK_IMAGE_CREATE_SPARSE_RESIDENCY_BIT;
+ if (aliased) {
+ sparse_ci.flags |= VK_IMAGE_CREATE_SPARSE_ALIASED_BIT;
+ }
VkImage sparse{};
if (device.GetDispatchLoader().vkCreateImage(*device.GetLogical(), &sparse_ci, nullptr,
&sparse) != VK_SUCCESS) {
@@ -310,6 +337,24 @@ VmaAllocation MemoryAllocator::AllocateSparseMemory(const VkMemoryRequirements &
};
VmaAllocation allocation{};
VmaAllocationInfo info{};
+ // In a pool of their own (see Device::SeparatePool); anything over half a block, or a pool
+ // allocation that fails, takes the default blocks.
+ u32 memory_type{};
+ if (requirements.size <= Device::SeparatePoolBlockSize / 2 &&
+ vmaFindMemoryTypeIndex(allocator, requirements.memoryTypeBits, &alloc_ci, &memory_type) ==
+ VK_SUCCESS) {
+ if (const VmaPool pool =
+ device.GetSeparatePool(Device::SeparatePool::SparsePages, memory_type)) {
+ VmaAllocationCreateInfo pool_ci = alloc_ci;
+ pool_ci.pool = pool;
+ if (vmaAllocateMemory(allocator, &requirements, &pool_ci, &allocation, &info) ==
+ VK_SUCCESS) {
+ memory = info.deviceMemory;
+ offset = info.offset;
+ return allocation;
+ }
+ }
+ }
if (vmaAllocateMemory(allocator, &requirements, &alloc_ci, &allocation, &info) != VK_SUCCESS) {
return VK_NULL_HANDLE;
}
@@ -324,6 +369,39 @@ void MemoryAllocator::FreeSparseMemory(VmaAllocation allocation) const {
}
}
+bool MemoryAllocator::CreateBufferInSeparatePool(const VkBufferCreateInfo& ci, MemoryUsage usage,
+ const VmaAllocationCreateInfo& alloc_ci,
+ VkBuffer& handle, VmaAllocation& allocation,
+ VmaAllocationInfo& alloc_info) const {
+ // The buffer cache's buffers, churned by its collector: in a pool of their own (see
+ // Device::SeparatePool). Anything over half a block takes the default blocks (dedicated).
+ if (usage != MemoryUsage::DeviceLocal || ci.size > Device::SeparatePoolBlockSize / 2) {
+ return false;
+ }
+ u32 memory_type{};
+ {
+ std::scoped_lock lk{buffer_type_mutex};
+ const auto key = (u64{ci.usage} << 32) | ci.flags;
+ if (const auto it = buffer_memory_types.find(key); it != buffer_memory_types.end()) {
+ memory_type = it->second;
+ } else if (vmaFindMemoryTypeIndexForBufferInfo(allocator, &ci, &alloc_ci, &memory_type) ==
+ VK_SUCCESS) {
+ buffer_memory_types.emplace(key, memory_type);
+ } else {
+ return false;
+ }
+ }
+ const VmaPool pool = device.GetSeparatePool(Device::SeparatePool::DeviceBuffers, memory_type);
+ if (pool == VK_NULL_HANDLE) {
+ return false;
+ }
+ VmaAllocationCreateInfo pool_ci = alloc_ci;
+ pool_ci.pool = pool;
+ pool_ci.memoryTypeBits = 0;
+ return vmaCreateBuffer(allocator, &ci, &pool_ci, &handle, &allocation, &alloc_info) ==
+ VK_SUCCESS;
+}
+
vk::Buffer MemoryAllocator::CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const {
// MESA will do memcpy() if not marked as host cached, so just force mark it for most buffers
auto const anv_flags = (usage == MemoryUsage::Stream
@@ -345,7 +423,9 @@ vk::Buffer MemoryAllocator::CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsa
VmaAllocation allocation{};
VkMemoryPropertyFlags property_flags{};
- vk::Check(vmaCreateBuffer(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info));
+ if (!CreateBufferInSeparatePool(ci, usage, alloc_ci, handle, allocation, alloc_info)) {
+ vk::Check(vmaCreateBuffer(allocator, &ci, &alloc_ci, &handle, &allocation, &alloc_info));
+ }
vmaGetAllocationMemoryProperties(allocator, allocation, &property_flags);
// Log GPU memory allocation for buffers
diff --git a/src/video_core/vulkan_common/vulkan_memory_allocator.h b/src/video_core/vulkan_common/vulkan_memory_allocator.h
index f3c6c177a2..8b9e72b3e3 100644
--- a/src/video_core/vulkan_common/vulkan_memory_allocator.h
+++ b/src/video_core/vulkan_common/vulkan_memory_allocator.h
@@ -6,8 +6,11 @@
#pragma once
+#include <atomic>
#include <memory>
+#include <mutex>
#include <span>
+#include <unordered_map>
#include <vector>
#include "common/common_types.h"
@@ -109,14 +112,27 @@ namespace Vulkan {
/// A sparse resident image with nothing bound (reads return zero, writes are dropped), or
/// a null image when the device can't make one.
- vk::Image CreateUnbackedImage(const VkImageCreateInfo &ci) const;
+ /// aliased: its sparse blocks may share memory (VK_IMAGE_CREATE_SPARSE_ALIASED_BIT).
+ vk::Image CreateUnbackedImage(const VkImageCreateInfo &ci, bool aliased = false) const;
/// Device memory to bind to part of a sparse resource. Null when there is none left.
+ /// Device-local buffers in their own VMA pool; false when it doesn't apply or failed.
+ bool CreateBufferInSeparatePool(const VkBufferCreateInfo& ci, MemoryUsage usage,
+ const VmaAllocationCreateInfo& alloc_ci, VkBuffer& handle,
+ VmaAllocation& allocation,
+ VmaAllocationInfo& alloc_info) const;
+
VmaAllocation AllocateSparseMemory(const VkMemoryRequirements &requirements,
VkDeviceMemory &memory, VkDeviceSize &offset) const;
void FreeSparseMemory(VmaAllocation allocation) const;
+ /// Counts the images that did not fit the device memory budget (made anyway, in host
+ /// memory or with nothing bound): the texture cache collects harder when it grows.
+ u32 MemoryPressureEvents() const noexcept {
+ return pressure_events.load(std::memory_order_relaxed);
+ }
+
vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage) const;
vk::Buffer CreateBuffer(const VkBufferCreateInfo &ci, MemoryUsage usage,
@@ -152,6 +168,10 @@ namespace Vulkan {
const VkPhysicalDeviceMemoryProperties properties; ///< Physical device memory properties.
VkDeviceSize buffer_image_granularity; ///< Adjacent buffer/image granularity
u32 valid_memory_types{~0u};
+ mutable std::atomic<u32> pressure_events{0};
+ mutable std::mutex buffer_type_mutex;
+ /// Memory type of device-local buffers by usage and flags (CreateBufferInSeparatePool).
+ mutable std::unordered_map<u64, u32> buffer_memory_types;
};
/// Logs VMA's blocks and allocations per memory type, and each heap's budget.
diff --git a/src/video_core/vulkan_common/vulkan_wrapper.cpp b/src/video_core/vulkan_common/vulkan_wrapper.cpp
index dbaaf450a0..d0bca88125 100644
--- a/src/video_core/vulkan_common/vulkan_wrapper.cpp
+++ b/src/video_core/vulkan_common/vulkan_wrapper.cpp
@@ -116,6 +116,7 @@ void Load(VkDevice device, DeviceDispatch& dld) noexcept {
X(vkCmdDrawIndirectCount);
X(vkCmdDrawIndexedIndirectCount);
X(vkCmdDrawIndirectByteCountEXT);
+ X(vkCmdDrawMeshTasksEXT);
X(vkCmdEndConditionalRenderingEXT);
X(vkCmdEndQuery);
X(vkCmdEndRenderPass);
@@ -525,6 +526,19 @@ void Image::Release() const noexcept {
}
}
+Image::MemoryBlock Image::GetMemoryBlock() const noexcept {
+ if (allocation == nullptr) {
+ return {};
+ }
+ VmaAllocationInfo2 info{};
+ vmaGetAllocationInfo2(allocator, allocation, &info);
+ if (info.dedicatedMemory) {
+ return {0, info.blockSize, info.allocationInfo.size};
+ }
+ return {reinterpret_cast<u64>(info.allocationInfo.deviceMemory), info.blockSize,
+ info.allocationInfo.size};
+}
+
void Buffer::Flush() const {
if (!is_coherent) {
vmaFlushAllocation(allocator, allocation, 0, VK_WHOLE_SIZE);
diff --git a/src/video_core/vulkan_common/vulkan_wrapper.h b/src/video_core/vulkan_common/vulkan_wrapper.h
index 3010368973..f49e9518bb 100644
--- a/src/video_core/vulkan_common/vulkan_wrapper.h
+++ b/src/video_core/vulkan_common/vulkan_wrapper.h
@@ -232,6 +232,7 @@ struct DeviceDispatch : InstanceDispatch {
PFN_vkCmdDrawIndirectCount vkCmdDrawIndirectCount{};
PFN_vkCmdDrawIndexedIndirectCount vkCmdDrawIndexedIndirectCount{};
PFN_vkCmdDrawIndirectByteCountEXT vkCmdDrawIndirectByteCountEXT{};
+ PFN_vkCmdDrawMeshTasksEXT vkCmdDrawMeshTasksEXT{};
PFN_vkCmdEndConditionalRenderingEXT vkCmdEndConditionalRenderingEXT{};
PFN_vkCmdEndDebugUtilsLabelEXT vkCmdEndDebugUtilsLabelEXT{};
PFN_vkCmdEndQuery vkCmdEndQuery{};
@@ -736,6 +737,15 @@ public:
return usage;
}
+ /// Where the image's memory lies: the block (0 when the image has no memory or a block of
+ /// its own), the block's size and the image's part of it.
+ struct MemoryBlock {
+ u64 block;
+ u64 block_size;
+ u64 size;
+ };
+ [[nodiscard]] MemoryBlock GetMemoryBlock() const noexcept;
+
private:
void Release() const noexcept;
@@ -1319,6 +1329,10 @@ public:
dld->vkCmdDrawIndexedIndirect(handle, src_buffer, src_offset, draw_count, stride);
}
+ void DrawMeshTasksEXT(u32 group_count_x, u32 group_count_y, u32 group_count_z) const noexcept {
+ dld->vkCmdDrawMeshTasksEXT(handle, group_count_x, group_count_y, group_count_z);
+ }
+
void DrawIndirectCount(VkBuffer src_buffer, VkDeviceSize src_offset, VkBuffer count_buffer,
VkDeviceSize count_offset, u32 draw_count, u32 stride) const noexcept {
dld->vkCmdDrawIndirectCount(handle, src_buffer, src_offset, count_buffer, count_offset,
diff --git a/src/yuzu/CMakeLists.txt b/src/yuzu/CMakeLists.txt
index 40ff1d45f9..f5895edd10 100644
--- a/src/yuzu/CMakeLists.txt
+++ b/src/yuzu/CMakeLists.txt
@@ -219,6 +219,7 @@ add_executable(yuzu
yuzu.qrc
yuzu.rc
migration_dialog.h migration_dialog.cpp
+ nup_dialogs.h nup_dialogs.cpp
migration_worker.h migration_worker.cpp
libqt_common.h libqt_common.cpp
diff --git a/src/yuzu/Info.plist b/src/yuzu/Info.plist
index cf6f597e99..9b6d49b87a 100644
--- a/src/yuzu/Info.plist
+++ b/src/yuzu/Info.plist
@@ -13,7 +13,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<key>CFBundleDevelopmentRegion</key>
<string>English</string>
<key>CFBundleExecutable</key>
- <string>${EXECUTABLE_NAME}</string>
+ <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
<key>CFBundleGetInfoString</key>
<string></string>
<key>CFBundleIconFile</key>
diff --git a/src/yuzu/configuration/configure_graphics.cpp b/src/yuzu/configuration/configure_graphics.cpp
index 641f65d82a..074b6f405b 100644
--- a/src/yuzu/configuration/configure_graphics.cpp
+++ b/src/yuzu/configuration/configure_graphics.cpp
@@ -218,6 +218,7 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) {
std::map<u32, QWidget*> hold_graphics;
std::vector<QWidget*> hold_api;
+ QCheckBox* frame_gen_checkbox = nullptr;
for (const auto setting : Settings::values.linkage.by_category[Settings::Category::Renderer]) {
#ifndef HAS_RESHADE
@@ -322,6 +323,9 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) {
hold_graphics.emplace(setting->Id(), widget);
#endif
+ } else if (setting->Id() == Settings::values.frame_gen.Id()) {
+ frame_gen_checkbox = widget->checkbox;
+ hold_graphics.emplace(setting->Id(), widget);
} else {
hold_graphics.emplace(setting->Id(), widget);
}
@@ -331,6 +335,21 @@ void ConfigureGraphics::Setup(const ConfigurationShared::Builder& builder) {
graphics_layout.addWidget(widget);
}
+ // Hide rather than disable, a disabled widget would read as using the global value
+ if (frame_gen_checkbox != nullptr) {
+ for (const u32 id : {Settings::values.frame_gen_type.Id(),
+ Settings::values.frame_gen_multiplier.Id(),
+ Settings::values.frame_gen_target_rate.Id(),
+ Settings::values.frame_gen_flow_scale_auto.Id(),
+ Settings::values.frame_gen_flow_scale.Id(),
+ Settings::values.frame_gen_queue_target.Id()}) {
+ if (const auto it = hold_graphics.find(id); it != hold_graphics.end()) {
+ it->second->setVisible(frame_gen_checkbox->isChecked());
+ connect(frame_gen_checkbox, &QCheckBox::toggled, it->second, &QWidget::setVisible);
+ }
+ }
+ }
+
for (auto widget : hold_api) {
api_grid_layout->addWidget(widget);
}
diff --git a/src/yuzu/configuration/shared_widget.cpp b/src/yuzu/configuration/shared_widget.cpp
index 0e829e9100..4971c0cfa7 100644
--- a/src/yuzu/configuration/shared_widget.cpp
+++ b/src/yuzu/configuration/shared_widget.cpp
@@ -515,7 +515,13 @@ void Widget::SetupComponent(const QString& label, std::function<void()>& load_fu
layout->setContentsMargins(0, 0, 0, 0);
if (other_setting == nullptr) {
- other_setting = setting.PairedSetting();
+ // A paired setting without the Paired specialization gets a widget of its own, and
+ // several settings can share it, so they must not each save their own checkbox to it
+ auto* const paired_setting = setting.PairedSetting();
+ if (paired_setting != nullptr &&
+ paired_setting->Specialization() == Settings::Specialization::Paired) {
+ other_setting = paired_setting;
+ }
}
const bool require_checkbox = other_setting != nullptr && other_setting->TypeId() == "bool";
diff --git a/src/yuzu/game/game_list.cpp b/src/yuzu/game/game_list.cpp
index cd7fc19de4..d2613697bf 100644
--- a/src/yuzu/game/game_list.cpp
+++ b/src/yuzu/game/game_list.cpp
@@ -24,6 +24,7 @@
#include "common/common_types.h"
#include "core/core.h"
+#include "core/file_sys/nup_writer.h"
#include "core/file_sys/patch_manager.h"
#include "core/file_sys/registered_cache.h"
#include "qt_common/config/uisettings.h"
@@ -99,6 +100,10 @@ GameList::GameList(FileSys::VirtualFilesystem vfs_, FileSys::ManualContentProvid
ResetViewMode();
}
+void GameList::SetNupConversionRunning(bool running) {
+ nup_conversion_running = running;
+}
+
GameList::~GameList() {
UnloadController();
}
@@ -155,6 +160,10 @@ void GameList::PopulateAsync(QVector<UISettings::GameDir>& game_dirs) {
item_model->PopulateAsync(game_dirs);
}
+void GameList::SetScanEnabled(bool enabled) {
+ item_model->SetScanEnabled(enabled);
+}
+
void GameList::SaveInterfaceLayout() {
tree_view->SaveInterfaceLayout();
}
@@ -477,6 +486,11 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
QAction* dump_romfs_sdmc = dump_romfs_menu->addAction(tr("Dump RomFS to SDMC"));
QAction* verify_integrity = context_menu.addAction(tr("Verify Integrity"));
QAction* copy_tid = context_menu.addAction(tr("Copy Title ID to Clipboard"));
+ QAction* convert_nup = nullptr;
+ if (FileSys::Nup::DumpRootFromMain(path)) {
+ convert_nup = context_menu.addAction(tr("Convert to NUP..."));
+ convert_nup->setEnabled(!nup_conversion_running);
+ }
#if !defined(__APPLE__)
QMenu* shortcut_menu = context_menu.addMenu(tr("Create Shortcut"));
QAction* create_desktop_shortcut = shortcut_menu->addAction(tr("Add to Desktop"));
@@ -557,6 +571,10 @@ void GameList::AddGamePopup(QMenu& context_menu, u64 program_id, const std::stri
[this, path]() { emit VerifyIntegrityRequested(path); });
connect(copy_tid, &QAction::triggered, this,
[this, program_id]() { emit CopyTIDRequested(program_id); });
+ if (convert_nup) {
+ connect(convert_nup, &QAction::triggered, this,
+ [this, path]() { emit ConvertToNupRequested(path); });
+ }
#if !defined(__APPLE__)
connect(create_desktop_shortcut, &QAction::triggered, this, [this, program_id, path]() {
emit CreateShortcut(program_id, path, QtCommon::Game::ShortcutTarget::Desktop);
diff --git a/src/yuzu/game/game_list.h b/src/yuzu/game/game_list.h
index 1540dd9cf0..b33dfdd9c8 100644
--- a/src/yuzu/game/game_list.h
+++ b/src/yuzu/game/game_list.h
@@ -76,6 +76,7 @@ public:
bool IsEmpty() const;
void PopulateAsync(QVector<UISettings::GameDir>& game_dirs);
+ void SetScanEnabled(bool enabled);
void SaveInterfaceLayout();
void LoadInterfaceLayout();
@@ -88,6 +89,7 @@ public:
void ResetViewMode();
public slots:
+ void SetNupConversionRunning(bool running);
void RefreshGameDirectory();
void RefreshExternalContent();
void UpdateIconSizes();
@@ -106,6 +108,7 @@ signals:
void SetPlayTimeRequested(u64 program_id);
void DumpRomFSRequested(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
void VerifyIntegrityRequested(const std::string& game_path);
+ void ConvertToNupRequested(const std::string& game_path);
void CopyTIDRequested(u64 program_id);
void CreateShortcut(u64 program_id, const std::string& game_path,
const QtCommon::Game::ShortcutTarget target);
@@ -165,6 +168,7 @@ private:
Core::System& system;
bool m_isTreeMode = true;
+ bool nup_conversion_running{};
QAbstractItemView* m_currentView = nullptr;
};
diff --git a/src/yuzu/main_window.cpp b/src/yuzu/main_window.cpp
index b0c0446add..dbe5ea2334 100644
--- a/src/yuzu/main_window.cpp
+++ b/src/yuzu/main_window.cpp
@@ -10,6 +10,7 @@
#include <boost/algorithm/string/split.hpp>
#include "common/fs/path_util.h"
+#include "common/game_presets.h"
#include "common/settings.h"
#include "common/settings_enums.h"
#include "frontend_common/settings_generator.h"
@@ -42,6 +43,7 @@
#include "set_play_time_dialog.h"
#include "util/util.h"
#include "yuzu/game/game_list.h"
+#include "yuzu/nup_dialogs.h"
#include "applets/qt_amiibo_settings.h"
#include "applets/qt_controller.h"
@@ -73,8 +75,11 @@
#include <QClipboard>
#include <QDesktopServices>
#include <QDir>
+#include <QElapsedTimer>
+#include <QEventLoop>
#include <QFileDialog>
#include <QFileInfo>
+#include <QFutureWatcher>
#include <QGuiApplication>
#include <QInputDialog>
#include <QMimeData>
@@ -149,6 +154,7 @@ static FileSys::VirtualFile VfsDirectoryCreateFileWrapper(const FileSys::Virtual
#include "core/file_sys/card_image.h"
#include "core/file_sys/common_funcs.h"
+#include "core/file_sys/nup_writer.h"
#include "core/file_sys/romfs.h"
#include "core/file_sys/savedata_factory.h"
@@ -315,6 +321,7 @@ __declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
constexpr int default_mouse_hide_timeout = 2500;
constexpr int default_input_update_timeout = 1;
+constexpr int EmuThreadStopTimeoutMs = 10'000;
constexpr size_t CopyBufferSize = 1_MiB;
@@ -450,7 +457,7 @@ MainWindow::MainWindow(bool has_broken_vulkan)
QtCommon::SetupContentProviders();
// Gen keys if necessary
- OnCheckFirmwareDecryption();
+ OnCheckFirmwareDecryption(false);
#ifdef __unix__
OnCheckGraphicsBackend();
@@ -554,6 +561,10 @@ MainWindow::MainWindow(bool has_broken_vulkan)
}
MainWindow::~MainWindow() {
+ if (nup_cancel) {
+ nup_cancel->store(true);
+ }
+ nup_watcher.waitForFinished();
#ifdef __APPLE__
AllowOSSleep();
#endif
@@ -1288,7 +1299,6 @@ void MainWindow::InitializeHotkeys() {
hotkey_registry.LoadHotkeys();
LinkActionShortcut(ui->action_Load_File, QStringLiteral("Load File"));
- LinkActionShortcut(ui->action_Load_Amiibo, QStringLiteral("Load/Remove Amiibo"));
LinkActionShortcut(ui->action_Exit, QStringLiteral("Exit Eglantine"));
LinkActionShortcut(ui->action_Restart, QStringLiteral("Restart Emulation"));
LinkActionShortcut(ui->action_Pause, QStringLiteral("Continue/Pause Emulation"));
@@ -1458,6 +1468,8 @@ void MainWindow::ConnectWidgetEvents() {
connect(game_list, &GameList::DumpRomFSRequested, this, &MainWindow::OnGameListDumpRomFS);
connect(game_list, &GameList::VerifyIntegrityRequested, this,
&MainWindow::OnGameListVerifyIntegrity);
+ connect(game_list, &GameList::ConvertToNupRequested, this,
+ &MainWindow::OnGameListConvertToNup);
connect(game_list, &GameList::CopyTIDRequested, this, &MainWindow::OnGameListCopyTID);
connect(game_list, &GameList::CreateShortcut, this, &MainWindow::OnGameListCreateShortcut);
connect(game_list, &GameList::AddDirectory, this, &MainWindow::OnGameListAddDirectory);
@@ -1826,24 +1838,6 @@ bool MainWindow::LoadROM(const QString& filename, Service::AM::FrontendAppletPar
const Core::SystemResultStatus result{
QtCommon::system->Load(*render_window, filename.toStdString(), params)};
- const auto drd_callout = (UISettings::values.callout_flags.GetValue() &
- static_cast<u32>(CalloutFlag::DRDDeprecation)) == 0;
-
- if (result == Core::SystemResultStatus::Success &&
- QtCommon::system->GetAppLoader().GetFileType() ==
- Loader::FileType::DeconstructedRomDirectory &&
- drd_callout) {
- UISettings::values.callout_flags = UISettings::values.callout_flags.GetValue() |
- static_cast<u32>(CalloutFlag::DRDDeprecation);
- QMessageBox::warning(
- this, tr("Warning: Outdated Game Format"),
- tr("You are using the deconstructed ROM directory format for this game, which is an "
- "outdated format that has been superseded by others such as NCA, NAX, XCI, or "
- "NSP. Deconstructed ROM directories lack icons, metadata, and update "
- "support.<br>For an explanation of the various Switch formats Eglantine supports, "
- "out our user handbook. This message will not be shown again."));
- }
-
if (result != Core::SystemResultStatus::Success) {
switch (result) {
case Core::SystemResultStatus::ErrorGetLoader:
@@ -1907,6 +1901,37 @@ bool MainWindow::SelectAndSetCurrentUser(
void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletParameters params,
StartGameType type) {
+ if (nup_running && filename.toStdString() == nup_source_path) {
+ QMessageBox::information(this, tr("Conversion in Progress"),
+ tr("This game cannot be started while its dump is being "
+ "converted."));
+ return;
+ }
+ if (boot_in_progress || close_pending) {
+ return;
+ }
+ boot_in_progress = true;
+ game_list->SetScanEnabled(false);
+ QElapsedTimer boot_timer;
+ boot_timer.start();
+ SCOPE_EXIT {
+ boot_in_progress = false;
+ LOG_INFO(Frontend, "Launch finished: elapsed_ms={} running={}", boot_timer.elapsed(),
+ emulation_running);
+ if (!emulation_running && !close_pending) {
+ game_list->SetScanEnabled(true);
+ game_list->PopulateAsync(UISettings::values.game_dirs);
+ }
+ if (close_pending) {
+ QTimer::singleShot(0, this, &QWidget::close);
+ }
+ };
+ if (emulation_running) {
+ ShutdownGame();
+ }
+ if (close_pending) {
+ return;
+ }
LOG_INFO(Frontend, "Eglantine starting...");
if (params.program_id == 0 ||
@@ -1941,6 +1966,26 @@ void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletPa
QtCommon::system->ApplySettings();
}
+ if (title_id == Settings::MarioKartWorldProgramId && !asked_recommended_mkw_settings) {
+ asked_recommended_mkw_settings = true;
+ use_recommended_mkw_settings =
+ QMessageBox::question(
+ this, tr("Recommended Mario Kart World settings"),
+ tr("Use the recommended settings for Mario Kart World?\n\n"
+ "Vulkan, 1x resolution, GPU Fast, asynchronous shaders, BC3, conservative VRAM, "
+ "reactive flushing off, FIFO Relaxed VSync, handheld mode, ScaleForce, and "
+ "FXAA.\n\n"
+ "They apply to Mario Kart World until Eglantine is closed. Your saved settings "
+ "are not changed."),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No) == QMessageBox::Yes;
+ LOG_INFO(Config, "MKW recommended settings accepted: {}", use_recommended_mkw_settings);
+ }
+ if (Settings::ApplyMarioKartWorldPreset(Settings::values, title_id,
+ use_recommended_mkw_settings)) {
+ QtCommon::system->ApplySettings();
+ Settings::UpdateGPUAccuracy();
+ LOG_INFO(Config, "Applied recommended MKW settings after per-game configuration");
+ }
Settings::LogSettings();
if (UISettings::values.select_user_on_boot && !user_flag_cmd_line) {
@@ -1960,9 +2005,43 @@ void MainWindow::BootGame(const QString& filename, Service::AM::FrontendAppletPa
// behavior of asking.
user_flag_cmd_line = false;
+ FileSys::VirtualFile prepared_romfs;
+ if (loader && loader->GetFileType() == Loader::FileType::DeconstructedRomDirectory) {
+ QProgressDialog progress(tr("Preparing game files..."), QString{}, 0, 0, this);
+ progress.setWindowModality(Qt::ApplicationModal);
+ progress.setWindowFlag(Qt::WindowCloseButtonHint, false);
+ progress.setCancelButton(nullptr);
+ progress.setMinimumDuration(0);
+ QFutureWatcher<FileSys::VirtualFile> watcher;
+ QEventLoop loop;
+ connect(&watcher, &QFutureWatcher<FileSys::VirtualFile>::finished, &loop, &QEventLoop::quit);
+ watcher.setFuture(QtConcurrent::run([&loader] {
+ FileSys::VirtualFile image;
+ loader->ReadRomFS(image);
+ return image;
+ }));
+ progress.show();
+ loop.exec();
+ try {
+ prepared_romfs = watcher.result();
+ } catch (const std::exception& error) {
+ LOG_ERROR(Frontend, "Loose RomFS preparation failed: {}", error.what());
+ QMessageBox::critical(this, tr("Unable to prepare game files"),
+ tr("Game preparation failed. See the log for details."));
+ return;
+ }
+ if (close_pending) {
+ return;
+ }
+ }
+
+ QElapsedTimer load_timer;
+ load_timer.start();
if (!LoadROM(filename, params)) {
+ LOG_ERROR(Frontend, "Core load failed: elapsed_ms={}", load_timer.elapsed());
return;
}
+ LOG_INFO(Frontend, "Core load completed: elapsed_ms={}", load_timer.elapsed());
QtCommon::system->SetShuttingDown(false);
game_list->setDisabled(true);
@@ -2097,6 +2176,9 @@ bool MainWindow::OnShutdownBegin() {
Settings::values.current_speed_mode = Settings::SpeedMode::Standard;
if (QtCommon::system->IsShuttingDown()) {
+ status_bar_update_timer.stop();
+ connect(QtCommon::emu_thread.get(), &QThread::finished, this,
+ &MainWindow::OnEmulationStopped, Qt::UniqueConnection);
return false;
}
@@ -2107,6 +2189,8 @@ bool MainWindow::OnShutdownBegin() {
}
QtCommon::system->SetShuttingDown(true);
+ status_bar_update_timer.stop();
+ LOG_INFO(Frontend, "Shutdown requested; waiting for emulation thread");
discord_rpc->Pause();
RequestGameExit();
@@ -2126,7 +2210,8 @@ bool MainWindow::OnShutdownBegin() {
shutdown_timer.setSingleShot(true);
shutdown_timer.start(shutdown_time);
- connect(&shutdown_timer, &QTimer::timeout, this, &MainWindow::OnEmulationStopTimeExpired);
+ connect(&shutdown_timer, &QTimer::timeout, this, &MainWindow::OnEmulationStopTimeExpired,
+ Qt::UniqueConnection);
// Disable everything to prevent anything from being triggered here
ui->action_Pause->setEnabled(false);
@@ -2146,18 +2231,38 @@ void MainWindow::OnShutdownBeginDialog() {
void MainWindow::OnEmulationStopTimeExpired() {
if (QtCommon::emu_thread) {
QtCommon::emu_thread->ForceStop();
+ emu_terminate_timer.setSingleShot(true);
+ emu_terminate_timer.start(EmuThreadStopTimeoutMs);
+ connect(&emu_terminate_timer, &QTimer::timeout, this, &MainWindow::OnEmulationStopTimeout,
+ Qt::UniqueConnection);
+ }
+}
+
+void MainWindow::OnEmulationStopTimeout() {
+ TerminateEmulationThread();
+ OnEmulationStopped();
+}
+
+void MainWindow::TerminateEmulationThread() {
+ if (QtCommon::emu_thread && QtCommon::emu_thread->isRunning()) {
+ LOG_ERROR(Frontend, "Emulation thread failed to stop, terminating it");
+ QtCommon::emu_thread->terminate();
+ QtCommon::emu_thread->wait();
}
}
void MainWindow::OnEmulationStopped() {
+ if (!emulation_running) {
+ return;
+ }
+ if (QtCommon::emu_thread && QtCommon::emu_thread->isRunning()) {
+ return;
+ }
shutdown_timer.stop();
+ emu_terminate_timer.stop();
if (QtCommon::emu_thread) {
QtCommon::emu_thread->disconnect();
- if (!QtCommon::emu_thread->wait(QDeadlineTimer(10'000))) {
- LOG_ERROR(Frontend, "Emulation thread failed to stop, terminating it");
- QtCommon::emu_thread->terminate();
- QtCommon::emu_thread->wait();
- }
+ QtCommon::emu_thread->wait();
QtCommon::emu_thread.reset();
}
@@ -2226,6 +2331,13 @@ void MainWindow::OnEmulationStopped() {
Settings::RestoreGlobalState(QtCommon::system->IsPoweredOn());
QtCommon::system->HIDCore().ReloadInputDevices();
UpdateStatusButtons();
+ LOG_INFO(Frontend, "Emulation cleanup completed");
+ if (close_pending) {
+ QTimer::singleShot(0, this, &QWidget::close);
+ } else if (!boot_in_progress) {
+ game_list->SetScanEnabled(true);
+ game_list->PopulateAsync(UISettings::values.game_dirs);
+ }
}
void MainWindow::ShutdownGame() {
@@ -2237,6 +2349,17 @@ void MainWindow::ShutdownGame() {
play_time_manager->Stop();
OnShutdownBegin();
OnEmulationStopTimeExpired();
+ emu_terminate_timer.stop();
+ if (QtCommon::emu_thread && QtCommon::emu_thread->isRunning()) {
+ QEventLoop loop;
+ QTimer timeout;
+ timeout.setSingleShot(true);
+ connect(QtCommon::emu_thread.get(), &QThread::finished, &loop, &QEventLoop::quit);
+ connect(&timeout, &QTimer::timeout, &loop, &QEventLoop::quit);
+ timeout.start(EmuThreadStopTimeoutMs);
+ loop.exec(QEventLoop::ExcludeUserInputEvents);
+ }
+ TerminateEmulationThread();
OnEmulationStopped();
}
@@ -3069,8 +3192,6 @@ void MainWindow::OnPauseContinueGame() {
void MainWindow::OnStopGame() {
if (ConfirmShutdownGame()) {
play_time_manager->Stop();
- // Update game list to show new play time
- game_list->PopulateAsync(UISettings::values.game_dirs);
if (OnShutdownBegin()) {
OnShutdownBeginDialog();
} else {
@@ -3967,6 +4088,129 @@ void MainWindow::OnGameListRefresh() {
SetFirmwareVersion();
}
+void MainWindow::OnGameListConvertToNup(const std::string& game_path) {
+ if (nup_running)
+ return;
+
+ const auto root = FileSys::Nup::DumpRootFromMain(game_path);
+ if (!root)
+ return;
+ const QString convert_title = tr("Convert to NUP");
+ const auto archive = FileSys::Nup::DefaultArchivePath(*root);
+ if (std::filesystem::exists(archive)) {
+ QMessageBox::information(this, convert_title,
+ tr("The target archive already exists:\n%1")
+ .arg(QString::fromStdString(Common::FS::PathToUTF8String(archive))));
+ return;
+ }
+
+ const bool resume = FileSys::Nup::HasResumableJournal(archive);
+ bool delete_sources = resume;
+ FileSys::Nup::Metadata metadata;
+ QString progress_title;
+ QPixmap progress_icon;
+ if (resume) {
+ if (QMessageBox::question(this, tr("Resume Conversion"),
+ tr("Resume the interrupted conversion?"),
+ QMessageBox::Yes | QMessageBox::No) != QMessageBox::Yes)
+ return;
+ } else {
+ u64 needed{};
+ {
+ std::error_code ec;
+ for (std::filesystem::recursive_directory_iterator it(*root, ec), end;
+ !ec && it != end; it.increment(ec)) {
+ if (it->is_regular_file(ec))
+ needed += it->file_size(ec);
+ }
+ }
+ std::error_code space_error;
+ const auto space = std::filesystem::space(archive.parent_path(), space_error);
+ NupMetadataDialog dialog(*root, archive, needed, space_error ? 0 : space.available, this);
+ if (dialog.exec() != QDialog::Accepted)
+ return;
+ delete_sources = dialog.DeleteSources();
+ metadata = dialog.Metadata();
+ progress_title = dialog.DisplayName();
+ progress_icon = dialog.CurrentIcon();
+ }
+
+ nup_running = true;
+ nup_source_path = game_path;
+ game_list->SetNupConversionRunning(true);
+ nup_cancel = std::make_shared<std::atomic_bool>(false);
+ nup_progress = new NupProgressDialog(progress_title, progress_icon, this);
+ connect(nup_progress, &NupProgressDialog::CancelRequested, this,
+ [this, cancel = nup_cancel] {
+ cancel->store(true);
+ if (nup_progress)
+ nup_progress->SetCancelling();
+ });
+ nup_progress->show();
+
+ const auto cancel = nup_cancel;
+ auto last_update = std::make_shared<std::chrono::steady_clock::time_point>();
+ nup_watcher.setFuture(QtConcurrent::run([this, root = *root, archive, delete_sources, metadata,
+ cancel, last_update] {
+ std::string error;
+ const auto on_progress = [this, last_update](
+ const FileSys::Nup::PackProgress& progress) {
+ const auto now = std::chrono::steady_clock::now();
+ if (now - *last_update < std::chrono::milliseconds(200))
+ return;
+ *last_update = now;
+ QMetaObject::invokeMethod(
+ this,
+ [this, progress] {
+ if (!nup_progress)
+ return;
+ nup_progress->SetProgress(progress);
+ },
+ Qt::QueuedConnection);
+ };
+ const auto result = FileSys::Nup::PackDump(
+ root, archive, {.delete_sources = delete_sources, .metadata = metadata}, on_progress,
+ cancel.get(), &error);
+ QMetaObject::invokeMethod(
+ this,
+ [this, result, delete_sources, error] {
+ nup_running = false;
+ nup_source_path.clear();
+ game_list->SetNupConversionRunning(false);
+ if (nup_progress) {
+ nup_progress->Finish();
+ nup_progress->deleteLater();
+ nup_progress = nullptr;
+ }
+ nup_cancel.reset();
+ switch (result) {
+ case FileSys::Nup::PackResult::Success:
+ QMessageBox::information(
+ this, tr("Conversion Complete"),
+ delete_sources ? tr("The NUP was created and the loose folder was "
+ "removed.")
+ : tr("The NUP was created. You can delete the loose "
+ "folder if you no longer need it."));
+ OnGameListRefresh();
+ break;
+ case FileSys::Nup::PackResult::Cancelled:
+ if (delete_sources)
+ QMessageBox::information(this, tr("Conversion Cancelled"),
+ tr("The conversion can be resumed by choosing "
+ "Convert to NUP again."));
+ break;
+ case FileSys::Nup::PackResult::InvalidSource:
+ case FileSys::Nup::PackResult::IoError:
+ QMessageBox::critical(this, tr("Conversion Failed"),
+ QString::fromStdString(error));
+ break;
+ }
+ },
+ Qt::QueuedConnection);
+ return result;
+ }));
+}
+
void MainWindow::LaunchFirmwareApplet(u64 raw_program_id,
std::optional<Service::NFP::CabinetMode> cabinet_mode) {
auto const program_id = Service::AM::AppletProgramId(raw_program_id);
@@ -4392,8 +4636,13 @@ void MainWindow::OnMouseActivity() {
}
}
-void MainWindow::OnCheckFirmwareDecryption() {
- if (!ContentManager::AreKeysPresent()) {
+void MainWindow::OnCheckFirmwareDecryption(bool prompt) {
+ if (!ContentManager::AreKeysPresent() && !prompt) {
+ // At startup only log it: loose ExeFS/RomFS dumps (all Switch 2 games so far) need no
+ // keys, and a modal question blocked every unattended launch. Tools > Install Decryption
+ // Keys still installs them.
+ LOG_WARNING(Frontend, "Decryption keys are missing: NCA, NSP and XCI games need them");
+ } else if (!ContentManager::AreKeysPresent()) {
const auto res = QtCommon::Frontend::Warning(
tr("Derivation Components Missing"),
tr("Decryption keys are missing. Install them now?"),
@@ -4576,10 +4825,28 @@ bool MainWindow::ConfirmClose() {
}
void MainWindow::closeEvent(QCloseEvent* event) {
- if (!ConfirmClose()) {
+ if (!close_pending && !ConfirmClose()) {
+ event->ignore();
+ return;
+ }
+ close_pending = true;
+ game_list->SetScanEnabled(false);
+ if (boot_in_progress) {
event->ignore();
return;
}
+ if (QtCommon::emu_thread && emulation_running) {
+ event->ignore();
+ if (!QtCommon::emu_thread->isRunning()) {
+ OnEmulationStopped();
+ return;
+ }
+ if (OnShutdownBegin()) {
+ OnShutdownBeginDialog();
+ }
+ return;
+ }
+ LOG_INFO(Frontend, "Closing frontend after emulation cleanup");
UpdateUISettings();
game_list->SaveInterfaceLayout();
diff --git a/src/yuzu/main_window.h b/src/yuzu/main_window.h
index 57f585fef9..1bd04a4d8b 100644
--- a/src/yuzu/main_window.h
+++ b/src/yuzu/main_window.h
@@ -10,8 +10,10 @@
#include <optional>
#include <filesystem>
+#include <QElapsedTimer>
#include <QMainWindow>
#include <QMessageBox>
+#include <QFutureWatcher>
#include <QPushButton>
#include <QTimer>
#include <QTranslator>
@@ -19,6 +21,7 @@
#include "common/common_types.h"
#include "common/settings_enums.h"
+#include "core/file_sys/nup_writer.h"
#include "frontend_common/content_manager.h"
#include "input_common/drivers/tas_input.h"
#include "qt_common/config/qt_config.h"
@@ -52,6 +55,7 @@ class QLabel;
class MultiplayerState;
class QPushButton;
class QProgressDialog;
+class NupProgressDialog;
class QSlider;
class QHBoxLayout;
class WaitTreeWidget;
@@ -275,6 +279,7 @@ private:
StartGameType with_config = StartGameType::Normal);
void BootGameFromList(const QString& filename, StartGameType with_config);
void ShutdownGame();
+ void TerminateEmulationThread();
void SetDiscordEnabled(bool state);
void LoadAmiibo(const QString& filename);
@@ -352,6 +357,7 @@ private slots:
void OnGameListSetPlayTime(u64 program_id);
void OnGameListDumpRomFS(u64 program_id, const std::string& game_path, DumpRomFSTarget target);
void OnGameListVerifyIntegrity(const std::string& game_path);
+ void OnGameListConvertToNup(const std::string& game_path);
void OnGameListCopyTID(u64 program_id);
void OnGameListCreateShortcut(u64 program_id, const std::string& game_path,
const QtCommon::Game::ShortcutTarget target);
@@ -421,7 +427,8 @@ private slots:
void OnCreateHomeMenuDesktopShortcut();
void OnCreateHomeMenuApplicationMenuShortcut();
void OnCaptureScreenshot();
- void OnCheckFirmwareDecryption();
+ /// Checks for decryption keys; `prompt` asks to install them when they are missing.
+ void OnCheckFirmwareDecryption(bool prompt = true);
#ifdef __unix__
void OnCheckGraphicsBackend();
#endif
@@ -431,6 +438,7 @@ private slots:
void OnShutdownBeginDialog();
void OnEmulationStopped();
void OnEmulationStopTimeExpired();
+ void OnEmulationStopTimeout();
#ifdef ENABLE_UPDATE_CHECKER
void OnEmulatorUpdateAvailable();
@@ -495,11 +503,14 @@ private:
#endif
MultiplayerState* multiplayer_state = nullptr;
+ bool use_recommended_mkw_settings = false;
+ bool asked_recommended_mkw_settings = false;
GRenderWindow* render_window = nullptr;
GameList* game_list = nullptr;
LoadingScreen* loading_screen = nullptr;
QTimer shutdown_timer;
+ QTimer emu_terminate_timer;
OverlayDialog* shutdown_dialog{};
PerformanceOverlay* perf_overlay = nullptr;
#ifdef HAS_RESHADE
@@ -539,6 +550,8 @@ private:
// Whether emulation is currently running in yuzu.
bool emulation_running = false;
+ bool boot_in_progress = false;
+ bool close_pending = false;
// The path to the game currently running
QString current_game_path;
// Whether a user was set on the command line (skips UserSelector if it's forced to show up)
@@ -570,6 +583,11 @@ private:
// Install progress dialog
QProgressDialog* install_progress = nullptr;
+ NupProgressDialog* nup_progress = nullptr;
+ QFutureWatcher<FileSys::Nup::PackResult> nup_watcher;
+ std::shared_ptr<std::atomic_bool> nup_cancel;
+ bool nup_running{};
+ std::string nup_source_path;
// Last game booted, used for multi-process apps
QString last_filename_booted;
diff --git a/src/yuzu/nup_dialogs.cpp b/src/yuzu/nup_dialogs.cpp
new file mode 100644
index 0000000000..cd7fcba496
--- /dev/null
+++ b/src/yuzu/nup_dialogs.cpp
@@ -0,0 +1,485 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "yuzu/nup_dialogs.h"
+
+#include <QBuffer>
+#include <QCheckBox>
+#include <QCloseEvent>
+#include <QDragEnterEvent>
+#include <QDropEvent>
+#include <QMimeData>
+#include <QMouseEvent>
+#include <QPaintEvent>
+#include <QDialogButtonBox>
+#include <QFile>
+#include <QFileDialog>
+#include <QFontMetrics>
+#include <QFormLayout>
+#include <QHBoxLayout>
+#include <QImage>
+#include <QImageReader>
+#include <QLabel>
+#include <QLineEdit>
+#include <QPainter>
+#include <QPainterPath>
+#include <QProgressBar>
+#include <QPushButton>
+#include <QRadioButton>
+#include <QUrl>
+#include <QVBoxLayout>
+
+#include "common/fs/path_util.h"
+
+namespace {
+
+constexpr int IconSize = 256;
+constexpr int PreviewSize = 128;
+
+QString ToQString(const std::filesystem::path& path) {
+ return QString::fromStdString(Common::FS::PathToUTF8String(path));
+}
+
+QString Megabytes(u64 bytes) {
+ return QString::number(bytes / (1024 * 1024));
+}
+
+QPixmap PixmapFromBytes(const std::vector<u8>& bytes) {
+ QPixmap pixmap;
+ pixmap.loadFromData(bytes.data(), static_cast<uint>(bytes.size()));
+ return pixmap;
+}
+
+QPixmap RoundedPreview(const QPixmap& source, int size) {
+ const qreal ratio = 2.0;
+ QPixmap out(QSize(size, size) * ratio);
+ out.setDevicePixelRatio(ratio);
+ out.fill(Qt::transparent);
+ QPainter painter(&out);
+ painter.setRenderHint(QPainter::Antialiasing);
+ painter.setRenderHint(QPainter::SmoothPixmapTransform);
+ QPainterPath clip;
+ clip.addRoundedRect(QRectF(0, 0, size, size), size / 8.0, size / 8.0);
+ painter.setClipPath(clip);
+ painter.drawPixmap(QRectF(0, 0, size, size), source, source.rect());
+ return out;
+}
+
+// The console distributes 256x256 JPEG icons. A picture that already is one is kept as is;
+// anything else is cropped to a square, scaled and re-encoded.
+QByteArray EncodeIcon(const QString& path) {
+ QFile file(path);
+ if (!file.open(QIODevice::ReadOnly)) {
+ return {};
+ }
+ const QByteArray raw = file.readAll();
+ QImage image;
+ if (!image.loadFromData(raw) || image.isNull()) {
+ return {};
+ }
+ if (image.size() == QSize(IconSize, IconSize) && raw.startsWith("\xFF\xD8")) {
+ return raw;
+ }
+ const int side = std::min(image.width(), image.height());
+ image = image.copy((image.width() - side) / 2, (image.height() - side) / 2, side, side)
+ .scaled(IconSize, IconSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
+ QImage flat(IconSize, IconSize, QImage::Format_RGB32);
+ flat.fill(Qt::black);
+ QPainter painter(&flat);
+ painter.drawImage(0, 0, image);
+ painter.end();
+ QByteArray out;
+ QBuffer buffer(&out);
+ buffer.open(QIODevice::WriteOnly);
+ flat.save(&buffer, "JPEG", 92);
+ return out;
+}
+
+// The first dropped local file that Qt can read as an image.
+QString DroppedImage(const QMimeData* mime) {
+ if (mime == nullptr) {
+ return {};
+ }
+ for (const auto& url : mime->urls()) {
+ if (url.isLocalFile() && !QImageReader(url.toLocalFile()).format().isEmpty()) {
+ return url.toLocalFile();
+ }
+ }
+ return {};
+}
+
+} // namespace
+
+IconDropZone::IconDropZone(int size, QWidget* parent) : QWidget(parent) {
+ setFixedSize(size, size);
+ setAcceptDrops(true);
+ setCursor(Qt::PointingHandCursor);
+ setToolTip(tr("Drop an image here or click to browse"));
+}
+
+void IconDropZone::SetIcon(const QPixmap& new_icon) {
+ icon = new_icon;
+ update();
+}
+
+void IconDropZone::paintEvent(QPaintEvent*) {
+ QPainter painter(this);
+ painter.setRenderHint(QPainter::Antialiasing);
+ const QRectF area = QRectF(rect()).adjusted(1, 1, -1, -1);
+ const bool active = dragging || hovered;
+ if (!icon.isNull()) {
+ painter.drawPixmap(0, 0, RoundedPreview(icon, width()));
+ if (active) {
+ painter.setBrush(QColor(0, 0, 0, 120));
+ painter.setPen(Qt::NoPen);
+ painter.drawRoundedRect(area, width() / 8.0, width() / 8.0);
+ painter.setPen(Qt::white);
+ painter.drawText(area, Qt::AlignCenter | Qt::TextWordWrap, tr("Drop to replace"));
+ }
+ return;
+ }
+ const auto accent = palette().color(QPalette::Highlight);
+ QPen pen(active ? accent : palette().color(QPalette::Mid), 2, Qt::DashLine);
+ painter.setPen(pen);
+ QColor fill = active ? accent : palette().color(QPalette::Mid);
+ fill.setAlpha(active ? 40 : 20);
+ painter.setBrush(fill);
+ painter.drawRoundedRect(area, width() / 8.0, width() / 8.0);
+ painter.setPen(palette().color(QPalette::PlaceholderText));
+ painter.drawText(area.adjusted(8, 8, -8, -8), Qt::AlignCenter | Qt::TextWordWrap,
+ dragging ? tr("Release to use this image")
+ : tr("Drop an icon here\nor click to browse"));
+}
+
+void IconDropZone::dragEnterEvent(QDragEnterEvent* event) {
+ if (!DroppedImage(event->mimeData()).isEmpty()) {
+ dragging = true;
+ event->acceptProposedAction();
+ update();
+ }
+}
+
+void IconDropZone::dragLeaveEvent(QDragLeaveEvent*) {
+ dragging = false;
+ update();
+}
+
+void IconDropZone::dropEvent(QDropEvent* event) {
+ dragging = false;
+ update();
+ if (const auto path = DroppedImage(event->mimeData()); !path.isEmpty()) {
+ event->acceptProposedAction();
+ emit FileDropped(path);
+ }
+}
+
+void IconDropZone::mouseReleaseEvent(QMouseEvent* event) {
+ if (event->button() == Qt::LeftButton && rect().contains(event->position().toPoint())) {
+ emit Clicked();
+ }
+}
+
+void IconDropZone::enterEvent(QEnterEvent*) {
+ hovered = true;
+ update();
+}
+
+void IconDropZone::leaveEvent(QEvent*) {
+ hovered = false;
+ update();
+}
+
+NupMetadataDialog::NupMetadataDialog(const std::filesystem::path& source,
+ const std::filesystem::path& archive, u64 needed_bytes,
+ u64 available_bytes, QWidget* parent)
+ : QDialog(parent), detected(FileSys::Nup::ProbeMetadata(source)) {
+ setWindowTitle(tr("Convert to NUP"));
+ setMinimumWidth(520);
+
+ icon_preview = new IconDropZone(PreviewSize);
+ connect(icon_preview, &IconDropZone::Clicked, this, &NupMetadataDialog::ChooseIcon);
+ connect(icon_preview, &IconDropZone::FileDropped, this, &NupMetadataDialog::SetIconFromFile);
+ icon_hint = new QLabel;
+ icon_hint->setWordWrap(true);
+ icon_hint->setEnabled(false);
+ auto* choose_icon = new QPushButton(tr("Choose Icon..."));
+ reset_icon = new QPushButton(tr("Use Detected"));
+ connect(choose_icon, &QPushButton::clicked, this, &NupMetadataDialog::ChooseIcon);
+ connect(reset_icon, &QPushButton::clicked, this, &NupMetadataDialog::ResetIcon);
+
+ auto* icon_buttons = new QVBoxLayout;
+ icon_buttons->addWidget(choose_icon);
+ if (!detected.icon.empty()) {
+ icon_buttons->addWidget(reset_icon);
+ } else {
+ reset_icon->hide();
+ }
+ icon_buttons->addWidget(icon_hint);
+ icon_buttons->addStretch();
+ auto* icon_row = new QHBoxLayout;
+ icon_row->setSpacing(16);
+ icon_row->addWidget(icon_preview, 0, Qt::AlignTop);
+ icon_row->addLayout(icon_buttons, 1);
+
+ name_edit = new QLineEdit(QString::fromStdString(detected.title_name));
+ name_edit->setPlaceholderText(tr("Name shown in the game list"));
+ name_edit->setClearButtonEnabled(true);
+
+ version_auto = new QRadioButton;
+ if (detected.display_version.empty()) {
+ version_auto->setText(tr("Auto-detect (nothing found in the dump)"));
+ } else {
+ version_auto->setText(tr("Auto-detect: %1 (from %2)")
+ .arg(QString::fromStdString(detected.display_version),
+ QString::fromStdString(detected.version_source)));
+ }
+ version_manual = new QRadioButton(tr("Set manually:"));
+ version_edit = new QLineEdit;
+ version_edit->setPlaceholderText(tr("e.g. 1.2.0"));
+ version_auto->setChecked(true);
+ connect(version_auto, &QRadioButton::toggled, this, &NupMetadataDialog::UpdateVersionEnabled);
+ auto* manual_row = new QHBoxLayout;
+ manual_row->addWidget(version_manual);
+ manual_row->addWidget(version_edit, 1);
+ auto* version_box = new QVBoxLayout;
+ version_box->setSpacing(6);
+ version_box->addWidget(version_auto);
+ version_box->addLayout(manual_row);
+
+ auto* form = new QFormLayout;
+ form->setFieldGrowthPolicy(QFormLayout::AllNonFixedFieldsGrow);
+ form->setHorizontalSpacing(16);
+ form->setVerticalSpacing(14);
+ form->addRow(tr("Icon"), icon_row);
+ form->addRow(tr("Name"), name_edit);
+ form->addRow(tr("Version"), version_box);
+
+ auto* paths = new QLabel(tr("<b>Source</b><br>%1<br><b>Target</b><br>%2")
+ .arg(ToQString(source).toHtmlEscaped(),
+ ToQString(archive).toHtmlEscaped()));
+ paths->setTextFormat(Qt::RichText);
+ paths->setWordWrap(true);
+ paths->setTextInteractionFlags(Qt::TextSelectableByMouse);
+
+ auto* space = new QLabel(tr("Approximate space needed: %1 MB. Free on the target volume: "
+ "%2 MB.")
+ .arg(Megabytes(needed_bytes), Megabytes(available_bytes)));
+ space->setWordWrap(true);
+ if (available_bytes < needed_bytes) {
+ space->setText(space->text() + QLatin1Char(' ') +
+ tr("Free space is less than the estimate."));
+ }
+
+ low_storage = new QCheckBox(tr("Low storage: delete the original files as they are packed "
+ "(cannot be undone)"));
+
+ auto* buttons = new QDialogButtonBox(QDialogButtonBox::Cancel);
+ auto* convert = buttons->addButton(tr("Convert"), QDialogButtonBox::AcceptRole);
+ convert->setDefault(true);
+ connect(buttons, &QDialogButtonBox::accepted, this, &QDialog::accept);
+ connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
+
+ auto* separator = new QFrame;
+ separator->setFrameShape(QFrame::HLine);
+ separator->setFrameShadow(QFrame::Sunken);
+
+ auto* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(20, 20, 20, 16);
+ layout->setSpacing(14);
+ layout->addLayout(form);
+ layout->addWidget(separator);
+ layout->addWidget(paths);
+ layout->addWidget(space);
+ layout->addWidget(low_storage);
+ layout->addSpacing(4);
+ layout->addWidget(buttons);
+
+ UpdateIconPreview();
+ UpdateVersionEnabled();
+}
+
+NupMetadataDialog::~NupMetadataDialog() = default;
+
+FileSys::Nup::Metadata NupMetadataDialog::Metadata() const {
+ FileSys::Nup::Metadata result;
+ // Only what differs from the dump is an override.
+ if (const auto name = name_edit->text().trimmed().toStdString(); name != detected.title_name) {
+ result.title_name = name;
+ }
+ if (!custom_icon.isEmpty()) {
+ result.icon.assign(custom_icon.begin(), custom_icon.end());
+ }
+ if (version_manual->isChecked()) {
+ result.display_version = version_edit->text().trimmed().toStdString();
+ }
+ return result;
+}
+
+bool NupMetadataDialog::DeleteSources() const {
+ return low_storage->isChecked();
+}
+
+QString NupMetadataDialog::DisplayName() const {
+ return name_edit->text().trimmed();
+}
+
+QPixmap NupMetadataDialog::CurrentIcon() const {
+ return current_icon;
+}
+
+void NupMetadataDialog::ChooseIcon() {
+ const auto path = QFileDialog::getOpenFileName(
+ this, tr("Choose Icon"), {}, tr("Images (*.png *.jpg *.jpeg *.bmp *.webp);;All Files (*)"));
+ if (!path.isEmpty()) {
+ SetIconFromFile(path);
+ }
+}
+
+void NupMetadataDialog::SetIconFromFile(const QString& path) {
+ const auto encoded = EncodeIcon(path);
+ if (encoded.isEmpty()) {
+ icon_hint->setText(tr("That file could not be read as an image."));
+ return;
+ }
+ custom_icon = encoded;
+ UpdateIconPreview();
+}
+
+void NupMetadataDialog::ResetIcon() {
+ custom_icon.clear();
+ UpdateIconPreview();
+}
+
+void NupMetadataDialog::UpdateIconPreview() {
+ QPixmap pixmap;
+ if (!custom_icon.isEmpty()) {
+ pixmap.loadFromData(custom_icon);
+ icon_hint->setText(tr("Custom icon, stored as %1x%1 JPEG.").arg(IconSize));
+ } else if (!detected.icon.empty()) {
+ pixmap = PixmapFromBytes(detected.icon);
+ icon_hint->setText(tr("Icon found in the dump."));
+ } else {
+ icon_hint->setText(tr("No icon found in the dump. Add one, or the NUP will have none."));
+ }
+ current_icon = pixmap;
+ reset_icon->setEnabled(!custom_icon.isEmpty());
+ icon_preview->SetIcon(pixmap);
+}
+
+void NupMetadataDialog::UpdateVersionEnabled() {
+ version_edit->setEnabled(version_manual->isChecked());
+ if (version_manual->isChecked()) {
+ version_edit->setFocus();
+ }
+}
+
+NupProgressDialog::NupProgressDialog(const QString& title, const QPixmap& icon, QWidget* parent)
+ : QDialog(parent, Qt::Dialog | Qt::CustomizeWindowHint | Qt::WindowTitleHint) {
+ setWindowTitle(tr("Converting to NUP"));
+ setModal(true);
+ setMinimumWidth(480);
+
+ auto* header = new QHBoxLayout;
+ header->setSpacing(14);
+ if (!icon.isNull()) {
+ auto* icon_label = new QLabel;
+ icon_label->setFixedSize(56, 56);
+ icon_label->setPixmap(RoundedPreview(icon, 56));
+ header->addWidget(icon_label);
+ }
+ auto* title_label = new QLabel(title.isEmpty() ? tr("Converting to NUP") : title);
+ QFont font = title_label->font();
+ font.setBold(true);
+ font.setPointSizeF(font.pointSizeF() * 1.2);
+ title_label->setFont(font);
+ title_label->setWordWrap(true);
+ status = new QLabel(tr("Preparing..."));
+ status->setEnabled(false);
+ auto* titles = new QVBoxLayout;
+ titles->setSpacing(2);
+ titles->addStretch();
+ titles->addWidget(title_label);
+ titles->addWidget(status);
+ titles->addStretch();
+ header->addLayout(titles, 1);
+
+ bar = new QProgressBar;
+ bar->setRange(0, 1000);
+ bar->setTextVisible(false);
+ bar->setFixedHeight(14);
+ bar->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
+
+ details = new QLabel;
+ details->setAlignment(Qt::AlignCenter);
+ details->setMinimumHeight(QFontMetrics(details->font()).height() * 2 + 4);
+
+ cancel = new QPushButton(tr("Cancel"));
+ connect(cancel, &QPushButton::clicked, this, &NupProgressDialog::CancelRequested);
+ auto* button_row = new QHBoxLayout;
+ button_row->addStretch();
+ button_row->addWidget(cancel);
+
+ auto* layout = new QVBoxLayout(this);
+ layout->setContentsMargins(24, 24, 24, 20);
+ layout->setSpacing(14);
+ layout->addLayout(header);
+ layout->addWidget(bar);
+ layout->addWidget(details);
+ layout->addLayout(button_row);
+ timer.start();
+}
+
+void NupProgressDialog::SetProgress(const FileSys::Nup::PackProgress& progress) {
+ if (cancelling) {
+ return;
+ }
+ const double fraction =
+ progress.bytes_total == 0 ? 0.0 : double(progress.bytes_done) / progress.bytes_total;
+ bar->setValue(static_cast<int>(fraction * 1000));
+ status->setText(tr("%1%").arg(fraction * 100, 0, 'f', 1));
+
+ QString text = tr("%1 / %2 MB - %3 / %4 files")
+ .arg(Megabytes(progress.bytes_done), Megabytes(progress.bytes_total))
+ .arg(progress.files_done)
+ .arg(progress.files_total);
+ const double seconds = timer.elapsed() / 1000.0;
+ if (seconds >= 3.0 && progress.bytes_done > 0 && fraction < 1.0) {
+ const double rate = progress.bytes_done / seconds;
+ const int remaining = static_cast<int>((progress.bytes_total - progress.bytes_done) / rate);
+ text += tr("\n%1 MB/s - about %2:%3 left")
+ .arg(rate / (1024 * 1024), 0, 'f', 0)
+ .arg(remaining / 60)
+ .arg(remaining % 60, 2, 10, QLatin1Char('0'));
+ }
+ details->setText(text);
+}
+
+void NupProgressDialog::SetCancelling() {
+ cancelling = true;
+ cancel->setEnabled(false);
+ status->setText(tr("Cancelling..."));
+ bar->setRange(0, 0);
+}
+
+void NupProgressDialog::Finish() {
+ finished = true;
+ close();
+}
+
+void NupProgressDialog::closeEvent(QCloseEvent* event) {
+ // Only the window code closes this dialog; a close request cancels the conversion.
+ if (!finished) {
+ emit CancelRequested();
+ event->ignore();
+ return;
+ }
+ QDialog::closeEvent(event);
+}
+
+void NupProgressDialog::reject() {
+ if (!finished) {
+ emit CancelRequested();
+ return;
+ }
+ QDialog::reject();
+}
diff --git a/src/yuzu/nup_dialogs.h b/src/yuzu/nup_dialogs.h
new file mode 100644
index 0000000000..b9258c216b
--- /dev/null
+++ b/src/yuzu/nup_dialogs.h
@@ -0,0 +1,114 @@
+// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#pragma once
+
+#include <filesystem>
+
+#include <QByteArray>
+#include <QDialog>
+#include <QElapsedTimer>
+#include <QPixmap>
+#include <QWidget>
+
+#include "common/common_types.h"
+#include "core/file_sys/nup_writer.h"
+
+class QCheckBox;
+class QLabel;
+class QLineEdit;
+class QProgressBar;
+class QPushButton;
+class QRadioButton;
+
+// Square preview that accepts an image dropped on it or chosen with a click.
+class IconDropZone : public QWidget {
+ Q_OBJECT
+public:
+ explicit IconDropZone(int size, QWidget* parent = nullptr);
+
+ void SetIcon(const QPixmap& icon);
+
+signals:
+ void FileDropped(const QString& path);
+ void Clicked();
+
+protected:
+ void paintEvent(QPaintEvent* event) override;
+ void dragEnterEvent(QDragEnterEvent* event) override;
+ void dragLeaveEvent(QDragLeaveEvent* event) override;
+ void dropEvent(QDropEvent* event) override;
+ void mouseReleaseEvent(QMouseEvent* event) override;
+ void enterEvent(QEnterEvent* event) override;
+ void leaveEvent(QEvent* event) override;
+
+private:
+ QPixmap icon;
+ bool dragging = false;
+ bool hovered = false;
+};
+
+// Asks for the metadata a NUP is written with and for the conversion options.
+class NupMetadataDialog : public QDialog {
+ Q_OBJECT
+public:
+ NupMetadataDialog(const std::filesystem::path& source, const std::filesystem::path& archive,
+ u64 needed_bytes, u64 available_bytes, QWidget* parent = nullptr);
+ ~NupMetadataDialog() override;
+
+ // Empty fields keep what the dump supplies.
+ FileSys::Nup::Metadata Metadata() const;
+ bool DeleteSources() const;
+ // What the summary shows: the name entered and the icon that will be stored.
+ QString DisplayName() const;
+ QPixmap CurrentIcon() const;
+
+private:
+ void ChooseIcon();
+ void SetIconFromFile(const QString& path);
+ void ResetIcon();
+ void UpdateIconPreview();
+ void UpdateVersionEnabled();
+
+ FileSys::Nup::Metadata detected;
+ QByteArray custom_icon;
+ QPixmap current_icon;
+
+ IconDropZone* icon_preview;
+ QLabel* icon_hint;
+ QPushButton* reset_icon;
+ QLineEdit* name_edit;
+ QRadioButton* version_auto;
+ QRadioButton* version_manual;
+ QLineEdit* version_edit;
+ QCheckBox* low_storage;
+};
+
+// Shown while a conversion runs. The bar spans the dialog; the labels show what is being
+// converted, how much is done and a rough time remaining.
+class NupProgressDialog : public QDialog {
+ Q_OBJECT
+public:
+ NupProgressDialog(const QString& title, const QPixmap& icon, QWidget* parent = nullptr);
+
+ void SetProgress(const FileSys::Nup::PackProgress& progress);
+ void SetCancelling();
+ // Closes the dialog without treating it as a cancel request.
+ void Finish();
+
+signals:
+ void CancelRequested();
+
+protected:
+ void closeEvent(QCloseEvent* event) override;
+ void reject() override;
+
+private:
+ QLabel* status;
+ QLabel* details;
+ QProgressBar* bar;
+ QPushButton* cancel;
+ QElapsedTimer timer;
+ bool cancelling = false;
+ bool finished = false;
+};
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp b/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp
index ecb8171ac7..93bbbfb8fc 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl3.cpp
@@ -38,8 +38,10 @@ EmuWindow_SDL3::EmuWindow_SDL3(InputCommon::InputSubsystem* input_subsystem_, Co
titlebar_timer = SDL_AddTimer(2000, [](void *userdata, SDL_TimerID, Uint32) -> Uint32 {
auto* this_ = (EmuWindow_SDL3*)userdata;
auto const results = this_->system.GetAndResetPerfStats();
- auto const title = fmt::format("{} | {}-{} | FPS: {:.0f} ({:.0f}%)", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc, results.average_game_fps, results.emulation_speed * 100.0f);
- SDL_SetWindowTitle(this_->render_window, title.c_str());
+ auto* const title = new std::string(fmt::format("{} | {}-{} | FPS: {:.0f} ({:.0f}%)", Common::g_build_fullname, Common::g_scm_branch, Common::g_scm_desc, results.average_game_fps, results.emulation_speed * 100.0f));
+ // Timers run on their own thread and AppKit throws when a title is set off the main
+ // thread: ApplyPendingTitle sets it from the main loop.
+ delete this_->pending_title.exchange(title);
return 2000;
}, this);
mouse_timer = SDL_AddTimer(100, [](void *userdata, SDL_TimerID, Uint32) -> Uint32 {
@@ -55,6 +57,7 @@ EmuWindow_SDL3::EmuWindow_SDL3(InputCommon::InputSubsystem* input_subsystem_, Co
EmuWindow_SDL3::~EmuWindow_SDL3() {
SDL_RemoveTimer(titlebar_timer);
SDL_RemoveTimer(mouse_timer);
+ delete pending_title.exchange(nullptr);
system.HIDCore().UnloadInputDevices();
input_subsystem->Shutdown();
SDL_Quit();
@@ -154,6 +157,13 @@ bool EmuWindow_SDL3::IsOpen() const {
return is_open;
}
+void EmuWindow_SDL3::ApplyPendingTitle() {
+ if (std::string* const title = pending_title.exchange(nullptr)) {
+ SDL_SetWindowTitle(render_window, title->c_str());
+ delete title;
+ }
+}
+
bool EmuWindow_SDL3::IsShown() const {
return is_shown;
}
diff --git a/src/yuzu_cmd/emu_window/emu_window_sdl3.h b/src/yuzu_cmd/emu_window/emu_window_sdl3.h
index 0dc2a4983a..a76a2bfd85 100644
--- a/src/yuzu_cmd/emu_window/emu_window_sdl3.h
+++ b/src/yuzu_cmd/emu_window/emu_window_sdl3.h
@@ -6,7 +6,9 @@
#pragma once
+#include <atomic>
#include <cstdlib>
+#include <string>
#include <tuple>
#include <utility>
@@ -41,6 +43,9 @@ public:
/// Whether the window is still open, and a close request hasn't yet been sent
bool IsOpen() const;
+ /// Sets the title the titlebar timer prepared; call from the main thread.
+ void ApplyPendingTitle();
+
/// Returns if window is shown (not minimized)
bool IsShown() const override;
@@ -92,6 +97,7 @@ protected:
/// Periodic changer of titlebar (independent of event loop)
SDL_TimerID titlebar_timer;
+ std::atomic<std::string*> pending_title{};
// Mouse resetter once it
SDL_TimerID mouse_timer;
diff --git a/src/yuzu_cmd/yuzu.cpp b/src/yuzu_cmd/yuzu.cpp
index 22abe6a759..bc4002d8af 100644
--- a/src/yuzu_cmd/yuzu.cpp
+++ b/src/yuzu_cmd/yuzu.cpp
@@ -12,7 +12,9 @@
#include <cstdlib>
#include <chrono>
#include <cstdio>
+#include <csignal>
#include <cstring>
+#include <filesystem>
#include <iostream>
#include <memory>
#include <mutex>
@@ -36,6 +38,7 @@
#include "core/core_timing.h"
#include "core/cpu_manager.h"
#include "core/crypto/key_manager.h"
+#include "core/file_sys/nup_writer.h"
#include "core/file_sys/registered_cache.h"
#include "core/file_sys/vfs/vfs_real.h"
#include "core/hle/service/am/applet_manager.h"
@@ -248,6 +251,49 @@ struct SdlState {
std::unique_ptr<EmuWindow_SDL3> emu_window;
};
+static std::atomic_bool pack_nup_cancel{false};
+
+// eden-cli --pack-nup <dump> [<out.nup>]; the output defaults to <dump>.nup next to the dump.
+static bool PackNup(const std::string& source, const std::string& output) {
+ const auto to_path = [](const std::string& utf8) {
+ return std::filesystem::path{std::u8string{utf8.begin(), utf8.end()}};
+ };
+ const auto dump_root = to_path(source);
+ const auto archive =
+ output.empty() ? FileSys::Nup::DefaultArchivePath(dump_root) : to_path(output);
+ std::signal(SIGINT, [](int) { pack_nup_cancel = true; });
+
+ const auto start = std::chrono::steady_clock::now();
+ auto last_report = start;
+ std::string error;
+ const auto result = FileSys::Nup::PackDump(
+ dump_root, archive, {},
+ [&](const FileSys::Nup::PackProgress& p) {
+ const auto now = std::chrono::steady_clock::now();
+ if (now - last_report < std::chrono::seconds(1) && p.files_done != p.files_total) {
+ return;
+ }
+ last_report = now;
+ fmt::print(stderr, "\r{} / {} MiB, {} / {} files", p.bytes_done >> 20,
+ p.bytes_total >> 20, p.files_done, p.files_total);
+ },
+ &pack_nup_cancel, &error);
+ const auto seconds =
+ std::chrono::duration<double>(std::chrono::steady_clock::now() - start).count();
+ fmt::print(stderr, "\n");
+ switch (result) {
+ case FileSys::Nup::PackResult::Success:
+ fmt::print(stderr, "Wrote {} in {:.1f} s\n", archive.string(), seconds);
+ return true;
+ case FileSys::Nup::PackResult::Cancelled:
+ fmt::print(stderr, "Cancelled\n");
+ return false;
+ default:
+ fmt::print(stderr, "Packing failed: {}\n", error);
+ return false;
+ }
+}
+
extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) {
#ifdef _WIN32
SetUnhandledExceptionFilter(WriteCrashDump);
@@ -298,6 +344,10 @@ extern "C" SDL_AppResult SDL_AppInit(void **appstate, int argc, char **argv) {
Core::ApplyLaunchParams(lp);
+ if (lp.pack_nup_source) {
+ return PackNup(*lp.pack_nup_source, lp.filepath) ? SDL_APP_SUCCESS : SDL_APP_FAILURE;
+ }
+
if (lp.filepath.empty()) {
LOG_CRITICAL(Frontend, "Failed to load ROM: No ROM specified");
return SDL_APP_FAILURE;
@@ -481,6 +531,9 @@ static void RunKeyScript(InputCommon::InputSubsystem& input_subsystem) {
extern "C" SDL_AppResult SDL_AppIterate(void *appstate) {
SdlState *state = (SdlState *)appstate;
RunKeyScript(state->input_subsystem);
+ if (state->emu_window) {
+ state->emu_window->ApplyPendingTitle();
+ }
return state->emu_window && state->emu_window->IsOpen() ? SDL_APP_CONTINUE : SDL_APP_SUCCESS;
}
extern "C" SDL_AppResult SDL_AppEvent(void *appstate, SDL_Event *event) {
diff --git a/tools/integration/game_discovery_test.cpp b/tools/integration/game_discovery_test.cpp
new file mode 100644
index 0000000000..a5cfe0fc5a
--- /dev/null
+++ b/tools/integration/game_discovery_test.cpp
@@ -0,0 +1,75 @@
+// SPDX-License-Identifier: GPL-3.0-or-later
+
+#include "qt_common/game_list/discovery.h"
+#include <chrono>
+#include <cstdlib>
+#include <fstream>
+#include <iostream>
+#include <source_location>
+
+namespace fs = std::filesystem;
+
+void Check(bool condition,
+ std::source_location location = std::source_location::current()) {
+ if (!condition) {
+ std::cerr << "Game discovery regression failed at line " << location.line()
+ << '\n';
+ std::exit(1);
+ }
+}
+
+int main() {
+ const auto temp = fs::temp_directory_path();
+ const auto root =
+ temp / ("eglantine-discovery-" +
+ std::to_string(
+ std::chrono::steady_clock::now().time_since_epoch().count()));
+ Check(fs::equivalent(root.parent_path(), temp) && fs::create_directory(root));
+ struct Cleanup {
+ fs::path path;
+ ~Cleanup() { fs::remove_all(path); }
+ } cleanup{root};
+ const auto touch = [](const fs::path &path) {
+ fs::create_directories(path.parent_path());
+ std::ofstream{path}.put('x');
+ };
+ const auto dump = [&](const fs::path &path) {
+ touch(path / "exefs/main");
+ touch(path / "exefs/main.npdm");
+ };
+ const auto supported = [](const fs::path &path) {
+ return path.filename() == "main" || path.extension() == ".nsp";
+ };
+ const auto discover = [&](const fs::path &path, bool deep) {
+ return GameListDiscovery::Discover(path, deep, supported,
+ [] { return false; });
+ };
+
+ const auto game = root / fs::path{u8"MKW-églantine"};
+ dump(game / "base");
+ dump(game / "updates/0100000000000800");
+ for (unsigned i = 0; i < 400; ++i) {
+ touch(game / "base/romfs/assets" / std::to_string(i) / "false-game.nsp");
+ }
+ touch(root / "standalone.nsp");
+ touch(root / "unrelated/romfs/actual-game.nsp");
+
+ const auto deep = discover(root, true);
+ Check(deep.files.size() == 4 && deep.skipped_romfs == 1 && deep.errors == 0);
+ Check(deep.directories.size() < 20);
+ Check(discover(root, false).files.size() == 3);
+ Check(discover(game / "base", false).files.size() == 1);
+ Check(discover(game / "base/exefs", false).files.size() == 1);
+ Check(discover(root / "missing", true).files.empty());
+
+ touch(root / "incomplete/exefs/main");
+ touch(root / "incomplete/romfs/actual-game.nsp");
+ Check(discover(root, true).files.size() == 6);
+
+ unsigned calls{};
+ const auto cancelled = GameListDiscovery::Discover(
+ root, true, supported, [&] { return ++calls > 3; });
+ Check(calls < 10 && cancelled.directories.size() <= 1);
+ std::cout << "Discovery: loose layouts, payload pruning, Unicode, shallow "
+ "scans and cancellation passed\n";
+}