Compare commits

...

93 Commits

Author SHA1 Message Date
nmzik 9e41d8320a shader: support returning 64-bit LDS max and compare-exchange 2026-10-13 21:33:50 +00:00
nmzik e998f21e71 shader: reuse EXEC proof contexts across shared predicate graphs 2026-10-13 21:33:50 +00:00
nmzik 3197ef093c test: align scalar image tables and buffer reads with PS5 semantics 2026-10-13 21:33:50 +00:00
nmzik fa0e81bfba shader: zero out-of-range scalar descriptor reads 2026-10-13 21:33:50 +00:00
nmzik 01ce973882 renderer: preserve guest writes to reused metadata buffers 2026-10-13 21:33:50 +00:00
nmzik f57d2c0965 shader: keep unbounded material selectors on bounded GPU heaps 2026-10-13 21:33:50 +00:00
nmzik 735a229236 shader: preserve native types in indirect image tables 2026-10-13 21:33:50 +00:00
nmzik 9ade7e3582 shader: prove inactive resources across masked traversal merges 2026-10-13 21:33:50 +00:00
nmzik 259a40abfc memory: honor sparse AMM unmaps and restore failed GPU ranges 2026-10-13 21:33:50 +00:00
nmzik f1577a5a94 shader: memoize bounded material record bit analysis 2026-10-13 21:33:50 +00:00
nmzik d08a6cef64 shader: specialize guarded packed material selectors 2026-10-13 21:33:50 +00:00
nmzik 907c65f54c shader: track material offsets scaled after lane selection 2026-10-13 21:33:50 +00:00
nmzik fe6499b86e shader: preserve clamped scalar material selector provenance 2026-10-13 21:33:50 +00:00
nmzik 6309ac838d shader: restrict shared-list descriptor bounds to overlapping producers 2026-10-13 21:33:50 +00:00
nmzik 19799b3079 shader: simplify invariant execution masks in descriptor proofs 2026-10-13 21:33:50 +00:00
nmzik 4e16234d32 shader: bound signed image loops from their initial descriptor index 2026-10-13 21:33:50 +00:00
Ali Almohaya 3c9da9c3f4 graphics: decode both channels of RG32 float metadata clears 2026-10-13 21:33:50 +00:00
nmzik ff6dffe298 graphics: decode packed unsigned float CMASK register clears
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
2026-10-13 21:33:50 +00:00
nmzik 5f6dc0a8d0 shader: stream tile bitmaps within their fixed descriptor domain 2026-10-13 21:33:50 +00:00
nmzik 34893dd8c8 shader: recognize unconditional predicates in implication proofs 2026-10-13 21:33:50 +00:00
nmzik 50595d1c13 shader: recover counted image rows through private LDS spills 2026-10-13 21:33:50 +00:00
nmzik 717c54e085 gpu: match complete NOP marker tags and ignore opaque payloads
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
2026-10-13 21:33:50 +00:00
nmzik 31b7be9ff2 graphics: materialize fast clears before sampling reused targets
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
2026-10-13 21:27:07 +00:00
nmzik 40692dfb64 graphics: copy sparse buffer uploads through resident mappings 2026-10-13 21:27:07 +00:00
nmzik 9a3aca963a shader: combine image execution masks with descriptor branch bounds 2026-10-13 21:27:07 +00:00
nmzik 1fa79d5cac shader: specialize bounded pointer and packed image tables 2026-10-13 21:27:07 +00:00
nmzik c9ec82e32e shader: factor live execution guards before resource planning 2026-10-13 21:27:07 +00:00
nmzik 2f88a0a3b2 shader: elide bitcasts between identical physical types 2026-10-13 21:27:07 +00:00
nmzik 7e884f55e0 shader: reuse dominating descriptor loads across image samples 2026-10-13 21:27:07 +00:00
nmzik d70cbd4184 shader: bound descriptor worklists by their finite bit masks 2026-10-13 21:27:07 +00:00
nmzik 9bcf80bf67 graphics: share temporary staging for oversized image uploads 2026-10-13 21:27:07 +00:00
nmzik ed1195d1e7 shader: prove descriptor bounds from simplified image EXEC predicates 2026-10-13 21:27:07 +00:00
nmzik 25e1cc7ec9 shader: preserve descriptor bounds through guarded branch joins 2026-10-13 21:27:07 +00:00
nmzik a287a54f26 shader: prove guarded descriptor counts for interleaved image records 2026-10-13 21:27:07 +00:00
nmzik 56738aa4e1 shader: sample packed normalized images with one bilinear gather 2026-10-13 21:27:07 +00:00
nmzik c11254a996 shader: share format conversion for packed normalized image stores 2026-10-13 21:27:07 +00:00
nmzik aec8c88755 graphics: support signed 32-bit image atomic descriptors 2026-10-13 21:27:07 +00:00
nmzik 68d7bc94e2 shader: correlate masked first-lane material rows 2026-10-13 21:27:07 +00:00
nmzik 8ee31abd9d shader: bound indirect image selectors by counted scalar rows 2026-10-13 21:27:07 +00:00
nmzik 9a7c54da58 shader: share formatted buffer payload reads across format conversions 2026-10-13 21:27:07 +00:00
nmzik 5dc6914688 shader: lower bounded UInt formatted buffer copies to shared stores 2026-10-13 21:27:07 +00:00
nmzik a7a734b5d3 video: implement PS5 Vdecsw using a shared queued AVC decoder
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
2026-10-13 21:27:07 +00:00
nmzik e2c8b61de6 shader: remove unreachable mixed image specialization paths 2026-10-13 21:27:07 +00:00
nmzik 145578ac03 shader: materialize image keys from proven GPU bitmap subsets 2026-10-13 21:27:07 +00:00
nmzik f01a3750d4 shader: validate indirect image operands at each typed access 2026-10-13 21:27:07 +00:00
nmzik 0e9c77b0a2 shader: bound material selectors by proven one-wave LDS append lists 2026-10-13 21:27:07 +00:00
nmzik d692d8ee08 shader: preserve full region addresses for LDS and GDS reads and writes 2026-10-13 21:27:07 +00:00
Ali Almohaya 460a196fca shader: emit dynamic sample offsets with the enabled maintenance8 feature 2026-10-13 21:27:07 +00:00
nmzik 652154aead shader: merge typed image candidates as raw vectors with shared sampler variants 2026-10-13 21:27:07 +00:00
nmzik 49ebbfd627 shader: prove descending image ranges and prune impossible register selectors
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
2026-10-13 21:27:07 +00:00
nmzik 7688093ca3 shader: resolve GPU scalar image selectors within buffer bounds
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
2026-10-13 21:27:07 +00:00
nmzik df08b83287 shader: propagate late constants through affected IR users 2026-10-13 21:27:07 +00:00
nmzik fa68e104ea shader: fold full wave64 masks before register predication 2026-10-13 21:27:07 +00:00
nmzik fd66f580b4 shader: resolve packed buffer selectors with shared indexed stores
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
2026-10-13 21:27:07 +00:00
nmzik 8f7d745383 graphics: simplify image usage and dirty state 2026-10-13 21:27:07 +00:00
nmzik f4692a450e graphics: avoid full-cache scans when deleting stencil associations
Co-authored-by: overlordxrz-source <overlordxrz-source@users.noreply.github.com>
Co-authored-by: TheCruZ <thecruzwtf@gmail.com>
2026-10-13 21:27:07 +00:00
nmzik df5b9b92be minor cleanup 2026-10-12 15:06:38 +02:00
Devran Cosmo Uenal 5b0ef07742
audio: keep NGS2 sampler voices within max_waveform_blocks (#1450)
audio: preserve NGS2 streaming buffer ownership
2026-10-12 14:22:06 +02:00
Ettore Sartori 5ce9a52936
Fix the splash screen (#1378)
Fix swapchain acquire semaphore reuse

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-12 13:40:59 +02:00
nmzik dad900c61d Fix BC alias writes and GC 2026-10-12 04:51:02 +02:00
nmzik 5b730d4df0 simplify guest gpu 2026-10-12 02:52:49 +02:00
nmzik e699a35f20 netctl: add opt-in host IPv4 connectivity and launcher toggle 2026-10-12 01:20:43 +02:00
nmzik 41beda4367 ampr: fix APR completion loss in 768-byte read batches
Co-authored-by: Chai T <ct@talnikar.in>
2026-10-12 01:20:43 +02:00
Chai T 048fe9f413 kernel: export sceKernelFtruncate through existing native handler 2026-10-12 01:20:43 +02:00
nmzik e51de54ee4 add hdr display support 2026-10-11 20:29:32 +02:00
nmzik 984cfcebd1 shader: emit native texel offsets using sample execution context
Co-authored-by: netwarex <mail@pety.me>
2026-10-11 18:53:01 +02:00
nmzik df0412afb5 shader: preserve shared predicates during resource plan extraction
Co-authored-by: netwarex <mail@pety.me>
2026-10-11 18:53:01 +02:00
nmzik f28c0fccb2 texture: preserve guest DCC metadata for comparison depth aliases
Co-authored-by: netwarex <mail@pety.me>
2026-10-11 18:53:01 +02:00
netwarex 4c87dbebb2 shader: select indirect texture descriptors for image gathers 2026-10-11 18:53:01 +02:00
nmzik 16a7fe689a
fix windows compilation (2) (#1411)
* fix windows compialtion (2)

* loader: guard LoadExec close_range path with __linux__ for macOS

---------

Co-authored-by: ReverendOzmund <catmilker@proton.me>
2026-10-11 11:11:02 +02:00
thenotsoquietninja44-collab 6f0cddd440 implement LoadExec (windows) 2026-10-11 08:24:33 +02:00
nmzik 4084a008ea fix windows compilation 2026-10-11 07:53:59 +02:00
HamsterCL c0ebf22476
tests: rename a local that collides with the Windows small macro (#1400)
windows.h pulls in rpcndr.h, which defines `small` as `char`. The R64 BC1
array case added in b7b335ac declared `const auto small`, so the Windows
build of shader_recompiler_compute_tests failed to compile:

  ShaderRecompilerComputeTests.cpp(17928,24): error: expected unqualified-id

Rename the local to `small_array`.
2026-10-11 09:35:18 +02:00
nmzik cb375ec35a add load exec
Co-authored-by: thenotsoquietninja44-collab <thenotsoquietninja44@gmail.com>
2026-10-11 09:31:31 +02:00
nmzik 6f3bebcb50 playgo: read plgx chunks 2026-10-11 09:31:31 +02:00
nmzik 1029511bbc add depth to color copy
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-10-11 09:31:31 +02:00
nmzik 65144ce4ac gpu: centralize depth copy state validation in target resolution 2026-10-11 09:31:31 +02:00
nmzik f848d2761b add srt walker 2026-10-11 09:31:31 +02:00
ReverendOzmund 730e1a3cf3
docs: add Fedora and Fedora Atomic (Bazzite) build instructions (#1372)
* docs: add Fedora and Fedora Atomic (Bazzite) build instructions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SscrMzrvwrj17PJsfzqog9

* docs: note --nvidia for Distrobox on NVIDIA hosts

* docs: streamline Fedora build instructions

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-11 03:24:15 +02:00
nmzik d4bb7fd786 agc: remove command buffer debug dump 2026-10-11 03:03:26 +02:00
popcoder565 fbea51c684
agc: implement sceAgcSetNop (#1343)
* agc: implement sceAgcSetNop

Star Wars Outlaws calls sceAgcSetNop (K2mciNVxUCE) and crashes right after
the unresolved stub returns, without the packet being disabled.

The call turns a packet into a NOP of the same length. Clear the emulator's
custom NOP code as well, so packets that the HLE encodes as NOPs, such as
end-of-pipe actions, stop executing too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* agc: keep packets disabled by sceAgcSetNop from reading as markers

A disabled packet whose first payload word carries the 0x6875 marker
signature still reached the marker handler, which can flip or stop on an
unknown marker ID. Clear the signature so the packet is skipped like any
other NOP.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* agc: tidy SetNop logging and comments

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-11 03:00:09 +02:00
Devran Cosmo Uenal a6f4e8e7f4 AvPlayer: recycle output buffers so the decoder can run ahead 2026-10-11 02:08:27 +02:00
nmzik 448a4c8a6a launcher: reorganize panels 2026-10-11 01:44:04 +02:00
nmzik ece4e28740 gpu: store private format byte sizes in uint8_t 2026-10-11 01:25:47 +02:00
nmzik 58ba1e4cb4 docs: update README wording and grammar 2026-10-11 01:13:14 +02:00
nmzik cf77fb4353 shader: initialize NGG vertex count from active native invocations 2026-10-11 01:05:56 +02:00
nmzik 7e4e142aae gpu: enable native R8 SNORM texture sampling 2026-10-11 01:05:56 +02:00
nmzik b7b335ac63 gpu: admit block-compressed textures in PS5 R64 tiling 2026-10-11 01:05:56 +02:00
nmzik 9a5dd9faff agc: reserve driver backing before guest direct-memory allocation 2026-10-11 01:05:56 +02:00
nmzik 947627d10d docs: clarify pull request scope 2026-10-10 22:47:36 +02:00
Kelvin Oliveira 1cfb9522a3
controller: use Back as the touchpad click on pads without a touchpad (#1355)
* controller: use Back as the touchpad click on pads without a touchpad

Gamepads without a touchpad (Switch, Xbox, 8BitDo, ...) cannot click it, so
games that require the touchpad can't be played with them. The keyboard
already emulates it (Backspace/Tab); gamepads had nothing.

For SDL_GAMEPAD_BUTTON_BACK on a pad that reports no touchpads, report
PAD_BUTTON_TOUCH_PAD plus a finger at the centre of the touchpad. Pads with a
real touchpad are unchanged.

Fixes #1298

* controller: establish touch before fallback click

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-10 19:20:34 +02:00
nmzik f63d3befdc Add startup splash screen 2026-10-10 19:07:06 +02:00
nmzik 307fdcf7ad
add self updater (#1352)
* add self updater

* remove updater tests after validation
2026-10-10 05:42:32 +02:00
111 changed files with 12585 additions and 2702 deletions

View File

@ -257,6 +257,7 @@ jobs:
"$install_dir/libMoltenVK.dylib" \
"$app_dir/Contents/MacOS/KytyPS5" \
"$app_dir/Contents/MacOS/kyty_emulator" \
"$app_dir/Contents/MacOS/kyty_updater" \
"$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do
file "$binary"
lipo "$binary" -verify_arch x86_64
@ -266,6 +267,7 @@ jobs:
"$install_dir/libMoltenVK.dylib" \
"$app_dir/Contents/MacOS/KytyPS5" \
"$app_dir/Contents/MacOS/kyty_emulator" \
"$app_dir/Contents/MacOS/kyty_updater" \
"$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do
codesign --verify --strict "$binary"
done
@ -407,6 +409,7 @@ jobs:
run: |
file _Build/linux/install/launcher
file _Build/linux/install/kyty_emulator
test -x _Build/linux/install/kyty_updater
file _Build/linux/install/kyty_emulator | grep -q "ELF 64-bit LSB .*x86-64"
ldd _Build/linux/install/kyty_emulator > /dev/null
readelf -d _Build/linux/install/launcher | grep -q 'RPATH.*\$ORIGIN/lib'
@ -495,7 +498,8 @@ jobs:
test -d "$linux_dir"
chmod a+x \
"$linux_dir/launcher" \
"$linux_dir/kyty_emulator"
"$linux_dir/kyty_emulator" \
"$linux_dir/kyty_updater"
tar -C "$linux_dir" -czf \
"$GITHUB_WORKSPACE/$RELEASE_NAME-Linux-x86_64.tar.gz" .

View File

@ -40,6 +40,11 @@ if (NOT (WIN32 OR LINUX OR APPLE))
message(FATAL_ERROR "only Windows, Linux, and macOS builds are supported")
endif()
if(WIN32)
# Prevent Windows headers from defining min/max macros in all targets.
add_compile_definitions(NOMINMAX)
endif()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
@ -298,28 +303,27 @@ list(APPEND kyty_emulator_src "${fault_buffer_shader_header}")
list(APPEND kyty_emulator_src ${gpu_tiler_shader_headers})
set(gpu_blit_generated_dir "${PROJECT_BINARY_DIR}/gpu_blit_shaders")
set(gpu_blit_shader_sources
"${gpu_tiler_shader_dir}/gpu_blit_fs_triangle.vert"
"${gpu_tiler_shader_dir}/gpu_blit_color_to_ms_depth.frag"
"${gpu_tiler_shader_dir}/gpu_video_out_overlay.frag"
)
foreach(shader_source IN LISTS gpu_blit_shader_sources)
get_filename_component(shader_name "${shader_source}" NAME_WE)
function(add_gpu_blit_shader shader_name shader_file)
set(shader_source "${gpu_tiler_shader_dir}/${shader_file}")
set(shader_spv "${gpu_blit_generated_dir}/${shader_name}.spv")
set(shader_header "${gpu_blit_generated_dir}/${shader_name}_spv.h")
string(TOUPPER "${shader_name}_SPV" shader_symbol)
add_custom_command(
OUTPUT "${shader_header}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${gpu_blit_generated_dir}"
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os ${ARGN}
"-I${gpu_tiler_shader_dir}" -o "${shader_spv}" "${shader_source}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${shader_spv} -DOUTPUT=${shader_header}
-DSYMBOL=${shader_symbol} -P "${KYTY_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${shader_source}"
VERBATIM
)
list(APPEND gpu_blit_shader_headers "${shader_header}")
endforeach()
set(gpu_blit_shader_headers ${gpu_blit_shader_headers} "${shader_header}" PARENT_SCOPE)
endfunction()
add_gpu_blit_shader(gpu_blit_fs_triangle gpu_blit_fs_triangle.vert)
add_gpu_blit_shader(gpu_blit_color_to_ms_depth gpu_blit_color_to_ms_depth.frag)
add_gpu_blit_shader(gpu_video_out_overlay gpu_video_out_overlay.frag)
add_gpu_blit_shader(gpu_imgui_hdr gpu_video_out_overlay.frag -DIMGUI_HDR=1)
list(APPEND kyty_emulator_src ${gpu_blit_shader_headers})
list(APPEND kyty_emulator_src
@ -399,7 +403,7 @@ add_executable(scalar_provenance_tests EXCLUDE_FROM_ALL
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/SrtWalker.cpp"
)
target_link_libraries(scalar_provenance_tests fmt::fmt)
target_link_libraries(scalar_provenance_tests fmt::fmt xbyak::xbyak)
target_include_directories(scalar_provenance_tests PRIVATE ${inc_headers})
add_executable(page_manager_tests EXCLUDE_FROM_ALL
@ -428,6 +432,14 @@ add_executable(avplayer_file_tests EXCLUDE_FROM_ALL
target_link_libraries(avplayer_file_tests common FFmpeg::ffmpeg ZArchive::zarchive)
target_include_directories(avplayer_file_tests PRIVATE ${inc_headers})
add_executable(vdecsw_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/VdecswTests.cpp"
"${KYTY_SOURCE_DIR}/libs/videoDec2Decoder.cpp"
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
)
target_link_libraries(vdecsw_tests common FFmpeg::ffmpeg)
target_include_directories(vdecsw_tests PRIVATE ${inc_headers})
add_executable(bit_array_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/BitArrayTests.cpp"
)
@ -467,7 +479,7 @@ add_executable(resource_materialization_tests EXCLUDE_FROM_ALL
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/SrtWalker.cpp"
)
target_link_libraries(resource_materialization_tests fmt::fmt)
target_link_libraries(resource_materialization_tests fmt::fmt xbyak::xbyak)
target_include_directories(resource_materialization_tests PRIVATE ${inc_headers})
add_executable(resource_tracking_tests EXCLUDE_FROM_ALL
@ -480,7 +492,7 @@ add_executable(resource_tracking_tests EXCLUDE_FROM_ALL
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp"
)
target_link_libraries(resource_tracking_tests fmt::fmt)
target_link_libraries(resource_tracking_tests fmt::fmt xbyak::xbyak)
target_include_directories(resource_tracking_tests PRIVATE ${inc_headers})
add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
@ -618,6 +630,7 @@ if(BUILD_TESTING)
add_test(NAME archive_file COMMAND $<TARGET_FILE:archive_file_tests>)
add_test(NAME trophy_system COMMAND $<TARGET_FILE:trophy_system_tests>)
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_tests>)
add_test(NAME vdecsw COMMAND $<TARGET_FILE:vdecsw_tests>)
add_test(NAME bit_array COMMAND $<TARGET_FILE:bit_array_tests>)
add_test(NAME lru_cache COMMAND $<TARGET_FILE:lru_cache_tests>)
add_test(NAME shader_vertex_metadata COMMAND $<TARGET_FILE:shader_vertex_metadata_tests>)
@ -694,6 +707,7 @@ if(BUILD_TESTING)
archive_file_tests
trophy_system_tests
avplayer_file_tests
vdecsw_tests
bit_array_tests
lru_cache_tests
shader_vertex_metadata_tests

View File

@ -7,12 +7,12 @@
[![Status](https://img.shields.io/badge/status-active%20development-orange.svg)](#current-status)
[![License](https://img.shields.io/badge/license-GPL--2.0-blue.svg)](LICENSE)
**[Weekly updates](https://github.com/KytyPS5/KytyPS5/discussions/862)** — game progress, recent fixes and ongoing development.
**[Weekly updates](https://github.com/KytyPS5/KytyPS5/discussions/862)** — game progress, recent fixes, and ongoing development.
**[Development on Discord](https://discord.gg/UNrkMqGaBg)** — KytyPS5 development.
KytyPS5 is a free and open-source PlayStation 5 emulator written in C++ for Windows and Linux,
with experimental macOS support. It is based on a heavily modified version of
KytyPS5 is a free and open-source PlayStation 5 translation layer written in C++ for Windows
and Linux, with experimental macOS support. It is based on a heavily modified version of
[Kyty](https://github.com/InoriRus/Kyty). The project is in active development, and behavior
can change significantly between builds.
@ -23,13 +23,13 @@ can change significantly between builds.
## Current Status
KytyPS5 can boot 2D games and a selection of 3D games, including titles built with Unreal Engine
KytyPS5 can boot 2D and 3D games, including titles built with Unreal Engine
4/5, Unity, and custom engines. External low-level emulation modules are neither required nor
planned.
Development is currently focused on expanding game compatibility and improving boot reliability.
Windows and Linux are the primary platforms and receive the most testing.
Windows and Linux are the primary target platforms and receive the most testing.
macOS support is experimental. The emulator is built for x86-64 and runs on Apple Silicon under
Rosetta 2, with Vulkan provided by MoltenVK. A small number of titles have been verified in-game
@ -85,14 +85,20 @@ or graphical glitches, so please include the version you tested when reporting a
Testing games and submitting detailed bug reports are useful ways to contribute. Search existing
issues first, then use the **Game Emulation Status Report** template and attach the complete log file.
Code contributions should be focused, build successfully on the platforms they touch, and include
relevant tests where practical. Windows is the primary target, so a change that alters shared code
should not regress it; changes confined to a platform's own code paths only need to build there. Because KytyPS5 is still evolving quickly, consider opening an issue before
starting a large change.
**Pull requests for general fixes, architectural changes, or refactoring that do not address a concrete bug or launch problem in a specific game will be automatically closed.**
**Large, unfocused commits or pull requests that bundle unrelated changes without a clear purpose or game-related benefit will also be automatically closed.**
Keep each PR focused. Name the affected game, describe the exact problem, and provide evidence showing what the change fixes.
Windows and Linux are the primary target platforms, so changes to shared code should not
introduce regressions on either platform. Changes confined to a platform's own code paths
only need to build on that platform. Because KytyPS5 is still evolving quickly, consider
opening an issue before starting a large change.
### Formatting
Set up the clang-format hook after cloning:
Set up the clang-format hook after cloning.
Install `pre-commit` using the method appropriate for your platform:
@ -171,9 +177,9 @@ The finished application and its runtime dependencies will be placed in
### Building on Linux
Install the toolchain and the libraries the bundled SDL3 needs. Without the audio, Wayland and
udev development packages SDL3 quietly configures itself without those backends, and the resulting
build has no working sound and no gamepad hotplug:
Install the toolchain and the libraries the bundled SDL3 needs. Without the audio, Wayland, and
udev development packages, SDL3 quietly configures itself without those backends, and the resulting
build has no working audio or gamepad hotplug support:
```bash
sudo apt-get install --no-install-recommends \
@ -183,8 +189,8 @@ sudo apt-get install --no-install-recommends \
libasound2-dev libpulse-dev libudev-dev libdbus-1-dev libwayland-dev wayland-protocols
```
Qt 6 (Concurrent, Network, Widgets) is required for the launcher — either the distribution packages
(`qt6-base-dev`) or an official Qt installation.
Qt 6 (Concurrent, Network, and Widgets) is required for the launcher. Use your distribution's
packages (`qt6-base-dev`) or an official Qt installation.
```bash
git submodule update --init --recursive
@ -198,7 +204,7 @@ cmake --install _Build/linux --prefix _Build/linux/install
```
The install step copies the Qt libraries and plugins next to the binaries, so
`_Build/linux/install` runs without a matching system Qt. FFmpeg is linked statically
`_Build/linux/install` runs without a matching system Qt installation. FFmpeg is linked statically
from the pinned [KytyPS5 FFmpeg core](https://github.com/KytyPS5/ext-ffmpeg-core)
release, including VP9 and WebM support. System FFmpeg packages are not required.
@ -242,6 +248,43 @@ sources, so it needs network access; a fully sandboxed `nix build` would require
inputs. A Vulkan 1.3 driver must be available at runtime (on NixOS,
`hardware.graphics.enable = true`).
### Building on Fedora and Fedora Atomic (Bazzite, Bluefin, Silverblue)
On regular Fedora, install dependencies on the host. On Fedora Atomic variants such as Bazzite,
Bluefin and Silverblue, use [Distrobox](https://distrobox.it/) and run the install and build steps
inside the container. Keep the checkout in your home directory so it is shared with the container.
Add `--nvidia` to the create command for hosts using the proprietary NVIDIA driver:
```bash
distrobox create --name kyty-build --image registry.fedoraproject.org/fedora:latest
distrobox enter kyty-build
```
Install the toolchain, Qt 6 and SDL3 dependencies:
```bash
sudo dnf install -y \
clang lld ninja-build cmake git glslang python3 pkgconf-pkg-config \
mesa-libGL-devel libX11-devel libXcursor-devel libXext-devel libXfixes-devel \
libXi-devel libXrandr-devel libXScrnSaver-devel libXtst-devel libxkbcommon-devel \
alsa-lib-devel pulseaudio-libs-devel systemd-devel dbus-devel \
wayland-devel wayland-protocols-devel qt6-qtbase-devel
```
From the repository root, follow the launcher configure, build and install steps in
[Building on Linux](#building-on-linux), omitting `-DCMAKE_PREFIX_PATH="$Qt6_DIR"`; CMake finds
Fedora's packaged Qt 6 automatically.
Run `./_Build/linux/install/launcher` on regular Fedora or inside the container. To launch a
container build from the host, leave the container with `exit`, then run from the repository root:
```bash
distrobox enter kyty-build -- ./_Build/linux/install/launcher
```
Qt libraries and plugins are bundled. You can also launch directly on a Fedora Atomic host
if its other system libraries are compatible with the container build.
### Building on macOS
macOS builds target x86-64 and run under Rosetta 2 on Apple Silicon, so the PS5's x86-64 game
@ -253,7 +296,7 @@ Requirements:
- An Apple Silicon Mac with Rosetta 2 installed (`softwareupdate --install-rosetta`)
- Xcode (or the Command Line Tools)
- Homebrew packages: `brew install cmake ninja glslang python`
- Qt 6 (Concurrent, Network, Widgets) with x86-64 support. The official Qt installation is
- Qt 6 (Concurrent, Network, and Widgets) with x86-64 support. The official Qt installation is
universal and works; Homebrew's Qt is arm64-only and will not link
```bash
@ -269,14 +312,14 @@ cmake --install _Build/macos --prefix _Build/macos/install
```
The build re-signs `kyty_emulator` with the JIT entitlements it needs to execute translated
guest code; no manual signing step is required. When the launcher is built, the install
guest code; no manual signing step is required. When the launcher is built, the installation
also produces `_Build/macos/install/KytyPS5.app` — double-click to launch the GUI.
A flat `kyty_emulator` is kept for CLI usage.
Vulkan comes from MoltenVK. Download `MoltenVK-macos.tar` from the
[MoltenVK releases](https://github.com/KhronosGroup/MoltenVK/releases), then copy
`MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib` next to the flat `kyty_emulator`
(and, for the bundle, into `KytyPS5.app/Contents/Frameworks/`) and ad-hoc sign it:
(and, for the bundle, into `KytyPS5.app/Contents/Frameworks/`) and sign it with an ad hoc signature:
```bash
codesign --force --sign - _Build/macos/install/libMoltenVK.dylib
@ -338,6 +381,14 @@ folders recursively for game directories containing `eboot.bin` and ZArchive (`.
whose archive root contains `eboot.bin`. Select a detected game and run it from the game list.
ZArchive dumps are mounted read-only and streamed directly; they do not need to be extracted first.
Official release builds include a built-in updater. Leave **Check for updates on startup** enabled,
or click **Check for updates**, then choose **Install Update**. The launcher downloads the package
for your platform from GitHub, checks its SHA-256 checksum, and restarts after installation. Close
any running games first. Settings, saves, patches, and other files outside the release package are
preserved; replacement failures restore the previous files. The installation folder must be writable.
If a package cannot be installed automatically, the updater offers the GitHub release page.
Source builds and builds from forks do not replace themselves with official releases.
The emulator can also be started directly with a legally obtained game directory, ELF file, or
ZArchive dump:
@ -393,5 +444,5 @@ licenses included with those components.
- [InoriRus/Kyty](https://github.com/InoriRus/Kyty) — KytyPS5 is based on a heavily modified version
of the original Kyty project.
- [shadps4-emu/shadPS4](https://github.com/shadps4-emu/shadPS4) — reference for understanding PS4
memory behavior, GPU resource aliasing and cache coherency,
memory behavior, GPU resource aliasing, cache coherency,
and the AVPlayer implementation.

View File

@ -72,6 +72,10 @@ bool FullscreenEnabled() {
return g_config->fullscreen_enabled;
}
bool HdrEnabled() {
return g_config->hdr_enabled;
}
bool HideCursorEnabled() {
return g_config->hide_cursor_enabled;
}
@ -96,6 +100,10 @@ bool VulkanValidationEnabled() {
return g_config->vulkan_validation_enabled;
}
bool IsNetworkConnected() {
return g_config->network_connected;
}
bool ShaderValidationEnabled() {
return g_config->shader_validation_enabled;
}
@ -182,4 +190,8 @@ const Keymap& GetKeymap() {
return g_config->keymap;
}
const std::vector<std::string>& GetGuestArguments() {
return g_config->guest_arguments;
}
} // namespace Config

View File

@ -53,9 +53,12 @@ struct ConfigOptions {
PresentMode present_mode = PresentMode::Mailbox;
int32_t gpu_index = -1;
bool fullscreen_enabled = false;
bool hdr_enabled = false;
bool hide_cursor_enabled = false;
bool splash_enabled = true;
bool vr_enabled = false;
bool amd_cpu_enabled = false;
bool network_connected = false;
uint32_t vblank_frequency = 60;
uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@ -82,6 +85,8 @@ struct ConfigOptions {
bool red_zone_protection_enabled = false;
#endif
Keymap keymap;
// Ordinary boots need a non-null argv[0]; --guest-exec supplies the caller's arguments.
std::vector<std::string> guest_arguments {""};
};
void Load(const ConfigOptions& cfg);
@ -97,9 +102,11 @@ uint32_t GetControllerVibrationIntensity();
PresentMode GetPresentMode();
int32_t GetGpuIndex();
bool FullscreenEnabled();
bool HdrEnabled();
bool HideCursorEnabled();
bool VrEnabled();
bool AmdCpuEnabled();
bool IsNetworkConnected();
uint32_t GetVblankFrequency();
uint32_t GetConsoleLanguage();
bool VulkanValidationEnabled();
@ -135,6 +142,7 @@ bool RedZoneProtectionEnabled();
#endif
const Keymap& GetKeymap();
const std::vector<std::string>& GetGuestArguments();
} // namespace Config

View File

@ -8,10 +8,6 @@
#include <condition_variable> // IWYU pragma: keep
#include <mutex>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS && KYTY_COMPILER == KYTY_COMPILER_CLANG
#define KYTY_WIN_CS
#endif
// macOS has no clock_nanosleep.
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS && !defined(__APPLE__)
#define KYTY_POSIX_HIGH_RES_SLEEP
@ -22,7 +18,7 @@
#include <string>
#include <thread>
#ifdef KYTY_WIN_CS
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#include <windows.h> // IWYU pragma: keep
// IWYU pragma: no_include <winbase.h>
constexpr DWORD KYTY_CS_SPIN_COUNT = 4000;
@ -133,7 +129,7 @@ namespace Common {
using thread_id_t = std::thread::id;
struct MutexPrivate {
#ifdef KYTY_WIN_CS
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
MutexPrivate() { InitializeCriticalSectionAndSpinCount(&m_cs, KYTY_CS_SPIN_COUNT); }
~MutexPrivate() { DeleteCriticalSection(&m_cs); }
KYTY_CLASS_NO_COPY(MutexPrivate);
@ -144,7 +140,7 @@ struct MutexPrivate {
};
struct CondVarPrivate {
#ifdef KYTY_WIN_CS
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
CondVarPrivate() {
static auto func = ResolveInitializeConditionVariable();
EXIT_NOT_IMPLEMENTED(func == nullptr);
@ -216,7 +212,7 @@ void Thread::Detach() {
}
void Thread::SleepMicro(uint32_t micros) {
#ifdef KYTY_WIN_CS
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
SleepHighResolution100ns(static_cast<uint64_t>(micros) * 10);
#elif defined(KYTY_POSIX_HIGH_RES_SLEEP)
SleepHighResolutionNanos(static_cast<uint64_t>(micros) * 1000);
@ -226,7 +222,7 @@ void Thread::SleepMicro(uint32_t micros) {
}
void Thread::SleepNano(uint64_t nanos) {
#ifdef KYTY_WIN_CS
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
SleepHighResolution100ns((nanos + 99) / 100);
#elif defined(KYTY_POSIX_HIGH_RES_SLEEP)
SleepHighResolutionNanos(nanos);
@ -262,7 +258,7 @@ Mutex::~Mutex() {
}
void Mutex::Lock() {
#ifdef KYTY_WIN_CS
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
EnterCriticalSection(&m_mutex->m_cs);
#else
m_mutex->m_mutex.lock();
@ -270,7 +266,7 @@ void Mutex::Lock() {
}
void Mutex::Unlock() {
#ifdef KYTY_WIN_CS
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
LeaveCriticalSection(&m_mutex->m_cs);
#else
m_mutex->m_mutex.unlock();
@ -278,7 +274,7 @@ void Mutex::Unlock() {
}
bool Mutex::TryLock() {
#ifdef KYTY_WIN_CS
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
return (TryEnterCriticalSection(&m_mutex->m_cs) != 0);
#else
return m_mutex->m_mutex.try_lock();
@ -292,7 +288,7 @@ CondVar::~CondVar() {
}
void CondVar::Wait(Mutex* mutex) {
#ifndef KYTY_WIN_CS
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
std::unique_lock<std::recursive_mutex> cpp_lock(mutex->m_mutex->m_mutex, std::adopt_lock_t());
#endif
auto poll_callback = [&] {
@ -300,7 +296,7 @@ void CondVar::Wait(Mutex* mutex) {
if (callback == nullptr) {
return;
}
#if defined(KYTY_WIN_CS)
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
LeaveCriticalSection(&mutex->m_mutex->m_cs);
callback();
EnterCriticalSection(&mutex->m_mutex->m_cs);
@ -310,7 +306,7 @@ void CondVar::Wait(Mutex* mutex) {
cpp_lock.lock();
#endif
};
#ifdef KYTY_WIN_CS
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
static auto func = ResolveSleepConditionVariableCS();
EXIT_NOT_IMPLEMENTED(func == nullptr);
if (g_cond_wait_poll_callback == nullptr) {
@ -340,10 +336,10 @@ void CondVar::SetWaitPollCallback(wait_poll_func_t callback) {
bool CondVar::WaitFor(Mutex* mutex, uint32_t micros) {
bool ok = false;
#ifndef KYTY_WIN_CS
#if KYTY_PLATFORM != KYTY_PLATFORM_WINDOWS
std::unique_lock<std::recursive_mutex> cpp_lock(mutex->m_mutex->m_mutex, std::adopt_lock_t());
#endif
#ifdef KYTY_WIN_CS
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
static auto func = ResolveSleepConditionVariableCS();
EXIT_NOT_IMPLEMENTED(func == nullptr);
ok = !(func(&m_cond_var->m_cv, &mutex->m_mutex->m_cs, (micros < 1000 ? 1 : micros / 1000)) ==
@ -358,7 +354,7 @@ bool CondVar::WaitFor(Mutex* mutex, uint32_t micros) {
}
void CondVar::Signal() {
#ifdef KYTY_WIN_CS
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
static auto func = ResolveWakeConditionVariable();
EXIT_NOT_IMPLEMENTED(func == nullptr);
func(&m_cond_var->m_cv);
@ -368,7 +364,7 @@ void CondVar::Signal() {
}
void CondVar::SignalAll() {
#ifdef KYTY_WIN_CS
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
static auto func = ResolveWakeAllConditionVariable();
EXIT_NOT_IMPLEMENTED(func == nullptr);
func(&m_cond_var->m_cv);

View File

@ -11,6 +11,7 @@
#include "common/subsystems.h"
#include "common/systemInfo.h"
#include "common/threads.h"
#include "graphics/presentation/systemOverlay.h"
#include "graphics/presentation/window.h"
#include "kernel/fileSystem.h"
#include "kernel/memory.h"
@ -138,6 +139,9 @@ static void Init(const Config::ConfigOptions& cfg, const std::filesystem::path&
Libs::LibKernel::Memory::SetFlexibleMemorySize(flexible_memory_size);
}
}
if (cfg.splash_enabled) {
Libs::Graphics::InitializeSplashScreen(param_json.parent_path());
}
// Initialization order is explicit; destruction is automatic and reversed.
subsystems.Initialize<Loader::Timer::Lifecycle>();

View File

@ -16,6 +16,7 @@ struct RunOptions {
};
void Run(const RunOptions& options);
int LoadExec(const char* path, const char* const argv[]);
} // namespace Emulator

View File

@ -1934,13 +1934,7 @@ KYTY_CP_OP_PARSER(CpOpIndirectBuffer) {
auto* indirect_buffer =
reinterpret_cast<const uint32_t*>(buffer[0] | (static_cast<uint64_t>(buffer[1]) << 32u));
uint32_t indirect_num_dw = control & 0xfffffu;
static std::atomic<uint32_t> indirect_log_count {0};
if (indirect_log_count.fetch_add(1) < 128) {
LOGF("\t indirect buffer: addr=0x%016" PRIx64 ", num_dw=%" PRIu32 ", control=0x%08" PRIx32
"\n",
reinterpret_cast<uint64_t>(indirect_buffer), indirect_num_dw, control);
}
const uint32_t indirect_num_dw = control & 0xfffffu;
if (indirect_num_dw == 0) {
return 3;
@ -2148,27 +2142,23 @@ KYTY_CP_OP_PARSER(CpOpIndirectUcRegs) {
KYTY_CP_OP_PARSER(CpOpMarker) {
KYTY_PROFILER_FUNCTION();
// EXIT_NOT_IMPLEMENTED(cmd_id != 0xC0001000);
uint32_t id = buffer[0] & 0xfffu;
uint32_t len_dw = ((cmd_id >> 16u) & 0x3fffu);
switch (id) {
case 0x0: break;
case 0x4: cp.SetUserDataMarker(HW::UserSgprType::Vsharp); break;
case 0xd: cp.SetUserDataMarker(HW::UserSgprType::Region); break;
case 0x777: {
switch (buffer[0]) {
case 0x68750004: cp.SetUserDataMarker(HW::UserSgprType::Vsharp); break;
case 0x6875000d: cp.SetUserDataMarker(HW::UserSgprType::Region); break;
case 0x68750777: {
cp.Flip();
break;
}
case 0x778: {
case 0x68750778: {
auto* addr =
reinterpret_cast<void*>(buffer[1] | (static_cast<uint64_t>(buffer[2]) << 32u));
uint32_t value = buffer[3];
cp.Flip(addr, value);
break;
}
case 0x781: {
case 0x68750781: {
auto* addr =
reinterpret_cast<void*>(buffer[1] | (static_cast<uint64_t>(buffer[2]) << 32u));
uint32_t value = buffer[3];
@ -2177,7 +2167,8 @@ KYTY_CP_OP_PARSER(CpOpMarker) {
cp.FlipWithInterrupt(eop_event_type, cache_action, addr, value);
break;
}
default: EXIT("unknown marker at %05" PRIx32 ": 0x%" PRIx32 "\n", num_dw - dw, id);
// NOP payloads are caller-owned data; only complete known marker tags execute.
default: break;
}
return len_dw + 1;

View File

@ -10,9 +10,9 @@ namespace {
struct FormatInfo {
BufferFormat format;
uint32_t bytes_per_element;
uint32_t block_compressed_bytes_per_block;
uint32_t render_target_bytes_per_element;
uint8_t bytes_per_element;
uint8_t block_compressed_bytes_per_block;
uint8_t render_target_bytes_per_element;
bool sampled_texture;
bool uint_texture;
bool sint_texture = false;
@ -21,7 +21,7 @@ struct FormatInfo {
constexpr FormatInfo kFormatInfo[] = {
{BufferFormat::k8UNorm, 1, 0, 1, true, false},
{BufferFormat::k8UScaled, 1, 0, 0, true, false},
{BufferFormat::k8SNorm, 1, 0, 1, false, false},
{BufferFormat::k8SNorm, 1, 0, 1, true, false},
{BufferFormat::k8UInt, 1, 0, 1, true, true},
{BufferFormat::k16UNorm, 2, 0, 2, true, false},
{BufferFormat::k16SNorm, 2, 0, 2, true, false},
@ -41,6 +41,7 @@ constexpr FormatInfo kFormatInfo[] = {
{BufferFormat::k16_16UInt, 4, 0, 4, true, true},
{BufferFormat::k16_16SInt, 4, 0, 4, true, false, true},
{BufferFormat::k16_16Float, 4, 0, 4, true, false},
{BufferFormat::k11_11_10UNorm, 4, 0, 0, true, false},
{BufferFormat::k11_11_10UInt, 4, 0, 4, true, true},
{BufferFormat::k11_11_10Float, 4, 0, 4, true, false},
{BufferFormat::k10_11_11Float, 4, 0, 4, true, false},
@ -237,6 +238,7 @@ BufferFormat RemapTextureFormat(BufferFormat format) {
switch (format) {
case BufferFormat::k8UScaled: return BufferFormat::k8UNorm;
case BufferFormat::k8_8UScaled: return BufferFormat::k8_8UNorm;
case BufferFormat::k11_11_10UNorm:
case BufferFormat::k11_11_10UInt: return BufferFormat::k32UInt;
default: return format;
}

View File

@ -576,12 +576,7 @@ bool GuestGpu::Process(Submission& submission) {
}
}
if (progressed) {
if (complete) {
m_renderer.RunGarbageCollector();
}
cp.BufferFlush();
} else if (complete) {
m_renderer.RunGarbageCollector();
}
break;
}
@ -602,20 +597,16 @@ bool GuestGpu::Process(Submission& submission) {
complete = cp.Process(submission.command_execution, submission.commands) ==
Pm4ProcessResult::Complete;
if (submission.command_execution.MadeProgress()) {
if (complete) {
m_renderer.RunGarbageCollector();
}
cp.BufferFlush();
} else if (complete) {
m_renderer.RunGarbageCollector();
}
break;
}
case SubmissionType::FlipPreparation:
m_renderer.RunGarbageCollector();
cp.PrepareCpuFlip(submission.flip_request_id);
break;
case SubmissionType::SuspendPoint:
// Age caches at the frame boundary, not once per command list.
m_renderer.RunGarbageCollector();
cp.EmitGlobalBarrier();
m_renderer.GetCommandScheduler().DeferPriorityOperation(
[ready = m_suspend_point_ready] { ready->release(); });
@ -1072,8 +1063,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
case 0x03: with_interrupt = false; break;
case 0x01:
if (!IsAsyncComputeQueue()) {
Sync::TriggerEopEventAtEndOfPipe(command, m_interrupt_event_id,
interrupt_context_id);
TriggerEopEventAtEndOfPipe(interrupt_context_id);
return;
}
with_interrupt = true;
@ -1082,24 +1072,15 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
default: EXIT("unknown interrupt_selector\n");
}
auto write32 = [&](bool with_writeback) {
auto write32 = [&] {
auto* dst = static_cast<uint32_t*>(dst_gpu_addr);
auto data = static_cast<uint32_t>(value);
EXIT_IF(dst == nullptr);
(void)command.Handle();
std::memcpy(dst, &data, sizeof(data));
if (with_interrupt) {
if (with_writeback) {
Sync::WriteAtEndOfPipeWithInterruptWriteBack32(m_submit_id, command, dst,
data, m_interrupt_event_id,
interrupt_context_id);
} else {
Sync::WriteAtEndOfPipeWithInterrupt32(m_submit_id, command, dst, data,
m_interrupt_event_id, interrupt_context_id);
}
} else if (with_writeback) {
Sync::WriteAtEndOfPipeWithWriteBack32(m_submit_id, command, dst, data);
} else {
Sync::WriteAtEndOfPipe32(m_submit_id, command, dst, data);
TriggerEopEventAtEndOfPipe(interrupt_context_id);
}
};
@ -1111,15 +1092,13 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
SynchronizeGpu();
Sync::ReadGds(*m_renderer.GetBufferCache().GetGdsBuffer(), dst, value & 0xffffu,
value >> 16u);
Sync::WriteAtEndOfPipeGds32(m_submit_id, command, dst, value & 0xffffu,
value >> 16u);
if (with_interrupt) {
m_renderer.TriggerInterrupt(m_interrupt_event_id, interrupt_context_id);
}
return;
}
} else if (eop_event_type == 0x04 && cache_action == 0x00 && event_index == 0x05) {
write32(false);
write32();
return;
}
break;
@ -1128,8 +1107,8 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
if constexpr (sizeof(T) == sizeof(uint32_t)) {
if (event_write_source == 0x02 && eop_event_type == 0x2f && event_index == 0x06) {
switch (cache_action) {
case 0x00: write32(false); return;
case 0x38: write32(true); return;
case 0x00:
case 0x38: write32(); return;
default: break;
}
}
@ -1137,25 +1116,14 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
if (event_write_source == 0x04) {
value = Sync::ReadReferenceClock();
}
auto write64 = [&](bool with_writeback) {
auto write64 = [&] {
auto* dst = static_cast<uint64_t*>(dst_gpu_addr);
EXIT_IF(dst == nullptr);
(void)command.Handle();
std::memcpy(dst, &value, sizeof(value));
if (with_interrupt) {
if (with_writeback) {
Sync::WriteAtEndOfPipeWithInterruptWriteBack64(
m_submit_id, command, dst, value, m_interrupt_event_id,
interrupt_context_id);
} else {
Sync::WriteAtEndOfPipeWithInterrupt64(m_submit_id, command, dst,
value, m_interrupt_event_id,
interrupt_context_id);
}
} else if (with_writeback) {
Sync::WriteAtEndOfPipeWithWriteBack64(m_submit_id, command, dst,
value);
} else {
Sync::WriteAtEndOfPipe64(m_submit_id, command, dst, value);
TriggerEopEventAtEndOfPipe(interrupt_context_id);
}
};
@ -1164,7 +1132,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
switch (eop_event_type) {
case 0x04:
if (event_index == 0x05) {
write64(false);
write64();
return;
}
break;
@ -1172,7 +1140,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
case 0x28:
case 0x2f:
if (event_index == 0x00) {
write64(false);
write64();
return;
}
break;
@ -1180,7 +1148,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
case 0x2d:
case 0x30:
if (event_index == 0x00 && !with_interrupt) {
write64(false);
write64();
return;
}
break;
@ -1195,20 +1163,20 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
if (((eop_event_type == 0x04 || eop_event_type == 0x28) &&
event_index == 0x05) ||
(event_index == 0x00)) {
write64(true);
write64();
return;
}
break;
case 0x2b:
case 0x2d:
if (event_index == 0x00 && !with_interrupt) {
write64(true);
write64();
return;
}
break;
case 0x2f:
if (event_index == 0x06 && !with_interrupt) {
write64(true);
write64();
return;
}
break;
@ -1217,7 +1185,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
break;
case 0x3b:
if (eop_event_type == 0x04 && event_index == 0x05 && with_interrupt) {
write64(true);
write64();
return;
}
break;
@ -1270,7 +1238,11 @@ void CommandProcessor::EmitGlobalBarrier() {
}
void CommandProcessor::TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id) {
Sync::TriggerEopEventAtEndOfPipe(CurrentBuffer(), m_interrupt_event_id, interrupt_context_id);
(void)CurrentBuffer().Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, event_id = m_interrupt_event_id, interrupt_context_id] {
renderer.TriggerInterrupt(event_id, interrupt_context_id);
});
}
void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
@ -1360,8 +1332,9 @@ void CommandProcessor::Flip() {
auto& command = CurrentBuffer();
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
Sync::WriteAtEndOfPipeOnlyFlip(m_submit_id, command, m_flip.handle, m_flip.index,
m_flip.flip_mode, m_flip.flip_arg, request);
(void)command.Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, request] { renderer.GetVideoOut().CompleteFlip(request); });
GetScheduler().Flush();
}
@ -1375,12 +1348,13 @@ void CommandProcessor::Flip(void* dst_gpu_addr, uint32_t value) {
reinterpret_cast<uint64_t>(dst_gpu_addr), value);
}
EXIT_IF(dst_gpu_addr == nullptr);
std::memcpy(dst_gpu_addr, &value, sizeof(value));
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
Sync::WriteAtEndOfPipeWithFlip32(m_submit_id, command, static_cast<uint32_t*>(dst_gpu_addr),
value, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg, request);
(void)command.Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, request] { renderer.GetVideoOut().CompleteFlip(request); });
GetScheduler().Flush();
}
@ -1400,12 +1374,16 @@ void CommandProcessor::FlipWithInterrupt(uint32_t eop_event_type, uint32_t cache
if (eop_event_type != 0x00000004 || cache_action != 0x00000038) {
EXIT("unknown event type\n");
}
EXIT_IF(dst_gpu_addr == nullptr);
std::memcpy(dst_gpu_addr, &value, sizeof(value));
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
Sync::WriteAtEndOfPipeWithInterruptWriteBackFlip32(
m_submit_id, command, static_cast<uint32_t*>(dst_gpu_addr), value, m_flip.handle,
m_flip.index, m_flip.flip_mode, m_flip.flip_arg, request, m_interrupt_event_id);
(void)command.Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, event_id = m_interrupt_event_id, request] {
renderer.GetVideoOut().CompleteFlip(request);
renderer.TriggerInterrupt(event_id, 0);
});
GetScheduler().Flush();
}

View File

@ -299,8 +299,8 @@ bool TileGetTextureBlockLayout(Prospero::BufferFormat format, Prospero::TileMode
if (!TileGetTextureElementLayout(format, element)) {
return false;
}
if ((family == TileBlockFamily::Depth64KB || family == TileBlockFamily::RenderTarget64KB) &&
Prospero::RenderTargetBytesPerElement(format) != element.bytes) {
if (family == TileBlockFamily::Depth64KB &&
(element.texel_width != 1 || element.texel_height != 1)) {
return false;
}

View File

@ -33,6 +33,7 @@ struct GraphicContext {
// bool fp64_denorm_preserve = false; // Temporarily disabled.
bool attachment_feedback_loop_enabled = false;
bool provoking_vertex_last_enabled = false;
bool maintenance8_enabled = false;
bool supports_block_texel_view = false;
bool mesh_shader_enabled = false;
vk::PhysicalDeviceMeshShaderPropertiesEXT mesh_shader_properties = {};

View File

@ -27,6 +27,26 @@ namespace {
constexpr uint64_t MiB = 1024 * 1024;
constexpr uint64_t GdsBufferSize = 64 * 1024;
std::pair<Buffer*, uint64_t> UploadMemory(GraphicContext& graphics, CommandScheduler& scheduler,
StreamBuffer& staging, uint64_t size, uint64_t alignment,
vk::BufferUsageFlags usage, const auto& write) {
const auto [mapped, offset] = staging.Map(size, alignment);
if (mapped != nullptr) {
write(mapped, offset);
staging.Commit();
return {&staging, offset};
}
// Large transfers use the same one-time host allocation as ordinary buffer uploads.
auto temporary = std::make_unique<Buffer>(graphics, scheduler, MemoryUsage::Upload, 0,
usage, size);
write(temporary->Mapped().data(), 0);
temporary->Flush(0, size);
auto* buffer = temporary.get();
scheduler.DeferOperation([owner = std::move(temporary)]() mutable { owner.reset(); });
return {buffer, 0};
}
} // namespace
void BufferCache::WriteDataBuffer(Buffer& buffer, uint64_t address, const void* source,
@ -447,28 +467,17 @@ vk::Buffer BufferCache::UploadCopies(Buffer& buffer, std::span<vk::BufferCopy> c
return nullptr;
}
auto [mapped, base_offset] = m_staging_buffer.Map(total_size, 4);
if (mapped != nullptr) {
const auto source = UploadMemory(m_graphics, m_scheduler, m_staging_buffer, total_size, 4,
vk::BufferUsageFlagBits::eTransferSrc, [&](uint8_t* mapped, uint64_t base_offset) {
for (auto& copy: copies) {
const auto address = buffer.CpuAddress() + copy.dstOffset;
std::memcpy(mapped + copy.srcOffset, reinterpret_cast<const void*>(address), copy.size);
if (!LibKernel::Memory::CopySparseMemory(address, mapped + copy.srcOffset, copy.size)) {
EXIT("BufferCache: failed to read sparse guest buffer memory\n");
}
copy.srcOffset += base_offset;
}
m_staging_buffer.Commit();
return m_staging_buffer.Handle();
}
auto temporary = std::make_unique<Buffer>(m_graphics, m_scheduler, MemoryUsage::Upload, 0,
vk::BufferUsageFlagBits::eTransferSrc, total_size);
for (const auto& copy: copies) {
const auto address = buffer.CpuAddress() + copy.dstOffset;
std::memcpy(temporary->Mapped().data() + copy.srcOffset,
reinterpret_cast<const void*>(address), copy.size);
}
temporary->Flush(0, total_size);
const auto handle = temporary->Handle();
m_scheduler.DeferOperation([owner = std::move(temporary)]() mutable { owner.reset(); });
return handle;
});
return source.first->Handle();
}
std::pair<Buffer*, uint64_t> BufferCache::ObtainBuffer(uint64_t vaddr, uint64_t size,
@ -486,7 +495,9 @@ std::pair<Buffer*, uint64_t> BufferCache::ObtainBuffer(uint64_t vaddr, uint64_t
m_graphics.physical_device_properties.limits.minUniformBufferOffsetAlignment, 1);
auto [mapped, offset] = m_stream_buffer.Map(size, alignment, false);
if (mapped != nullptr) {
std::memcpy(mapped, reinterpret_cast<const void*>(vaddr), size);
if (!LibKernel::Memory::CopySparseMemory(vaddr, mapped, size)) {
EXIT("BufferCache: failed to read sparse guest buffer memory\n");
}
m_stream_buffer.Commit();
return {&m_stream_buffer, offset};
}
@ -521,12 +532,13 @@ std::pair<Buffer*, uint64_t> BufferCache::ObtainBufferForImage(uint64_t vaddr, u
return ObtainBuffer(vaddr, size, false, false);
}
auto [staging, stage_offset] = m_staging_buffer.Map(size, 16);
if (staging == nullptr || !Libs::LibKernel::Memory::TryReadSparseBacking(vaddr, staging, size)) {
EXIT("BufferCache: failed to read mapped guest image backing\n");
}
m_staging_buffer.Commit();
return {&m_staging_buffer, stage_offset};
return UploadMemory(m_graphics, m_scheduler, m_staging_buffer, size, 16,
vk::BufferUsageFlagBits::eTransferSrc | vk::BufferUsageFlagBits::eStorageBuffer,
[&](uint8_t* mapped, uint64_t) {
if (!Libs::LibKernel::Memory::TryReadSparseBacking(vaddr, mapped, size)) {
EXIT("BufferCache: failed to read mapped guest image backing\n");
}
});
}
void BufferCache::FillBuffer(uint64_t vaddr, uint64_t size, uint32_t value, bool is_gds) {

View File

@ -25,6 +25,7 @@
#include <mutex>
#include <span>
#include <tuple>
#include <utility>
#include <vulkan/vulkan_format_traits.hpp>
namespace Libs::Graphics {
@ -36,7 +37,9 @@ constexpr uint64_t NumFramesBeforeRemoval = 32;
[[nodiscard]] bool DecodeColorClear(const TextureCache::ImageDesc& desc, uint8_t code,
vk::ClearColorValue& clear) {
const auto& metadata = desc.info.metadata;
const auto format = desc.view_info.format;
const auto format = desc.info.IsDepth()
? TextureGetSurfaceFormatInfo(desc.info.guest_format).vk_format
: desc.view_info.format;
const bool cmask = metadata.kind == ImageMetadataKind::Cmask;
if (cmask ? code == 0 : code == 0x20) {
// Register clears belong to the color buffer; the texture pipe cannot decode them.
@ -249,14 +252,11 @@ void TextureCache::DeleteImage(ImageId id) {
if (image == nullptr || !image->registered) {
return;
}
if (!image->depth_id) {
std::vector<ImageId> associations;
m_slot_images.ForEach([&](ImageId candidate, const Image& associated) {
if (associated.depth_id == id) {
associations.push_back(candidate);
}
});
for (const auto association: associations) {
if (image->depth_id) {
std::erase(m_slot_images[image->depth_id].stencil_ids, id);
} else {
// Detach first: deleting each association also unlinks it from its depth owner.
for (const auto association: std::exchange(image->stencil_ids, {})) {
FreeImage(association);
}
}
@ -297,9 +297,6 @@ void TextureCache::TouchImage(Image& image) {
void TextureCache::MarkAsMaybeDirty(ImageId id, Image& image) {
image.MarkMaybeCpuDirty();
if (image.NeedsMaybeCpuHash()) {
image.SetMaybeCpuHash(image.HashGuestEdges());
}
UntrackImage(id);
}
@ -631,6 +628,9 @@ void TextureCache::CopyImage(ImageId destination_id, ImageId source_id) {
void TextureCache::CopyImageMip(ImageId destination_id, ImageId source_id, uint32_t mip,
uint32_t layer) {
RefreshCopySource(source_id);
if (m_slot_images[destination_id].IsBufferModified()) {
RefreshImage(destination_id);
}
auto& destination = m_slot_images[destination_id];
auto& source = m_slot_images[source_id];
TrackImage(destination_id);
@ -751,11 +751,63 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
return replacement_id;
}
namespace BcAlias {
bool Matches(const ImageInfo& requested, const ImageInfo& cached) {
return requested.guest_format != cached.guest_format &&
requested.IsBlock() != cached.IsBlock() &&
requested.tile_mode == cached.tile_mode &&
requested.bytes_per_block == cached.bytes_per_block &&
requested.samples == 1 && cached.samples == 1 &&
(requested.data != cached.data || requested.resources != cached.resources ||
requested.BlockExtent() != cached.BlockExtent()) &&
!TextureCache::SameBacking(cached, requested, false);
}
bool Publish(TextureCache& cache, Image& image) {
if (!cache.CanDownload(image)) {
return false;
}
// ObtainBuffer preserves padding and records ownership before the GPU-only transfer.
auto [buffer, offset] =
cache.m_buffer_cache.ObtainBuffer(image.info.data.address, image.info.data.size, true);
cache.DownloadImage(image, *buffer, offset, image.info.data.size, image.info.resources.levels);
image.ClearGpuModified();
image.MarkBufferModified();
return true;
}
void Resolve(TextureCache& cache, ImageId cached_id, ImageId merged_id) {
auto& cached = cache.m_slot_images[cached_id];
auto* merged = merged_id ? &cache.m_slot_images[merged_id] : nullptr;
// PS5 raw block images can span several packed BC mips. Transfer their complete
// guest bytes, including tail padding, before retiring the incompatible backing.
if (cached.SafeToDownload()) {
if (merged && merged->SafeToDownload() && !Publish(cache, *merged)) {
return;
}
if (!Publish(cache, cached)) {
return;
}
if (merged) {
merged->MarkBufferModified();
}
}
cached.binding.needs_rebind |= cached.binding.is_bound || cached.binding.is_target;
cache.FreeImage(cached_id);
}
} // namespace BcAlias
TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& requested,
BindingType binding, ImageId cached_id,
ImageId merged_id) {
auto owner = m_slot_images.try_get(cached_id);
if (owner == nullptr) {
if (owner == nullptr || !owner->registered) {
return {merged_id};
}
if (BcAlias::Matches(requested, owner->info) && !owner->IsCpuDirty()) {
BcAlias::Resolve(*this, cached_id, merged_id);
return {merged_id};
}
auto& cached = *owner;
@ -1059,8 +1111,15 @@ void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
}
{
std::scoped_lock lock {m_lock};
ClearImage(m_scheduler.Current(), id, view.format,
{vk::ImageAspectFlagBits::eColor, view.base_level, view.level_count,
const auto& image = m_slot_images[id];
const bool depth = image.info.IsDepth();
if (depth) {
const auto value = clear.color.float32[0];
clear.depthStencil = {value, 0u};
}
ClearImage(m_scheduler.Current(), id, depth ? image.backing.format : view.format,
{depth ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor,
view.base_level, view.level_count,
image_first + slice, 1}, clear);
}
// Publish the conversion's expanded keys without treating them as guest writes
@ -1082,12 +1141,7 @@ void TextureCache::RefreshImage(ImageId id) {
}
TrackImage(id);
if (image.IsMaybeCpuDirty()) {
const auto hash = image.HashGuestEdges();
if (image.NeedsMaybeCpuHash()) {
image.SetMaybeCpuHash(hash);
return;
}
(void)image.ResolveMaybeCpuHash(hash);
(void)image.ResolveMaybeCpuHash(image.HashGuestEdges());
}
bool cpu_dirty = image.IsBufferModified() || image.IsDefinitelyCpuDirty();
if (image.info.metadata.compression != VideoOutCompression::Uncompressed) {
@ -1141,6 +1195,12 @@ ImageId TextureCache::AssociateStencil(ImageId depth_id, GuestRange stencil) {
plane.IsTiled() ? TileGetDepthPitch(plane.extent.width, 1, 0) : depth.info.pitch;
plane.bytes_per_block = 1;
plane.UpdateSize();
if (record.depth_id != depth_id) {
if (record.depth_id) {
std::erase(m_slot_images[record.depth_id].stencil_ids, association);
}
depth.stencil_ids.push_back(association);
}
record.info = plane;
record.depth_id = depth_id;
record.stencil_subresources = depth.stencil_subresources;
@ -1166,6 +1226,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
std::scoped_lock lock {m_lock};
const auto candidates =
FindImagesInRegion(desc.info.data.address, desc.info.data.size, false);
bool block_alias = false;
for (const auto id: candidates) {
const auto& image = m_slot_images[id];
@ -1184,6 +1245,9 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
}
if (SameBacking(image.info, desc.info, exact_format)) {
result = id;
} else if (image.SafeToDownload() &&
BcAlias::Matches(desc.info, image.info)) {
block_alias = true;
}
}
@ -1201,6 +1265,15 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
view_layer = overlap.layer;
}
}
} else if (block_alias) {
for (const auto candidate: candidates) {
const auto* owner = m_slot_images.try_get(candidate);
if (candidate != result && owner != nullptr && owner->registered &&
!owner->IsCpuDirty() &&
BcAlias::Matches(m_slot_images[result].info, owner->info)) {
BcAlias::Resolve(*this, candidate, result);
}
}
}
if (result && !m_slot_images[result].depth_id) {
@ -1363,7 +1436,6 @@ vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
}
TouchImage(image);
image.MarkGpuModified();
image.usage.depth_target = true;
if (desc.info.HasStencil()) {
const auto layers = image.info.resources.layers;
EXIT_IF(layers == 0 || image.info.data.size % layers != 0 ||
@ -1779,7 +1851,7 @@ void TextureCache::InvalidateCpuAliases(uint64_t address, uint64_t size) {
continue;
}
if (owner->Overlaps(address, size)) {
owner->InvalidateCpuWrite(address, size);
owner->MarkCpuDirty();
UntrackImage(id);
continue;
}
@ -1923,6 +1995,9 @@ void TextureCache::RunGarbageCollector() {
}
void TextureCache::ProcessDownloadImages() {
if (!m_readback_linear_images) {
return;
}
std::scoped_lock lock {m_lock};
for (const auto id: m_download_images) {
const auto owner = m_slot_images.try_get(id);

View File

@ -26,8 +26,15 @@ class BufferCache;
class CommandBuffer;
class CommandScheduler;
class RenderExecutor;
class TextureCache;
struct TextureCacheTestAccess;
namespace BcAlias {
[[nodiscard]] bool Matches(const ImageInfo& requested, const ImageInfo& cached);
[[nodiscard]] bool Publish(TextureCache& cache, Image& image);
void Resolve(TextureCache& cache, ImageId cached, ImageId merged);
} // namespace BcAlias
class TextureCache {
public:
enum class BindingType : uint8_t { Texture, Storage, RenderTarget, DepthTarget, VideoOut };
@ -181,6 +188,9 @@ private:
friend struct TextureCacheTestAccess;
friend class BufferCache;
friend class RenderExecutor;
friend bool BcAlias::Matches(const ImageInfo&, const ImageInfo&);
friend bool BcAlias::Publish(TextureCache&, Image&);
friend void BcAlias::Resolve(TextureCache&, ImageId, ImageId);
};
} // namespace Libs::Graphics

View File

@ -236,7 +236,8 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
// DCC owns the clear when both planes are enabled; single-sample CMASK stays expanded.
desc.info.metadata.kind = has_dcc ? ImageMetadataKind::Dcc : ImageMetadataKind::Cmask;
desc.info.metadata.range = {has_dcc ? rt.dcc_addr.addr : rt.cmask.addr, metadata_size.size};
desc.info.metadata.clear_word = rt.clear_word0.word0;
desc.info.metadata.clear_word =
rt.clear_word0.word0 | (uint64_t {rt.clear_word1.word1} << 32u);
desc.info.metadata.clear_register_valid = true;
}
desc.view_info.format = target_format.format;

View File

@ -394,26 +394,20 @@ static void RcPrint(const char* func, const HW::RenderControl& c) {
LOGF("\t depth_clear_enable = %s\n"
"\t stencil_clear_enable = %s\n"
"\t copy_depth_to_color = %s\n"
"\t copy_stencil_to_color = %s\n"
"\t resummarize_enable = %s\n"
"\t stencil_compress_disable = %s\n"
"\t depth_compress_disable = %s\n"
"\t copy_centroid = %s\n"
"\t copy_sample = %" PRIu8 "\n",
c.depth_clear_enable ? "true" : "false", c.stencil_clear_enable ? "true" : "false",
c.copy_depth_to_color ? "true" : "false", c.copy_stencil_to_color ? "true" : "false",
c.resummarize_enable ? "true" : "false", c.stencil_compress_disable ? "true" : "false",
c.depth_compress_disable ? "true" : "false", c.copy_centroid ? "true" : "false",
c.copy_sample);
}
static void RcCheck(const HW::RenderControl& c) {
// EXIT_NOT_IMPLEMENTED(c.depth_clear_enable != false);
// EXIT_NOT_IMPLEMENTED(c.stencil_clear_enable != false);
// EXIT_NOT_IMPLEMENTED(c.stencil_compress_disable != false);
// EXIT_NOT_IMPLEMENTED(c.depth_compress_disable != false);
EXIT_NOT_IMPLEMENTED(c.copy_centroid != false);
EXIT_NOT_IMPLEMENTED(c.copy_sample != 0);
}
static void McPrint(const char* func, const HW::ModeControl& c) {
LOGF("%s\n", func);
@ -869,9 +863,6 @@ void hw_check(const CommandBuffer& buffer) {
VpCheck(vp, smc);
log_phase("clip");
ClipCheck(c);
log_phase("rc");
RcCheck(rc);
log_phase("depth");
log_phase("mode");
McCheck(mc);
log_phase("blend");

View File

@ -298,13 +298,20 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
}
return;
}
if (rc.copy_depth_to_color || rc.copy_stencil_to_color || rc.copy_centroid ||
rc.copy_sample != 0 || dc.zfunc > static_cast<uint8_t>(vk::CompareOp::eAlways) ||
if (rc.copy_stencil_to_color ||
dc.zfunc > static_cast<uint8_t>(vk::CompareOp::eAlways) ||
(!z.depth_view.depth_write_disable && z.z_write_base_addr != z.z_read_base_addr) ||
(has_stencil && !z.depth_view.stencil_write_disable &&
z.stencil_write_base_addr != z.stencil_read_base_addr)) {
DepthFatal("unsupported depth register state");
}
if (rc.copy_depth_to_color &&
(z.z_info.num_samples != 0 || rc.copy_sample != 0 ||
z.z_info.format != Prospero::DepthFormat::kZ32F ||
dc.z_enable || dc.z_write_enable || dc.depth_bounds_enable || dc.stencil_enable ||
rc.depth_clear_enable || rc.stencil_clear_enable)) {
DepthFatal("unsupported depth-to-color copy state");
}
r.desc = MakeDepthTargetDesc(buffer, z);
r.depth_clear_enable = rc.depth_clear_enable;
r.depth_load_clear_enable = r.depth_clear_enable;

View File

@ -29,10 +29,8 @@ struct CachedImageView {
};
struct ImageUsage {
bool texture = false;
bool storage = false;
bool render_target = false;
bool depth_target = false;
bool video_out = false;
};
@ -70,40 +68,25 @@ public:
void CopyImageWithBuffer(Image& source, Buffer& buffer, TileManager& tiler);
void CopyMip(Image& source, uint32_t mip, uint32_t layer);
void InvalidateCpuWrite(uint64_t vaddr, uint64_t size) {
if (ImageRangeOverlaps(info.data.address, info.data.size, vaddr, size)) {
m_cpu_dirty = true;
m_maybe_cpu_dirty = false;
m_maybe_hash_valid = false;
} else if (ImagePageRangesOverlap(info.data.address, info.data.size, vaddr, size)) {
m_maybe_cpu_dirty = true;
}
void MarkCpuDirty() {
m_cpu_dirty = true;
m_maybe_cpu_dirty = false;
}
[[nodiscard]] bool IsCpuDirty() const { return m_cpu_dirty || m_maybe_cpu_dirty; }
[[nodiscard]] bool IsDefinitelyCpuDirty() const { return m_cpu_dirty; }
[[nodiscard]] bool IsMaybeCpuDirty() const { return m_maybe_cpu_dirty; }
void MarkMaybeCpuDirty() {
if (!m_cpu_dirty) {
if (!IsCpuDirty()) {
m_maybe_cpu_hash = HashGuestEdges();
m_maybe_cpu_dirty = true;
}
}
[[nodiscard]] bool NeedsMaybeCpuHash() const {
return m_maybe_cpu_dirty && !m_maybe_hash_valid;
}
void SetMaybeCpuHash(uint64_t hash) {
if (!NeedsMaybeCpuHash()) {
EXIT("image cannot initialize maybe-dirty hash\n");
}
m_maybe_cpu_hash = hash;
m_maybe_hash_valid = true;
}
[[nodiscard]] bool ResolveMaybeCpuHash(uint64_t hash) {
if (!m_maybe_cpu_dirty || !m_maybe_hash_valid || m_cpu_dirty) {
if (!m_maybe_cpu_dirty || m_cpu_dirty) {
EXIT("image cannot resolve maybe-dirty hash\n");
}
m_maybe_cpu_dirty = false;
m_maybe_hash_valid = false;
m_maybe_cpu_dirty = false;
m_cpu_dirty |= hash != m_maybe_cpu_hash;
return m_cpu_dirty;
}
@ -112,9 +95,8 @@ public:
if (!IsCpuDirty()) {
EXIT("clean image cannot complete a refresh\n");
}
m_cpu_dirty = false;
m_maybe_cpu_dirty = false;
m_maybe_hash_valid = false;
m_cpu_dirty = false;
m_maybe_cpu_dirty = false;
}
[[nodiscard]] bool IsGpuModified() const noexcept { return m_gpu_modified; }
@ -149,6 +131,8 @@ public:
uint64_t track_addr = 0;
uint64_t track_addr_end = 0;
ImageId depth_id {};
// Stencil associations that refer to this depth image.
std::vector<ImageId> stencil_ids;
// The current stencil plane's mapping into the depth image, also retained by its association.
ImageSubresourceRange stencil_subresources;
uint64_t tick_accessed_last = 0;
@ -165,12 +149,11 @@ private:
GraphicContext& m_graphics;
CommandScheduler& m_scheduler;
uint64_t m_maybe_cpu_hash = 0;
bool m_cpu_dirty = false;
bool m_maybe_cpu_dirty = false;
bool m_maybe_hash_valid = false;
bool m_gpu_modified = false;
bool m_buffer_modified = false;
uint64_t m_maybe_cpu_hash = 0;
bool m_cpu_dirty = false;
bool m_maybe_cpu_dirty = false;
bool m_gpu_modified = false;
bool m_buffer_modified = false;
};
namespace ImageOps {

View File

@ -27,7 +27,7 @@ struct ImageMetadataInfo {
GuestRange range;
ImageMetadataKind kind = ImageMetadataKind::None;
uint32_t control = 0;
uint32_t clear_word = 0;
uint64_t clear_word = 0;
VideoOutCompression compression = VideoOutCompression::Uncompressed;
bool stencil_compressed = false;
bool clear_register_valid = false;
@ -362,8 +362,17 @@ struct VideoOutFormatPolicy {
};
inline constexpr uint64_t VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ = 0x8100070422000000ull;
inline constexpr uint64_t VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ = 0x8100070400000000ull;
inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {{
[[nodiscard]] constexpr bool IsHdrVideoOutPixelFormat(uint64_t format) {
switch (format) {
case VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ:
case VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ: return true;
default: return false;
}
}
inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {{
{0x8000000022000000ull,
{vk::Format::eR8G8B8A8Srgb, Prospero::BufferFormat::k8_8_8_8Srgb, 4, false}},
{0x8000000000000000ull,
@ -374,6 +383,8 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
{vk::Format::eA2R10G10B10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
{VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ,
{vk::Format::eA2B10G10R10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
{VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ,
{vk::Format::eA2R10G10B10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
{0xc001000622000000ull,
{vk::Format::eR16G16B16A16Sfloat, Prospero::BufferFormat::k16_16_16_16Float, 8, false}},
{0xc001000600000000ull,
@ -403,9 +414,10 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
return false;
}
[[nodiscard]] inline bool DecodePackedColorClear(vk::Format format, uint32_t packed,
[[nodiscard]] inline bool DecodePackedColorClear(vk::Format format, uint64_t clear_words,
vk::ClearColorValue& clear) {
vk::ClearColorValue next {};
const auto packed = static_cast<uint32_t>(clear_words);
const auto unorm8 = [](uint32_t value) { return static_cast<float>(value & 0xffu) / 255.0f; };
const auto srgb8 = [](uint32_t value) {
const auto encoded = static_cast<float>(value & 0xffu) / 255.0f;
@ -423,6 +435,15 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
(mantissa << 13u));
};
switch (format) {
case vk::Format::eB10G11R11UfloatPack32:
next.float32[0] = float16((packed & 0x7ffu) << 4u);
next.float32[1] = float16(((packed >> 11u) & 0x7ffu) << 4u);
next.float32[2] = float16((packed >> 22u) << 5u);
break;
case vk::Format::eR16G16B16A16Sfloat:
next.float32[2] = float16(static_cast<uint32_t>(clear_words >> 32u));
next.float32[3] = float16(static_cast<uint32_t>(clear_words >> 48u));
[[fallthrough]];
case vk::Format::eR16G16Sfloat:
next.float32[1] = float16(packed >> 16u);
[[fallthrough]];
@ -433,9 +454,10 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
case vk::Format::eR16Unorm:
next.float32[0] = static_cast<float>(packed & 0xffffu) / 65535.0f;
break;
// A single-plane float target carries its clear as raw float bits, the same encoding the
// depth decoder below uses. Without this the clear is discarded and the target keeps stale
// contents.
// Each clear register stores one raw 32-bit float channel.
case vk::Format::eR32G32Sfloat:
next.float32[1] = std::bit_cast<float>(static_cast<uint32_t>(clear_words >> 32u));
[[fallthrough]];
case vk::Format::eR32Sfloat: next.float32[0] = std::bit_cast<float>(packed); break;
case vk::Format::eR32Uint: next.uint32[0] = packed; break;
case vk::Format::eR32Sint: next.int32[0] = static_cast<int32_t>(packed); break;
@ -496,7 +518,7 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
clear = vk::ClearColorValue {};
return true;
}
return DecodePackedColorClear(format, packed, clear);
return DecodePackedColorClear(format, uint64_t {packed} | (uint64_t {packed} << 32u), clear);
}
[[nodiscard]] inline bool DecodePackedStencilClear(uint32_t packed, uint8_t& clear) {

View File

@ -179,7 +179,9 @@ static void ValidateSampledDepthBinding(const ShaderRecompiler::IR::ImageResourc
const ShaderTextureResource& descriptor, const Image& image,
vk::Format view_format, uint64_t size) {
const bool resource_ok = IsSupportedSampledDepthResource(resource);
const bool encoding_ok = IsSupportedDepthTextureEncoding(descriptor, resource.r128);
// Comparison sampling changes the host format, not the guest metadata encoding.
const bool encoding_ok = descriptor.TileMode() != Prospero::TileMode::kDepth ||
IsSupportedDepthTextureEncoding(descriptor, resource.r128);
const bool view_ok =
IsSupportedSampledDepthView(image.info.pixel_format, view_format, descriptor.DstSelXYZW());
if (resource_ok && encoding_ok && view_ok) {
@ -307,8 +309,10 @@ void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource,
const bool encoding_ok = IsSupportedStorageTextureEncoding(resource, descriptor);
const bool uint_resource = resource.numeric_class == Prospero::TextureNumericClass::Uint;
const bool raw_sint_storage = format == Prospero::BufferFormat::k32SInt && uint_resource &&
resource.written && !resource.read && !resource.atomic;
const auto numeric_class = Prospero::SampledTextureNumericClass(format);
resource.written &&
(resource.atomic ? !resource.atomic64 : !resource.read);
const auto numeric_class =
Prospero::SampledTextureNumericClass(Prospero::RemapTextureFormat(format));
const bool raw_float_atomic = format == Prospero::BufferFormat::k32Float && uint_resource &&
resource.atomic && !resource.atomic64;
const bool format_ok =
@ -564,7 +568,8 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
const bool shader_conversion =
surface_format.conversion_format != Prospero::BufferFormat::kInvalid;
const bool sampled_numeric_class =
storage || resource.numeric_class == Prospero::SampledTextureNumericClass(format);
storage || resource.numeric_class ==
Prospero::SampledTextureNumericClass(Prospero::RemapTextureFormat(format));
if (!storage && resource.resource_class == ShaderRecompiler::IR::ImageResourceClass::Sampled &&
!sampled_numeric_class) {
EXIT("sampled image numeric class mismatch: numeric=%u format=%u addr=0x%016" PRIx64 "\n",
@ -622,8 +627,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
if (storage) {
ValidateStorageTexture(resource, descriptor, desc.info.data.size);
}
if (!resource.r128 && descriptor.MetaCompress() && tile != Prospero::TileMode::kDepth &&
!desc.info.IsDepth()) {
if (!resource.r128 && descriptor.MetaCompress() && tile != Prospero::TileMode::kDepth) {
TileSizeAlign metadata_size {};
(void)TileGetDccSize(width, height, volume ? depth : image_layers,
desc.info.bytes_per_block, physical_levels, tile, metadata_size,
@ -872,7 +876,6 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
auto& image = texture_cache.GetImage(binding.image_id);
const bool storage = binding.desc.type == TextureCache::BindingType::Storage;
image.usage.storage |= storage;
image.usage.texture |= !storage;
}
}

View File

@ -148,7 +148,7 @@ void DumpShaderOriginal(const char* stage_name, uint64_t shader_hash,
}
bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
const std::vector<uint32_t>& spirv) {
const std::vector<uint32_t>& spirv, bool maintenance8_enabled) {
if (!Config::ShaderValidationEnabled()) {
return true;
}
@ -160,7 +160,9 @@ bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
static_cast<int>(position.column), static_cast<int>(position.index),
message);
});
if (tools.Validate(spirv)) {
spvtools::ValidatorOptions validator_options;
validator_options.SetAllowOffsetTextureOperand(maintenance8_enabled);
if (tools.Validate(spirv.data(), spirv.size(), validator_options)) {
return true;
}
spvtools::SpirvTools disassembler(SPV_ENV_VULKAN_1_2);
@ -296,14 +298,15 @@ struct PipelineCache::ProgramCache {
std::span<const uint32_t> function_code) {
auto result = ShaderRecompiler::CompileProgram(std::move(translated), options,
specialization, push_data_start_dword);
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv)) {
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv,
options.maintenance8_enabled)) {
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
EXIT("%s failed hash=0x%016" PRIx64 ": SPIR-V validation failed\n", options.dump_label,
options.shader_hash);
}
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
const auto module = CompileSPV(result.spirv, device);
const auto module = CompileSPV(result.spirv, graphics.device);
EXIT_IF(module == nullptr);
if (options.dump_ir) {
LOGF("%s SPIR-V words=%" PRIu64 " wave_size=%u\n", options.dump_label,
@ -384,6 +387,7 @@ struct PipelineCache::ProgramCache {
}
ShaderRecompiler::CompileOptions options;
options.stage = stage;
options.maintenance8_enabled = graphics.maintenance8_enabled;
options.shader_hash = params.hash;
options.user_data = user_data;
options.back_code = params.back_code;
@ -456,26 +460,26 @@ struct PipelineCache::ProgramCache {
return permutation.handle;
}
explicit ProgramCache(vk::Device device): device(device) {
explicit ProgramCache(GraphicContext& graphics): graphics(graphics) {
lookup_key.static_state.reserve(MaxStaticKeyWords);
}
~ProgramCache() {
for (const auto& [key, entry]: programs) {
(void)key;
for (const auto& permutation: entry.permutations) {
device.destroyShaderModule(permutation.handle.module, nullptr);
graphics.device.destroyShaderModule(permutation.handle.module, nullptr);
}
}
}
std::unordered_map<ProgramKey, SourceEntry, ProgramKeyHash> programs;
ProgramKey lookup_key;
vk::Device device;
GraphicContext& graphics;
uint64_t next_shader_id = 0;
};
PipelineCache::PipelineCache(GraphicContext& graphics)
: m_graphics(graphics), m_program_cache(std::make_unique<ProgramCache>(graphics.device)) {
: m_graphics(graphics), m_program_cache(std::make_unique<ProgramCache>(graphics)) {
EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread());
InitializeDriverCache();
}

View File

@ -39,13 +39,7 @@ enum class CommandBufferDebugOp : uint32_t {
DispatchDirect,
DrawIndex,
DrawIndexAuto,
EopWrite,
EopInterrupt,
EopWriteBack,
EopFlip,
EopWriteBackFlip,
EopOnlyFlip,
DispatchIndirect,
DispatchIndirect = 9,
Unknown,
};
@ -186,6 +180,8 @@ private:
bool ignore_target_mask = false, bool exact_format = false);
void ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepthInfo& target);
[[nodiscard]] bool DepthStencilCopy(CommandBuffer& buffer);
[[nodiscard]] bool CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo& draw,
const DrawEmitInfo& emit, DrawRenderState& state);
[[nodiscard]] bool PrepareDrawRenderState(CommandBuffer& buffer,
const DrawCallInfo& draw,
uint32_t render_target_slice_offset,
@ -200,11 +196,13 @@ private:
std::span<PreparedBindings* const> stages = {});
[[nodiscard]] bool ResolveColorTargets(CommandBuffer& buffer,
uint32_t render_target_slice_offset);
[[nodiscard]] bool ConsumeMetadataColorOperation(CommandBuffer& buffer,
uint32_t render_target_slice_offset);
void BindImage(ImageId id, bool storage);
void BindRenderTarget(ImageId id);
void ResetBindings();
[[nodiscard]] bool TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer);
void UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer);
[[nodiscard]] bool TryConsumeComputeImageClear(const ShaderComputeInputInfo& input,
CommandBuffer& command, uint32_t group_x,
uint32_t group_y, uint32_t group_z, uint32_t mode);

View File

@ -49,8 +49,8 @@ static bool FillSourcesDisjoint(std::span<const ShaderRecompiler::IR::Descriptor
return true;
}
bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer) {
void RenderExecutor::UpdateComputeMetaClears(const ShaderComputeInputInfo& input,
const CommandBuffer& buffer) {
const auto& program = *input.stage.program;
const auto& resources = *input.stage.resources;
if (resources.buffers.size() != program.info.buffers.size()) {
@ -60,10 +60,9 @@ bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& in
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
const auto& resource = program.info.buffers[i];
const auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
// A metadata resource that is also read is not proven to be a full overwrite. Execute it
// conservatively instead of replacing the dispatch with a coarse full-surface clear.
// Reads and transforms of metadata do not establish a clear state.
if ((!resource.written || resource.read) && cache.IsMeta(descriptor.Base48())) {
return false;
return;
}
}
@ -73,13 +72,10 @@ bool RenderExecutor::TryConsumeComputeMetaClear(const ShaderComputeInputInfo& in
if (resource.written) {
const auto descriptor =
DecodeNativeDescriptor<ShaderBufferResource>(resources.buffers[i]);
if (cache.ClearMeta(descriptor.Base48())) {
return true;
}
(void)cache.ClearMeta(descriptor.Base48());
}
}
}
return false;
}
bool ResolveComputeBufferFill(const ShaderComputeInputInfo& input, uint32_t group_x,
@ -306,12 +302,14 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
const auto& program = *input_info.stage.program;
const auto& resources = *input_info.stage.resources;
if (resources.specialization_reads.empty() &&
(TryConsumeComputeMetaClear(input_info, buffer) ||
TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
thread_group_z, mode))) {
ResetBindings();
return;
if (resources.specialization_reads.empty()) {
// Cached metadata addresses can be reused as ordinary buffers; preserve their guest writes.
UpdateComputeMetaClears(input_info, buffer);
if (TryConsumeComputeImageClear(input_info, buffer, thread_group_x, thread_group_y,
thread_group_z, mode)) {
ResetBindings();
return;
}
}
if (use_thread_dimensions) {

View File

@ -608,16 +608,19 @@ enum class CbColorMode : uint8_t {
DccDecompress = 6,
};
static bool ConsumeMetadataColorOperation(const CommandBuffer& buffer) {
bool RenderExecutor::ConsumeMetadataColorOperation(CommandBuffer& buffer,
uint32_t render_target_slice_offset) {
const auto& ctx = buffer.GetRegisters();
const auto mode = ctx.GetColorControl().mode;
// These special modes run color-buffer metadata or decompression operations. The shader is a
// vehicle for that operation, and its exported color must not be applied as a normal draw.
// Kyty stores expanded Vulkan images rather than compressed guest surfaces, so no equivalent
// hardware pass is emitted. Tracked DCC clear state is materialized on attachment bind;
// future CMask/FMask support can consume its state through the same TextureCache path.
return mode == static_cast<uint8_t>(CbColorMode::EliminateFastClear) ||
mode == static_cast<uint8_t>(CbColorMode::FmaskDecompress) ||
if (mode == static_cast<uint8_t>(CbColorMode::EliminateFastClear)) {
// FCE can precede any ordinary draw. Discover the target to materialize its native
// clear metadata before the expanded image is sampled.
RenderColorInfo target {};
ResolveRenderColorTarget(buffer, target, render_target_slice_offset, 0, true);
return true;
}
// Metadata/decompression shaders must not export colors into the expanded image.
return mode == static_cast<uint8_t>(CbColorMode::FmaskDecompress) ||
mode == static_cast<uint8_t>(CbColorMode::DccDecompress);
}
@ -1029,12 +1032,71 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_
}
}
bool RenderExecutor::CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo& draw,
const DrawEmitInfo& emit, DrawRenderState& state) {
if (!buffer.GetRegisters().GetRenderControl().copy_depth_to_color) {
return false;
}
auto& ucfg = buffer.GetUserConfig();
const auto& hw = buffer.GetRegisters();
auto& depth = state.depth_info;
auto& color = state.color_info[0];
EXIT_NOT_IMPLEMENTED(!m_context.GetGraphics().maintenance8_enabled ||
!Prospero::IsRectList(ucfg.GetPrimType()) ||
draw.IsIndexed() || draw.index_count != 3 || draw.instance_count != 1 ||
emit.first_vertex != 0 || emit.first_instance != 0 ||
state.color_count != 1 || color.target_slot != 0 || !depth.image_id ||
color.desc.info.samples != 1 ||
depth.desc.info.extent.width != color.Extent().width ||
depth.desc.info.extent.height != color.Extent().height ||
depth.desc.view_info.layer_count != 1 ||
color.desc.view_info.layer_count != 1 ||
hw.GetBlendControl(0).enable || state.ps_input_info.ps_pixel_kill_enable ||
state.ps_input_info.ps_depth_export_enable ||
state.ps_input_info.ps_sample_mask_export_enable);
vk::ImageAspectFlags feedback;
const auto rendering = AcquireRenderTargets(buffer, &color, 1, depth, feedback);
if (depth.depth_load_clear_enable) {
m_context.GetCommandScheduler().BeginRendering(rendering);
}
auto& cache = m_context.GetTextureCache();
auto& source = cache.GetImage(depth.image_id);
auto& destination = cache.GetImage(color.image_id);
EXIT_NOT_IMPLEMENTED((source.backing.format != vk::Format::eD32Sfloat &&
source.backing.format != vk::Format::eD32SfloatS8Uint) ||
destination.backing.format != vk::Format::eR32Sfloat);
const auto rect = calc_final_scissor(hw.GetScreenViewport(), hw.GetScanModeControl(),
{rendering.width, rendering.height}, 0);
if (rect.right > rect.left && rect.bottom > rect.top) {
const vk::ImageCopy region {
{vk::ImageAspectFlagBits::eDepth, depth.desc.view_info.base_level,
depth.desc.view_info.base_layer, 1}, {rect.left, rect.top, 0},
{vk::ImageAspectFlagBits::eColor, color.desc.view_info.base_level,
color.desc.view_info.base_layer, 1}, {rect.left, rect.top, 0},
{static_cast<uint32_t>(rect.right - rect.left),
static_cast<uint32_t>(rect.bottom - rect.top), 1}};
m_context.GetCommandScheduler().EndRendering();
const auto command = buffer.Handle();
source.Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead,
{}, command);
destination.Transit(vk::ImageLayout::eTransferDstOptimal,
vk::AccessFlagBits2::eTransferWrite, {}, command);
command.copyImage(source.backing.image, vk::ImageLayout::eTransferSrcOptimal,
destination.backing.image, vk::ImageLayout::eTransferDstOptimal,
1, &region);
}
return true;
}
void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buffer,
const DrawCallInfo& draw, DrawRenderState& state,
vk::PrimitiveTopology topology, const DrawEmitInfo& emit,
const DrawIndexBufferSource& index_source,
bool primitive_restart_enable) {
auto& ucfg = buffer.GetUserConfig();
if (CopyDepthToColor(buffer, draw, emit, state)) {
return;
}
const auto vertex_stages =
std::span {state.vertex_info.data(), state.programs.VertexStageCount()};
const bool mesh_active = state.vertex_info[0].stage.program->stage == ShaderType::Mesh;
@ -1198,7 +1260,8 @@ void RenderExecutor::DrawIndex(uint64_t submit_id, CommandBuffer& buffer,
return;
}
if (ConsumeMetadataColorOperation(buffer) || DepthStencilCopy(buffer) ||
if (ConsumeMetadataColorOperation(buffer, args.render_target_slice_offset) ||
DepthStencilCopy(buffer) ||
ResolveColorTargets(buffer, args.render_target_slice_offset)) {
ResetBindings();
return;
@ -1311,7 +1374,8 @@ void RenderExecutor::DrawAuto(uint64_t submit_id, CommandBuffer& buffer, const D
return;
}
if (ConsumeMetadataColorOperation(buffer) || DepthStencilCopy(buffer) ||
if (ConsumeMetadataColorOperation(buffer, args.render_target_slice_offset) ||
DepthStencilCopy(buffer) ||
ResolveColorTargets(buffer, args.render_target_slice_offset)) {
ResetBindings();
return;

View File

@ -1,10 +1,6 @@
#include "graphics/host_gpu/renderer/sync.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/logging/log.h"
#include "common/threads.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/cache/bufferCache.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
@ -53,109 +49,6 @@ uint64_t ReadReferenceClock() {
return value;
}
enum class EndOfPipeWriteSize : uint32_t { Dword = 4, Qword = 8 };
enum class EndOfPipeWriteAction { Write, WriteBack, Interrupt, InterruptWriteBack };
static CommandBufferDebugOp DebugOperation(EndOfPipeWriteAction action) {
switch (action) {
case EndOfPipeWriteAction::Write: return CommandBufferDebugOp::EopWrite;
case EndOfPipeWriteAction::WriteBack:
case EndOfPipeWriteAction::InterruptWriteBack: return CommandBufferDebugOp::EopWriteBack;
case EndOfPipeWriteAction::Interrupt: return CommandBufferDebugOp::EopInterrupt;
}
EXIT("unsupported end-of-pipe write action\n");
return CommandBufferDebugOp::Unknown;
}
static bool TriggersInterrupt(EndOfPipeWriteAction action) {
return action == EndOfPipeWriteAction::Interrupt ||
action == EndOfPipeWriteAction::InterruptWriteBack;
}
static void RecordEndOfPipeWrite(uint64_t submit_id, CommandBuffer& buffer, uint64_t destination,
uint64_t value, EndOfPipeWriteSize size,
EndOfPipeWriteAction action, int interrupt_event_id = 0,
uint32_t context_id = 0) {
EXIT_IF(destination == 0);
(void)buffer.Handle();
const auto width = static_cast<uint32_t>(size);
const auto value_low = static_cast<uint32_t>(value);
const auto value_high = static_cast<uint32_t>(value >> 32u);
const auto operation = static_cast<uint32_t>(DebugOperation(action));
if (TriggersInterrupt(action)) {
buffer.SetDebugInfo(operation, submit_id, width, context_id, value_low, value_high,
destination);
TriggerEopEventAtEndOfPipe(buffer, interrupt_event_id, context_id);
} else {
buffer.SetDebugInfo(operation, submit_id, width, value_low, value_high, 0, destination);
}
}
void WriteAtEndOfPipe32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
uint32_t value) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::Write);
}
void WriteAtEndOfPipeGds32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
uint32_t dw_offset, uint32_t dw_num) {
EXIT_IF(dst_gpu_addr == nullptr);
(void)buffer.Handle();
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopWrite), submit_id,
dw_offset, dw_num, 0, 0, reinterpret_cast<uint64_t>(dst_gpu_addr));
}
void WriteAtEndOfPipe64(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst_gpu_addr,
uint64_t value) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::Write);
}
void WriteAtEndOfPipeWithWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
uint64_t* dst_gpu_addr, uint64_t value) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::WriteBack);
}
void WriteAtEndOfPipeWithWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::WriteBack);
}
void WriteAtEndOfPipeWithInterruptWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
uint32_t context_id) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::InterruptWriteBack,
event_id, context_id);
}
void WriteAtEndOfPipeWithInterruptWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
uint32_t context_id) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::InterruptWriteBack,
event_id, context_id);
}
void WriteAtEndOfPipeWithInterrupt64(uint64_t submit_id, CommandBuffer& buffer,
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
uint32_t context_id) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::Interrupt, event_id,
context_id);
}
void WriteAtEndOfPipeWithInterrupt32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
uint32_t context_id) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::Interrupt, event_id,
context_id);
}
uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index, int flip_mode,
int64_t flip_arg) {
for (;;) {
@ -176,65 +69,6 @@ uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index, int f
}
}
void WriteAtEndOfPipeWithInterruptWriteBackFlip32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value,
int handle, int index, int flip_mode,
int64_t flip_arg, uint64_t request_id,
int event_id) {
EXIT_IF(dst_gpu_addr == nullptr);
(void)buffer.Handle();
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopWriteBackFlip), submit_id,
static_cast<uint32_t>(handle), static_cast<uint32_t>(index),
static_cast<uint32_t>(flip_mode), value, static_cast<uint64_t>(flip_arg));
auto& renderer = buffer.GetContext();
auto& scheduler = renderer.GetCommandScheduler();
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
scheduler.DeferPriorityOperation([&renderer, event_id, request_id] {
renderer.GetVideoOut().CompleteFlip(request_id);
renderer.TriggerInterrupt(event_id, 0);
});
}
void WriteAtEndOfPipeWithFlip32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
uint32_t value, int handle, int index, int flip_mode,
int64_t flip_arg, uint64_t request_id) {
EXIT_IF(dst_gpu_addr == nullptr);
(void)buffer.Handle();
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopFlip), submit_id,
static_cast<uint32_t>(handle), static_cast<uint32_t>(index),
static_cast<uint32_t>(flip_mode), value, static_cast<uint64_t>(flip_arg));
auto& renderer = buffer.GetContext();
auto& scheduler = renderer.GetCommandScheduler();
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
scheduler.DeferPriorityOperation(
[&renderer, request_id] { renderer.GetVideoOut().CompleteFlip(request_id); });
}
void WriteAtEndOfPipeOnlyFlip(uint64_t submit_id, CommandBuffer& buffer, int handle, int index,
int flip_mode, int64_t flip_arg, uint64_t request_id) {
(void)buffer.Handle();
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopOnlyFlip), submit_id,
static_cast<uint32_t>(handle), static_cast<uint32_t>(index),
static_cast<uint32_t>(flip_mode), 0, static_cast<uint64_t>(flip_arg));
auto& renderer = buffer.GetContext();
auto& scheduler = renderer.GetCommandScheduler();
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
scheduler.DeferPriorityOperation(
[&renderer, request_id] { renderer.GetVideoOut().CompleteFlip(request_id); });
}
void TriggerEopEventAtEndOfPipe(CommandBuffer& buffer, int event_id, uint32_t context_id) {
(void)buffer.Handle();
auto& renderer = buffer.GetContext();
auto& scheduler = renderer.GetCommandScheduler();
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
scheduler.DeferPriorityOperation(
[&renderer, event_id, context_id] { renderer.TriggerInterrupt(event_id, context_id); });
}
static void InterruptEventResetFunc(LibKernel::EventQueue::KernelEqueueEvent* event) {
EXIT_IF(event == nullptr);
event->triggered = false;

View File

@ -17,44 +17,8 @@ namespace Sync {
uint64_t& value);
[[nodiscard]] uint64_t ReadReferenceClock();
void TriggerEopEventAtEndOfPipe(CommandBuffer& buffer, int event_id, uint32_t context_id);
void WriteAtEndOfPipe32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
uint32_t value);
void WriteAtEndOfPipe64(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst_gpu_addr,
uint64_t value);
void WriteAtEndOfPipeGds32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
uint32_t dw_offset, uint32_t dw_num);
void WriteAtEndOfPipeWithWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value);
void WriteAtEndOfPipeWithWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
uint64_t* dst_gpu_addr, uint64_t value);
void WriteAtEndOfPipeWithInterrupt32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
uint32_t context_id = 0);
void WriteAtEndOfPipeWithInterrupt64(uint64_t submit_id, CommandBuffer& buffer,
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
uint32_t context_id = 0);
void WriteAtEndOfPipeWithInterruptWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
uint32_t context_id = 0);
void WriteAtEndOfPipeWithInterruptWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
uint32_t context_id = 0);
[[nodiscard]] uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index,
int flip_mode, int64_t flip_arg);
void WriteAtEndOfPipeOnlyFlip(uint64_t submit_id, CommandBuffer& buffer, int handle, int index,
int flip_mode, int64_t flip_arg, uint64_t request_id);
void WriteAtEndOfPipeWithFlip32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
uint32_t value, int handle, int index, int flip_mode,
int64_t flip_arg, uint64_t request_id);
void WriteAtEndOfPipeWithInterruptWriteBackFlip32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value,
int handle, int index, int flip_mode,
int64_t flip_arg, uint64_t request_id,
int event_id);
int AddEqEvent(RenderContext& renderer, LibKernel::EventQueue::KernelEqueue eq, int id,
void* udata);
int DeleteEqEvent(RenderContext& renderer, LibKernel::EventQueue::KernelEqueue eq, int id);

View File

@ -1,15 +1,53 @@
#version 450
layout(location = 0) in vec2 uv;
layout(location = 0) out vec4 color;
vec3 SdrToHdr10(vec3 rgb, bool source_linear) {
rgb = clamp(rgb, 0.0, 1.0);
vec3 linear = source_linear ? rgb :
mix(rgb / 12.92, pow((rgb + 0.055) / 1.055, vec3(2.4)),
greaterThan(rgb, vec3(0.04045)));
// Convert primaries and scale 250-nit reference white relative to 10,000 nits.
const mat3 to_rec2020 = (250.0 / 10000.0) *
mat3(0.627404, 0.069097, 0.016391,
0.329283, 0.919540, 0.088013,
0.043313, 0.011362, 0.895595);
vec3 powered = pow(to_rec2020 * linear, vec3(2610.0 / 16384.0));
return pow((3424.0 / 4096.0 + (2413.0 / 128.0) * powered) /
(1.0 + (2392.0 / 128.0) * powered), vec3(2523.0 / 32.0));
}
#ifdef IMGUI_HDR
layout(set = 0, binding = 0) uniform texture2D image;
layout(set = 1, binding = 0) uniform sampler image_sampler;
layout(location = 0) in struct { vec4 Color; vec2 UV; } In;
void main() {
vec4 sampled = In.Color * texture(sampler2D(image, image_sampler), In.UV);
color = vec4(SdrToHdr10(sampled.rgb, false), sampled.a);
}
#else
layout(location = 0) in vec2 uv;
layout(binding = 0) uniform sampler2D source;
layout(push_constant) uniform AlphaControl {
uint premultiplied;
uint flags; // bit 0: premultiplied, bit 1: HDR target, bit 2: PQ source, bit 3: linear source
};
void main() {
color = texture(source, uv);
if (premultiplied == 0u) {
color.rgb *= color.a;
const uint HDR_TARGET = 2u;
const uint PQ_SOURCE = 4u;
vec4 sampled = texture(source, uv);
bool premultiplied = (flags & 1u) != 0u;
bool convert_to_hdr = (flags & (HDR_TARGET | PQ_SOURCE)) == HDR_TARGET;
if (convert_to_hdr && premultiplied) {
sampled.rgb = sampled.a > 0.0 ? sampled.rgb / sampled.a : vec3(0.0);
}
if (convert_to_hdr) {
sampled.rgb = SdrToHdr10(sampled.rgb, (flags & 8u) != 0u);
}
if (!premultiplied || convert_to_hdr) {
sampled.rgb *= sampled.a;
}
color = sampled;
}
#endif

View File

@ -28,6 +28,14 @@ vk::Format VulkanFormat(Prospero::BufferFormat guest_format);
void RequireVulkanSuccess(vk::Result result, const char* operation);
vk::ShaderModule CompileSPV(std::span<const uint32_t> code, vk::Device device);
[[nodiscard]] constexpr bool IsHdrSwapchainFormat(vk::Format format) {
switch (format) {
case vk::Format::eA2B10G10R10UnormPack32:
case vk::Format::eA2R10G10B10UnormPack32: return true;
default: return false;
}
}
template <typename Handle, typename... Args>
void SetVulkanObjectNameF(vk::Device device, Handle handle, fmt::format_string<Args...> format,
Args&&... args) {

View File

@ -0,0 +1,137 @@
#include "graphics/presentation/ddsImage.h"
#include "common/assert.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "imgui_impl_vulkan.h"
#include <array>
namespace Libs::Graphics {
DdsImage::~DdsImage() {
if (m_descriptor != VK_NULL_HANDLE) {
ImGui_ImplVulkan_RemoveTexture(m_descriptor);
}
m_graphics.device.destroyImageView(m_view, nullptr);
if (m_image.image != nullptr) {
m_graphics.DeleteImage(m_image);
}
if (m_staging != VK_NULL_HANDLE) {
vmaDestroyBuffer(m_graphics.allocator, m_staging, m_staging_allocation);
}
}
bool DdsImage::Load(const std::filesystem::path& path) {
EXIT_IF(m_image.image != nullptr || m_staging != VK_NULL_HANDLE);
Common::File file(path, Common::File::Mode::Read);
if (file.IsInvalid()) {
return false;
}
std::array<uint32_t, 37> header {};
uint32_t read = 0;
file.Read(header.data(), sizeof(header), &read);
const auto width = header[4];
const auto height = header[3];
if (read != sizeof(header) || header[0] != 0x20534444 || header[1] != 124 || header[19] != 32 ||
header[20] != 4 || header[21] != 0x30315844 || header[32] != 98 || header[33] != 3 ||
header[34] != 0 || header[35] != 1 || header[6] != 0 || header[7] != 1 || header[28] != 0 ||
width == 0 || height == 0 || width > 3840 || height > 2160) {
return false;
}
const uint32_t size = ((width + 3) / 4) * ((height + 3) / 4) * 16;
if (file.Size() != sizeof(header) + size) {
return false;
}
constexpr auto format = vk::Format::eBc7UnormBlock;
constexpr auto features = vk::FormatFeatureFlagBits::eSampledImage |
vk::FormatFeatureFlagBits::eSampledImageFilterLinear |
vk::FormatFeatureFlagBits::eTransferDst;
if ((m_graphics.GetFormatProperties(format).optimalTilingFeatures & features) != features) {
LOGF("[Splash] host does not support BC7 textures\n");
return false;
}
vk::BufferCreateInfo buffer {};
buffer.size = size;
buffer.usage = vk::BufferUsageFlagBits::eTransferSrc;
VmaAllocationCreateInfo allocation {};
allocation.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST;
allocation.flags =
VMA_ALLOCATION_CREATE_MAPPED_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
VmaAllocationInfo mapped {};
if (vmaCreateBuffer(m_graphics.allocator, static_cast<const VkBufferCreateInfo*>(buffer),
&allocation, &m_staging, &m_staging_allocation, &mapped) != VK_SUCCESS) {
LOGF("[Splash] could not allocate DDS upload buffer\n");
return false;
}
file.Read(mapped.pMappedData, size, &read);
if (read != size) {
return false;
}
RequireVulkanSuccess(static_cast<vk::Result>(vmaFlushAllocation(m_graphics.allocator,
m_staging_allocation, 0, size)),
"flush splash image upload");
vk::ImageCreateInfo image {};
image.imageType = vk::ImageType::e2D;
image.extent = {width, height, 1};
image.mipLevels = 1;
image.arrayLayers = 1;
image.format = format;
image.tiling = vk::ImageTiling::eOptimal;
image.usage = vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled;
image.samples = vk::SampleCountFlagBits::e1;
if (!m_graphics.CreateImage(image, m_image)) {
LOGF("[Splash] could not allocate DDS image\n");
return false;
}
vk::ImageViewCreateInfo view {};
view.image = m_image.image;
view.viewType = vk::ImageViewType::e2D;
view.format = format;
view.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
RequireVulkanSuccess(m_graphics.device.createImageView(&view, nullptr, &m_view),
"create splash image view");
m_descriptor = ImGui_ImplVulkan_AddTexture(static_cast<VkImageView>(m_view),
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
return true;
}
void DdsImage::Upload(vk::CommandBuffer command) {
if (m_image.state.layout == vk::ImageLayout::eShaderReadOnlyOptimal) {
return;
}
vk::ImageMemoryBarrier barrier {};
barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;
barrier.oldLayout = vk::ImageLayout::eUndefined;
barrier.newLayout = vk::ImageLayout::eTransferDstOptimal;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = m_image.image;
barrier.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
command.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe,
vk::PipelineStageFlagBits::eTransfer, {}, 0, nullptr, 0, nullptr, 1,
&barrier);
vk::BufferImageCopy copy {};
copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
copy.imageExtent = m_image.extent;
command.copyBufferToImage(m_staging, m_image.image, vk::ImageLayout::eTransferDstOptimal, 1,
&copy);
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
vk::PipelineStageFlagBits::eFragmentShader, {}, 0, nullptr, 0, nullptr,
1, &barrier);
m_image.state.layout = vk::ImageLayout::eShaderReadOnlyOptimal;
}
ImTextureRef DdsImage::Texture() const {
return ImTextureRef {static_cast<ImTextureID>(reinterpret_cast<uintptr_t>(m_descriptor))};
}
} // namespace Libs::Graphics

View File

@ -0,0 +1,32 @@
#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_DDSIMAGE_H_
#define EMULATOR_SRC_GRAPHICS_PRESENTATION_DDSIMAGE_H_
#include "graphics/host_gpu/graphicContext.h"
#include "imgui.h"
#include <filesystem>
namespace Libs::Graphics {
class DdsImage final {
public:
explicit DdsImage(GraphicContext& graphics): m_graphics(graphics) {}
~DdsImage();
KYTY_CLASS_NO_COPY(DdsImage);
bool Load(const std::filesystem::path& path);
void Upload(vk::CommandBuffer command);
[[nodiscard]] ImTextureRef Texture() const;
private:
GraphicContext& m_graphics;
VulkanImage m_image;
vk::ImageView m_view = nullptr;
VkDescriptorSet m_descriptor = VK_NULL_HANDLE;
VkBuffer m_staging = VK_NULL_HANDLE;
VmaAllocation m_staging_allocation = nullptr;
};
} // namespace Libs::Graphics
#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_DDSIMAGE_H_

View File

@ -26,11 +26,13 @@ public:
~Presenter();
KYTY_CLASS_NO_COPY(Presenter);
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info);
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info,
bool hdr = false);
[[nodiscard]] Frame& PrepareBlankFrame(uint32_t width, uint32_t height, bool opaque,
CommandBuffer* producer = nullptr);
[[nodiscard]] bool PresentLastFrame();
[[nodiscard]] bool IsGuestPaused() const noexcept;
[[nodiscard]] bool IsHdrSupported() const noexcept;
[[nodiscard]] bool NeedsSystemOverlayRefresh() const noexcept;
[[nodiscard]] RenderContext& Renderer() const noexcept;
void Present(Frame& frame);

View File

@ -3,9 +3,13 @@
#include <SDL3/SDL.h>
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "gpu_blit_shaders/gpu_imgui_hdr_spv.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/presentation/ddsImage.h"
#include "imgui.h"
#include "imgui_internal.h"
#include "imgui_impl_vulkan.h"
@ -13,6 +17,7 @@
#include "libs/dialog.h"
#include "libs/ime.h"
#include "libs/imeDialog.h"
#include "loader/systemContent.h"
#include "stb_image.h"
#include <algorithm>
@ -23,6 +28,7 @@
#include <cstdint>
#include <cstring>
#include <deque>
#include <fmt/format.h>
#include <limits>
#include <memory>
#include <mutex>
@ -37,6 +43,8 @@ namespace Libs::Graphics {
namespace {
using Clock = std::chrono::steady_clock;
namespace CoreIme = Libs::Ime;
namespace DialogIme = Libs::Dialog::ImeDialog;
namespace SystemDialog = Libs::Dialog::SystemDialog;
@ -194,6 +202,10 @@ struct TrophyNotification {
std::mutex g_trophy_notification_mutex;
std::deque<TrophyNotification> g_trophy_notifications;
std::atomic<bool> g_trophy_notification_active {false};
std::array<std::filesystem::path, 2> g_splash_paths;
std::atomic<bool> g_splash_active {false};
std::atomic<Clock::duration::rep> g_splash_hidden_at {0};
bool g_splash_fast = false;
std::vector<std::string_view> WrapTrophyTitle(std::string_view text, float font_size, float width) {
std::vector<std::string_view> lines;
@ -464,6 +476,29 @@ void CheckVulkanResult(VkResult result) {
} // namespace
void InitializeSplashScreen(const std::filesystem::path& sce_sys) {
const auto language = Config::GetConsoleLanguage();
for (size_t i = 0; i < g_splash_paths.size(); ++i) {
auto& path = g_splash_paths[i];
path = sce_sys / fmt::format("pic{}_{:02}.dds", i + 1, language);
if (!Common::File::IsFileExisting(path)) {
path = sce_sys / fmt::format("pic{}.dds", i + 1);
}
}
int32_t attribute = 0;
Loader::SystemContentParamSfoGetInt("ATTRIBUTE3", &attribute);
g_splash_fast = (attribute & 0x1000) != 0;
g_splash_hidden_at.store(0, std::memory_order_release);
g_splash_active.store(Common::File::IsFileExisting(g_splash_paths[0]) &&
Common::File::IsFileExisting(g_splash_paths[1]),
std::memory_order_release);
}
void HideSplashScreen() {
Clock::duration::rep expected = 0;
g_splash_hidden_at.compare_exchange_strong(expected, Clock::now().time_since_epoch().count());
}
void InitializeSystemOverlayInput(SDL_Window* window) {
EXIT_IF(window == nullptr);
const Uint32 type = SDL_RegisterEvents(1);
@ -481,6 +516,7 @@ void InitializeSystemOverlayInput(SDL_Window* window) {
}
void ShutdownSystemOverlayInput() {
g_splash_active.store(false, std::memory_order_release);
CoreIme::SetVisibilityCallback(nullptr);
DialogIme::SetVisibilityCallback(nullptr);
SystemDialog::SetVisibilityCallback(nullptr);
@ -515,8 +551,10 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept {
const auto dialog = DialogIme::GetVisualState();
const auto system = SystemDialog::GetVisualState();
const bool trophy_active = g_trophy_notification_active.load(std::memory_order_acquire);
return {core.active || dialog.active || system.active || trophy_active,
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u)};
const bool splash = g_splash_active.load(std::memory_order_acquire);
return {core.active || dialog.active || system.active || trophy_active || splash,
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u) +
(splash ? 2u : 0u)};
}
bool ProcessSystemOverlayInput(const SDL_Event& event) {
@ -656,8 +694,6 @@ void NotifyTrophyUnlocked(std::string_view name, int32_t grade,
}
struct SystemOverlay::Impl {
using Clock = std::chrono::steady_clock;
explicit Impl(GraphicContext& context): graphics(context) {}
~Impl() {
@ -770,6 +806,65 @@ struct SystemOverlay::Impl {
return texture;
}
void FinishSplash() {
g_splash_active.store(false, std::memory_order_release);
if (splash_dds[0] || splash_dds[1]) {
Common::LockGuard lock(graphics.queue_mutex);
RequireVulkanSuccess(graphics.queue.waitIdle(), "retire splash images");
splash_dds = {};
}
}
void DrawSplash(vk::Extent2D frame_extent) {
if (!g_splash_active.load(std::memory_order_acquire)) {
return;
}
for (size_t i = 0; i < g_splash_paths.size(); ++i) {
if (splash_dds[i]) {
continue;
}
const auto& path = g_splash_paths[i];
auto image = std::make_unique<DdsImage>(graphics);
if (!image->Load(path)) {
LOGF("[Splash] could not load %s\n", path.string().c_str());
FinishSplash();
return;
}
splash_dds[i] = std::move(image);
}
const auto now = Clock::now();
if (splash_born == Clock::time_point {}) {
splash_born = now;
}
const auto hidden = g_splash_hidden_at.load(std::memory_order_acquire);
const float fade =
hidden == 0
? 0.0f
: std::chrono::duration<float>(now.time_since_epoch() - Clock::duration {hidden})
.count() /
0.3f;
const float alpha = 1.0f - std::clamp(fade, 0.0f, 1.0f);
if (alpha == 0.0f) {
FinishSplash();
return;
}
const float age = std::chrono::duration<float>(now - splash_born).count();
const float logo = g_splash_fast ? 1.0f : std::clamp((age - 1.4f) / 2.2f, 0.0f, 1.0f);
const ImVec2 screen {static_cast<float>(frame_extent.width),
static_cast<float>(frame_extent.height)};
const float fit = std::min(screen.x / 3840.0f, screen.y / 2160.0f);
const ImVec2 size {3840.0f * fit, 2160.0f * fit};
const ImVec2 begin {(screen.x - size.x) * 0.5f, (screen.y - size.y) * 0.5f};
const ImVec2 end {begin.x + size.x, begin.y + size.y};
auto* draw = ImGui::GetBackgroundDrawList();
draw->AddRectFilled({0, 0}, screen, IM_COL32(0, 0, 0, static_cast<int>(255.0f * alpha)));
for (size_t i = 0; i < g_splash_paths.size(); ++i) {
draw->AddImage(
splash_dds[i]->Texture(), begin, end, {0, 0}, {1, 1},
IM_COL32(255, 255, 255, static_cast<int>(255.0f * alpha * (i == 0 ? 1.0f : logo))));
}
}
void EnsureVulkan(vk::Format format, uint32_t image_count) {
EnsureContext();
if (vulkan_initialized) {
@ -796,6 +891,11 @@ struct SystemOverlay::Impl {
info.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount = 1;
info.PipelineInfoMain.PipelineRenderingCreateInfo.pColorAttachmentFormats = &color_format;
info.CheckVkResultFn = CheckVulkanResult;
if (IsHdrSwapchainFormat(format)) {
info.CustomShaderFragCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
info.CustomShaderFragCreateInfo.codeSize = sizeof(GPU_IMGUI_HDR_SPV);
info.CustomShaderFragCreateInfo.pCode = GPU_IMGUI_HDR_SPV;
}
EXIT_IF(!ImGui_ImplVulkan_Init(&info));
vulkan_initialized = true;
}
@ -1215,7 +1315,8 @@ struct SystemOverlay::Impl {
OverlaySnapshot snapshot;
const bool has_overlay = GetOverlaySnapshot(&snapshot);
const auto now = Clock::now();
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire)) {
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire) &&
!g_splash_active.load(std::memory_order_acquire)) {
return false;
}
const auto prepared_session = snapshot.session;
@ -1245,12 +1346,9 @@ struct SystemOverlay::Impl {
last_frame = now;
ImGui_ImplVulkan_NewFrame();
ImGui::NewFrame();
DrawSplash(frame_extent);
GetOverlaySnapshot(&snapshot);
if (snapshot.session != prepared_session) {
ImGui::EndFrame();
return false;
}
if (has_overlay) {
if (has_overlay && snapshot.session == prepared_session) {
if (snapshot.session.kind == OverlayKind::Dialog) {
DrawDialog(snapshot.dialog, frame_extent);
} else {
@ -1264,6 +1362,11 @@ struct SystemOverlay::Impl {
}
void Record(vk::CommandBuffer command, vk::ImageView target) {
for (const auto& image: splash_dds) {
if (image) {
image->Upload(command);
}
}
vk::RenderingAttachmentInfo color {};
color.sType = vk::StructureType::eRenderingAttachmentInfo;
color.imageView = target;
@ -1290,6 +1393,7 @@ struct SystemOverlay::Impl {
return;
}
ImGui::SetCurrentContext(imgui_context);
splash_dds = {};
ImGui_ImplVulkan_Shutdown();
vulkan_initialized = false;
}
@ -1309,6 +1413,8 @@ struct SystemOverlay::Impl {
std::optional<TrophyNotification> trophy_notification;
Clock::time_point trophy_notification_born;
std::vector<std::unique_ptr<ImTextureData>> retired_trophy_images;
std::array<std::unique_ptr<DdsImage>, 2> splash_dds;
Clock::time_point splash_born;
SDL_AudioStream* trophy_sound_stream = nullptr;
uint8_t* trophy_sound_data = nullptr;
uint32_t trophy_sound_size = 0;

View File

@ -5,6 +5,7 @@
#include "graphics/host_gpu/vulkanCommon.h"
#include <cstddef>
#include <filesystem>
#include <memory>
#include <span>
#include <string_view>
@ -21,6 +22,8 @@ struct SystemOverlayVisualState {
uint64_t revision;
};
void InitializeSplashScreen(const std::filesystem::path& sce_sys);
void HideSplashScreen();
void InitializeSystemOverlayInput(SDL_Window* window);
void ShutdownSystemOverlayInput();
bool ProcessSystemOverlayInput(const SDL_Event& event);

View File

@ -272,6 +272,7 @@ public:
void Init(uint32_t width, uint32_t height);
FlipQueue& GetFlipQueue() { return m_flip_queue; }
Graphics::RenderContext& Renderer() const noexcept { return m_renderer; }
bool IsHdrSupported() const noexcept { return m_presenter.IsHdrSupported(); }
void VblankBegin();
void VblankEnd();
@ -558,14 +559,6 @@ Graphics::ImageInfo BufferAttributeGroup::ImageInfo(const VideoOutBuffer& buffer
if (!Graphics::DecodeVideoOutPixelFormat(attribute.pixel_format, pixel_format)) {
EXIT("unsupported video-out pixel format: 0x%016" PRIx64 "\n", attribute.pixel_format);
}
if (attribute.pixel_format == Graphics::VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ) {
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
if (!warned.test_and_set(std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(fmt::format(
"Warning: HDR-to-SDR conversion is not implemented; displaying PQ video-out "
"pixels unchanged (format=0x{:016x}).\n", attribute.pixel_format));
}
}
const auto tile_mode = Graphics::Prospero::TileMode::kRenderTarget;
Graphics::ImageInfo info {};
info.data = {buffer.data_address, 0};
@ -1071,31 +1064,31 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
uint32_t height = 0;
bool current = false;
bool premultiplied_alpha = false;
bool hdr = false;
{
Common::LockGuard lock(cfg->mutex);
current = cfg->opened && !cfg->closing && cfg->generation == generation;
if (current) {
if (special) {
width = cfg->width;
height = cfg->height;
} else {
const auto& surface = cfg->buffers[index];
if (!surface.Occupied()) {
EXIT("cannot prepare flip from an unregistered surface, id=%" PRIu64
" index=%d\n",
request_id, index);
}
if (surface.group_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX ||
!cfg->groups[surface.group_index].occupied) {
EXIT("video-out surface references an unavailable attribute group, id=%" PRIu64
" index=%d group=%d\n",
request_id, index, surface.group_index);
}
source_info = cfg->groups[surface.group_index].ImageInfo(surface);
premultiplied_alpha =
(cfg->groups[surface.group_index].attribute.option &
VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED) != 0;
if (current && special) {
width = cfg->width;
height = cfg->height;
} else if (current) {
const auto& surface = cfg->buffers[index];
if (!surface.Occupied()) {
EXIT("cannot prepare flip from an unregistered surface, id=%" PRIu64
" index=%d\n",
request_id, index);
}
if (surface.group_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX ||
!cfg->groups[surface.group_index].occupied) {
EXIT("video-out surface references an unavailable attribute group, id=%" PRIu64
" index=%d group=%d\n",
request_id, index, surface.group_index);
}
const auto& group = cfg->groups[surface.group_index];
source_info = group.ImageInfo(surface);
hdr = Graphics::IsHdrVideoOutPixelFormat(group.attribute.pixel_format);
premultiplied_alpha = (group.attribute.option &
VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED) != 0;
}
}
if (!current) {
@ -1108,12 +1101,18 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
}
return;
}
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
if (hdr && !m_presenter.IsHdrSupported() && !warned.test_and_set(std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
"Warning: HDR output is unavailable; HDR-to-SDR conversion is not implemented, "
"displaying PQ video-out pixels unchanged.\n");
}
Graphics::Presenter::Frame* frame = nullptr;
if (special) {
frame = &m_presenter.PrepareBlankFrame(width, height, index == VIDEO_OUT_BUFFER_INDEX_BLACK,
&buffer);
} else {
frame = &m_presenter.PrepareFrame(buffer, source_info);
frame = &m_presenter.PrepareFrame(buffer, source_info, hdr);
}
Common::LockGuard lock(m_mutex);
@ -1790,11 +1789,14 @@ KYTY_SYSV_ABI int VideoOutGetOutputStatus(int handle, VideoOutOutputStatus* stat
int32_t attribute3 = 0;
Loader::SystemContentParamSfoGetInt("ATTRIBUTE3", &attribute3);
int32_t attribute = 0;
const bool hdr_allowed = !Loader::SystemContentParamSfoGetInt("ATTRIBUTE", &attribute) ||
(attribute & 0x20000000) != 0;
ctx->mutex.Lock();
// Primary output reports 4K unless param.json Video-out Info enables resolution detection.
status->resolution =
((attribute3 & 4) != 0 && ctx->width < 3840 && ctx->height < 2160 ? 1u : 2u);
status->dynamicRange = 1;
status->dynamicRange = hdr_allowed && DriverState().IsHdrSupported() ? 2u : 1u;
status->refreshRate =
(ctx->output_mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ || Config::GetVblankFrequency() >= 119
? VIDEO_OUT_REFRESH_RATE_119_88HZ

View File

@ -17,6 +17,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <limits>
#include <memory>
#include <vector>
@ -31,6 +32,8 @@ struct Presenter::Frame {
vk::ImageView view = nullptr;
uint64_t present_tick = 0;
bool busy = false;
bool hdr = false;
bool blank = false;
void Configure(GraphicContext& graphics, vk::Extent2D extent, vk::Format format);
void Transit(vk::CommandBuffer command, vk::ImageLayout layout, vk::AccessFlags2 access);
@ -263,7 +266,7 @@ public:
void Create();
void Recreate(bool surface_lost = false);
[[nodiscard]] bool NeedsResize() const;
[[nodiscard]] Status AcquireNextImage();
[[nodiscard]] Status AcquireNextImage(CommandScheduler& scheduler);
[[nodiscard]] bool PrepareSystemOverlay();
void RecordPresentCommands(CommandBuffer& command, Presenter::Frame* source,
const Presenter::Layer& overlay, bool draw_system_overlay);
@ -274,20 +277,31 @@ public:
return static_cast<uint32_t>(m_images.size());
}
[[nodiscard]] vk::Format Format() const noexcept { return m_format; }
[[nodiscard]] bool IsHdrSupported() const noexcept { return m_hdr_supported.load(); }
bool SetHdr(bool hdr) {
const bool changed = m_hdr_active != (hdr && IsHdrSupported());
m_hdr_requested = hdr;
return changed;
}
private:
vk::SurfaceFormatKHR SelectFormat();
void Destroy();
void DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer);
WindowContext& m_window;
vk::SwapchainKHR m_handle = nullptr;
vk::Format m_format = vk::Format::eUndefined;
std::atomic_bool m_hdr_supported {false};
bool m_hdr_requested = false;
bool m_hdr_active = false;
vk::Extent2D m_extent {};
// Drawable pixel size observed when this swapchain was created.
vk::Extent2D m_window_extent {};
std::vector<vk::Image> m_images;
std::vector<vk::ImageView> m_image_views;
std::vector<vk::Semaphore> m_image_acquired;
std::vector<uint64_t> m_image_acquired_ticks;
std::vector<vk::Semaphore> m_render_complete;
std::unique_ptr<SystemOverlay> m_system_overlay;
vk::DescriptorSetLayout m_overlay_descriptors = nullptr;
@ -346,6 +360,40 @@ struct Presenter::Impl {
std::atomic<uint64_t> presented_overlay_revision {0};
};
vk::SurfaceFormatKHR Swapchain::SelectFormat() {
const auto& formats = m_window.surface_capabilities.formats;
auto& graphics = m_window.graphic_ctx;
const auto hdr_format = std::ranges::find_if(formats, [&](const auto& candidate) {
return candidate.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT &&
IsHdrSwapchainFormat(candidate.format) &&
static_cast<bool>(
graphics.GetFormatProperties(candidate.format).optimalTilingFeatures &
vk::FormatFeatureFlagBits::eBlitDst);
});
m_hdr_supported = Config::HdrEnabled() && hdr_format != formats.end();
m_hdr_active = m_hdr_requested && IsHdrSupported();
if (m_hdr_active) {
return *hdr_format;
}
if (Config::HdrEnabled() && !IsHdrSupported()) {
LOGF("HDR10 surface unavailable; using SDR output\n");
}
if (formats.size() == 1 && formats.front().format == vk::Format::eUndefined) {
return {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear};
}
for (const auto& candidate: formats) {
if (candidate.colorSpace != vk::ColorSpaceKHR::eSrgbNonlinear) {
continue;
}
switch (candidate.format) {
case vk::Format::eB8G8R8A8Unorm:
case vk::Format::eR8G8B8A8Unorm: return candidate;
default: break;
}
}
EXIT("no supported UNORM swapchain format\n");
}
void Swapchain::Create() {
auto& graphics = m_window.graphic_ctx;
EXIT_IF(graphics.device == nullptr);
@ -385,18 +433,7 @@ void Swapchain::Create() {
? vk::CompositeAlphaFlagBitsKHR::eOpaque
: vk::CompositeAlphaFlagBitsKHR::eInherit;
vk::SurfaceFormatKHR format {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear};
if (surface.formats.size() != 1 || surface.formats.front().format != vk::Format::eUndefined) {
const auto it = std::find_if(surface.formats.begin(), surface.formats.end(),
[](const vk::SurfaceFormatKHR& candidate) {
return candidate.format == vk::Format::eB8G8R8A8Unorm ||
candidate.format == vk::Format::eR8G8B8A8Unorm;
});
if (it == surface.formats.end()) {
EXIT("no supported UNORM swapchain format\n");
}
format = *it;
}
const auto format = SelectFormat();
m_format = format.format;
const auto swapchain_features = graphics.GetFormatProperties(m_format).optimalTilingFeatures;
if (!static_cast<bool>(swapchain_features & vk::FormatFeatureFlagBits::eBlitDst)) {
@ -436,6 +473,7 @@ void Swapchain::Create() {
RequireVulkanSuccess(graphics.device.createSwapchainKHR(&create_info, nullptr, &m_handle),
"vkCreateSwapchainKHR");
EXIT_IF(m_handle == nullptr);
LOGF("Vulkan presentation: %s\n", m_hdr_active ? "HDR10 (PQ)" : "SDR");
m_images = EnumerateVulkan<vk::Image>(
"vkGetSwapchainImagesKHR", [&](uint32_t* count, vk::Image* images) {
@ -464,6 +502,7 @@ void Swapchain::Create() {
vk::SemaphoreCreateInfo semaphore_info {};
semaphore_info.sType = vk::StructureType::eSemaphoreCreateInfo;
m_image_acquired.resize(m_images.size());
m_image_acquired_ticks.assign(m_images.size(), 0);
m_render_complete.resize(m_images.size());
for (size_t i = 0; i < m_images.size(); i++) {
RequireVulkanSuccess(
@ -532,6 +571,7 @@ void Swapchain::Destroy() {
m_images.clear();
m_image_views.clear();
m_image_acquired.clear();
m_image_acquired_ticks.clear();
m_render_complete.clear();
}
@ -557,8 +597,11 @@ bool Swapchain::NeedsResize() const {
m_window_extent.height != m_window.graphic_ctx.screen_height;
}
Swapchain::Status Swapchain::AcquireNextImage() {
Swapchain::Status Swapchain::AcquireNextImage(CommandScheduler& scheduler) {
EXIT_IF(m_handle == nullptr || m_frame_index >= m_image_acquired.size());
// Repeated frames can wrap the semaphore ring without acquiring a new source frame.
// Wait for the submission that consumed this slot before signaling it again.
scheduler.Wait(m_image_acquired_ticks[m_frame_index]);
m_image_index = static_cast<uint32_t>(-1);
const auto result = m_window.graphic_ctx.device.acquireNextImageKHR(
m_handle, std::numeric_limits<uint64_t>::max(), m_image_acquired[m_frame_index], nullptr,
@ -692,9 +735,11 @@ void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& l
write.descriptorType = vk::DescriptorType::eCombinedImageSampler;
write.pImageInfo = &image;
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eGraphics, m_overlay_layout, 0, 1, &write);
const uint32_t premultiplied = layer.premultiplied_alpha;
const uint32_t flags = (layer.premultiplied_alpha ? 1u : 0u) |
(m_hdr_active ? 2u : 0u) | (frame.hdr ? 4u : 0u) |
(frame.image.format == vk::Format::eR16G16B16A16Sfloat ? 8u : 0u);
command.pushConstants(m_overlay_layout, vk::ShaderStageFlagBits::eFragment, 0,
sizeof(premultiplied), &premultiplied);
sizeof(flags), &flags);
command.bindPipeline(vk::PipelineBindPoint::eGraphics, m_overlay_pipeline);
vk::RenderingAttachmentInfo attachment {};
attachment.imageView = m_image_views[m_image_index];
@ -828,7 +873,9 @@ uint64_t Swapchain::Submit(CommandScheduler& scheduler) {
SubmitInfo submit;
submit.AddWait(m_image_acquired[m_frame_index], 1, vk::PipelineStageFlagBits::eTransfer);
submit.AddSignal(m_render_complete[m_image_index]);
return scheduler.Submit(submit);
const auto tick = scheduler.Submit(submit);
m_image_acquired_ticks[m_frame_index] = tick;
return tick;
}
Swapchain::Status Swapchain::Present() {
@ -868,7 +915,7 @@ Presenter::Presenter(WindowContext& window): m_impl(std::make_unique<Impl>(windo
Presenter::~Presenter() = default;
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info) {
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(buffer.IsInvalid());
auto frame_format = info.pixel_format;
@ -878,6 +925,8 @@ Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo
default: break;
}
auto* frame = m_impl->frames.Acquire({info.extent.width, info.extent.height}, frame_format);
frame->hdr = hdr;
frame->blank = false;
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
auto& image = m_impl->ResolveSurface(info);
if (image.backing.format == vk::Format::eUndefined) {
@ -894,6 +943,8 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height,
KYTY_PROFILER_FUNCTION();
auto format = m_impl->frames.GetFormat();
auto* frame = m_impl->frames.Acquire({width, height}, format);
frame->hdr = false;
frame->blank = true;
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
frame->Configure(m_impl->window.graphic_ctx, {width, height}, format);
vk::ClearColorValue clear {};
@ -922,6 +973,10 @@ bool Presenter::IsGuestPaused() const noexcept {
return m_impl->window.loop.paused.load(std::memory_order_acquire);
}
bool Presenter::IsHdrSupported() const noexcept {
return m_impl->swapchain.IsHdrSupported();
}
bool Presenter::NeedsSystemOverlayRefresh() const noexcept {
const auto visual = GetSystemOverlayVisualState();
return visual.active ||
@ -967,12 +1022,16 @@ void Presenter::Impl::Present() {
KYTY_PROFILER_FUNCTION();
const auto overlay_visual = GetSystemOverlayVisualState();
// Keep black/blank flips in the current color space; only real main-bus frames switch it.
const auto* main_frame = layers[0].frame;
const bool hdr_changed = main_frame != nullptr && !main_frame->blank &&
swapchain.SetHdr(main_frame->hdr);
// Some window systems keep presenting an old swapchain after a resize.
if (swapchain.NeedsResize()) {
if (hdr_changed || swapchain.NeedsResize()) {
RecoverSwapchain(Swapchain::Status::Recreate);
}
for (uint32_t attempt = 0; attempt < 2; attempt++) {
auto status = swapchain.AcquireNextImage();
auto status = swapchain.AcquireNextImage(present_scheduler);
if (status != Swapchain::Status::Success) {
RecoverSwapchain(status);
continue;

View File

@ -63,6 +63,7 @@ vk::PhysicalDeviceVulkan12Features WindowContext::RequiredVulkan12Features() noe
features.shaderBufferInt64Atomics = VK_TRUE;
features.storageBuffer8BitAccess = VK_TRUE;
features.shaderSampledImageArrayNonUniformIndexing = VK_TRUE;
features.shaderStorageBufferArrayNonUniformIndexing = VK_TRUE;
return features;
}
@ -284,6 +285,9 @@ static void VulkanFindPhysicalDevice(vk::Instance instance, vk::SurfaceKHR surfa
check_feature(features12.shaderSampledImageArrayNonUniformIndexing,
"shaderSampledImageArrayNonUniformIndexing",
required_features12.shaderSampledImageArrayNonUniformIndexing);
check_feature(features12.shaderStorageBufferArrayNonUniformIndexing,
"shaderStorageBufferArrayNonUniformIndexing",
required_features12.shaderStorageBufferArrayNonUniformIndexing);
check_feature(features13.robustImageAccess, "robustImageAccess");
check_feature(features13.dynamicRendering, "dynamicRendering",
required_features13.dynamicRendering);
@ -478,6 +482,13 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
provoking_vertex.pNext = supported_features2.pNext;
supported_features2.pNext = &provoking_vertex;
}
const bool maintenance8_extension =
HasExtension(device_extensions, VK_KHR_MAINTENANCE_8_EXTENSION_NAME);
vk::PhysicalDeviceMaintenance8FeaturesKHR maintenance8 {};
if (maintenance8_extension) {
maintenance8.pNext = supported_features2.pNext;
supported_features2.pNext = &maintenance8;
}
const bool image_atomic_int64_extension =
HasExtension(device_extensions, VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME);
vk::PhysicalDeviceShaderImageAtomicInt64FeaturesEXT image_atomic_int64 {};
@ -542,6 +553,9 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
graphics.attachment_feedback_loop_enabled =
feedback_extensions && feedback_layout.attachmentFeedbackLoopLayout &&
feedback_dynamic.attachmentFeedbackLoopDynamicState;
graphics.maintenance8_enabled = maintenance8_extension && maintenance8.maintenance8;
LOGF("Vulkan maintenance8 support: %s\n",
graphics.maintenance8_enabled ? "true" : "false");
LOGF("Vulkan depth feedback support: %s\n",
graphics.attachment_feedback_loop_enabled ? "true" : "false");
if (graphics.mesh_shader_enabled) {
@ -638,6 +652,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
image_atomic_int64.sparseImageInt64Atomics = VK_FALSE;
create_info.pNext = &image_atomic_int64;
}
if (graphics.maintenance8_enabled) {
maintenance8.pNext = const_cast<void*>(create_info.pNext);
create_info.pNext = &maintenance8;
}
create_info.pQueueCreateInfos = &queue_create_info;
create_info.queueCreateInfoCount = 1;
create_info.enabledExtensionCount = static_cast<uint32_t>(device_extensions.size());
@ -671,6 +689,11 @@ static void VulkanGetExtensions(VulkanExtensions& r) {
});
r.enable_validation_layers = Config::VulkanValidationEnabled();
if (Config::HdrEnabled() &&
HasExtension(r.available_extensions, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME) &&
!HasExtension(r.required_extensions, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) {
r.required_extensions.push_back(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME);
}
if (HasExtension(r.available_extensions, VK_EXT_DEBUG_UTILS_EXTENSION_NAME)) {
r.required_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
@ -1002,6 +1025,7 @@ void WindowContext::CreateVulkan() {
graphic_ctx.memory_budget_ext_enabled = true;
}
for (const auto* extension: {VK_EXT_ROBUSTNESS_2_EXTENSION_NAME,
VK_KHR_MAINTENANCE_8_EXTENSION_NAME,
VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME,
VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME,
VK_EXT_MESH_SHADER_EXTENSION_NAME,

View File

@ -326,6 +326,19 @@ static void GameEventController([[maybe_unused]] const EventController& f) {
const auto button = ControllerButtonToPadButton(f.button);
if (button != 0) {
Controller::SetButton(f.id, button, f.down);
} else if (f.button == SDL_GAMEPAD_BUTTON_BACK) {
// Pads without a touchpad (Switch, Xbox, ...) have no way to click it, which
// blocks games that require it. Use their Back/Select button as a centered click.
auto* pad = SDL_GetGamepadFromID(f.id);
if (pad != nullptr && SDL_GetNumGamepadTouchpads(pad) == 0) {
if (f.down) {
Controller::SetTouchPad(f.id, 0, true, 0.5f, 0.5f);
}
Controller::SetButton(f.id, Controller::PAD_BUTTON_TOUCH_PAD, f.down);
if (f.up) {
Controller::SetTouchPad(f.id, 0, false, 0.5f, 0.5f);
}
}
}
}

View File

@ -789,19 +789,20 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
static_cast<uint64_t>(ir.blocks.size()), phase_ms());
IR::RewriteToSsa(ir.blocks);
IR::ConstantPropagationPass(ir.blocks, ir.wave_size);
IR::ResolveControlFlowIdentities(ir);
IR::RemoveIdentities(ir.blocks);
IR::EliminateDeadCode(ir.blocks);
const auto read_lane_stats = IR::EliminateReadLane(ir, ir.wave_size);
if (read_lane_stats.rewritten_reads != 0) {
LOGF("%s read-lane elimination: reads=%" PRIu32 "\n", GetDumpLabel(options),
read_lane_stats.rewritten_reads);
const auto simplify = [&ir] {
IR::ConstantPropagationPass(ir.blocks, ir.wave_size);
IR::ResolveControlFlowIdentities(ir);
IR::RemoveIdentities(ir.blocks);
IR::EliminateDeadCode(ir.blocks);
};
simplify();
const auto read_lane_stats = IR::EliminateReadLane(ir, ir.wave_size);
if (read_lane_stats.rewritten_reads != 0) {
LOGF("%s read-lane elimination: reads=%" PRIu32 "\n", GetDumpLabel(options),
read_lane_stats.rewritten_reads);
}
// Cleanup also exposes profitable folds by removing dead predicate consumers.
simplify();
if (decoded.has_swap_pc) {
// Leaf returns are resolved CFG edges; native PC values must not escape into GPU data.
for (const auto* block: ir.blocks)
@ -817,7 +818,7 @@ TranslateResult TranslateProgram(const Decoder::Program& decoded, const CompileO
MakeIrDump(cfg_dump, ir).c_str());
}
}
IR::TrackResources(ir, decoded, native_cfg);
IR::TrackResources(ir, decoded, native_cfg, options.input_info);
TranslateResult result;
result.program = std::move(ir);
if (options.dump_ir) {
@ -885,7 +886,8 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram\n",
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash);
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword);
auto spirv = Spirv::EmitProgram(ir, options.input_info, push_data_start_dword,
options.maintenance8_enabled);
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " SPIR-V EmitProgram words=%" PRIu64
" elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(ir.stage), ir.shader_hash,

View File

@ -21,6 +21,7 @@ struct CompileOptions {
uint64_t shader_hash = 0;
bool dump_ir = true;
bool early_dump = false;
bool maintenance8_enabled = false;
const char* dump_label = nullptr;
std::span<const uint32_t> user_data;
std::span<const uint32_t> back_code;

View File

@ -197,7 +197,7 @@ void ValidateNativeProgram(const IR::Program& program, bool lds_storage) {
} // namespace
std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo input_info,
uint32_t push_data_start_dword) {
uint32_t push_data_start_dword, bool maintenance8_enabled) {
using namespace Emitter;
if (program.stage != ShaderType::Compute && program.stage != ShaderType::Vertex &&
@ -215,7 +215,7 @@ std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo inp
}
IR::ValidateProgram(program, true);
program.bindings = {.push_data_start_dword = push_data_start_dword};
EmitterState state(program, input_info);
EmitterState state(program, input_info, maintenance8_enabled);
const auto* workgroup = ShaderWorkgroupInput(program.stage, input_info);
state.lane_count =
workgroup != nullptr && program.wave_size == 64u && workgroup->host_subgroup_size == 32u

View File

@ -11,7 +11,8 @@ namespace Libs::Graphics::ShaderRecompiler::Spirv {
// Resets and writes backend IDs and descriptor indices; do not emit the same program concurrently.
std::vector<uint32_t> EmitProgram(IR::Program& program, ShaderStageInputInfo input_info,
uint32_t push_data_start_dword = 0);
uint32_t push_data_start_dword = 0,
bool maintenance8_enabled = false);
} // namespace Libs::Graphics::ShaderRecompiler::Spirv

View File

@ -269,20 +269,13 @@ uint32_t ResultVector(ValueEmitContext& ctx, uint32_t value,
result, packed[0], packed[1], packed[2], packed[3]);
return result;
}
uint32_t component[4] {};
for (uint32_t index = 0; index < 4u; index++) {
if (dref) {
component[index] = index == 0u ? FloatBits(ctx, value) : ConstantU32(ctx.state, 0);
continue;
}
const auto scalar = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(
spv::OpCompositeExtract, ImageScalarType(ctx.state, value_class), scalar, value, index);
component[index] = SampledComponentBits(ctx, scalar, value_class);
}
if (!dref)
return value_class == Prospero::TextureNumericClass::Uint
? value : Unary(ctx.state, spv::OpBitcast, TypeU32Vector(ctx.state, 4), value);
const auto result = ctx.state.builder.AllocateId();
const auto zero = ConstantU32(ctx.state, 0);
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(ctx.state, 4), result,
component[0], component[1], component[2], component[3]);
FloatBits(ctx, value), zero, zero, zero);
return result;
}
@ -323,25 +316,26 @@ uint32_t QueryDimensions(ValueEmitContext& ctx, const IR::MemoryInfo& mem,
return vector;
}
uint32_t PackedOffset(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
const ImageSampleLayout& layout, ImageDimension dimension) {
uint32_t PackedOffset(ValueEmitContext& ctx, IR::Value value, ImageDimension dimension,
uint32_t& mask) {
const auto components = ImageDimensionInfoFor(dimension).spatial_components;
const auto zero = ConstantI32(ctx.state, 0);
if (layout.offset == NoImageComponent || mem.image_address_components <= layout.offset) {
if (components == 1u) return zero;
const auto result = ctx.state.builder.AllocateId();
if (components == 3u) {
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 3),
result, zero, zero, zero);
} else {
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 2),
result, zero, zero);
uint32_t values[3] {};
if (value.IsImmediate()) {
// RDNA2 packs signed six-bit texel offsets into the low bits of each byte.
for (uint32_t index = 0; index < components; index++) {
const auto bits = (value.U32() >> (index * 8u)) & 0x3fu;
values[index] = ConstantI32(ctx.state, static_cast<int32_t>(bits ^ 0x20u) - 0x20);
}
return result;
mask |= spv::ImageOperandsConstOffsetMask;
return components == 1u
? values[0]
: ctx.state.builder.Constant(spv::OpConstantComposite,
TypeI32Vector(ctx.state, components),
std::span(values).first(components));
}
const auto packed = Unary(ctx.state, spv::OpBitcast, TypeI32(ctx.state),
AddressU32(ctx, mem, address, layout.offset));
uint32_t values[3] = {zero, zero, zero};
ctx.state.builder.RequireCapability(spv::CapabilityImageGatherExtended);
mask |= spv::ImageOperandsOffsetMask;
const auto packed = Unary(ctx.state, spv::OpBitcast, TypeI32(ctx.state), ctx.Def(value));
for (uint32_t index = 0; index < components; index++) {
values[index] = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpBitFieldSExtract, TypeI32(ctx.state), values[index],
@ -350,13 +344,8 @@ uint32_t PackedOffset(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR
}
if (components == 1u) return values[0];
const auto result = ctx.state.builder.AllocateId();
if (components == 3u) {
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 3),
result, values[0], values[1], values[2]);
} else {
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 2),
result, values[0], values[1]);
}
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, components),
result, std::span(values).first(components));
return result;
}
@ -388,17 +377,28 @@ uint32_t InverseSwizzle(uint32_t swizzle, uint32_t component) {
Format::BufferFormatInfo ImageConversionFormat(const EmitterState& state,
const IR::MemoryInfo& mem) {
const auto format = state.program.info.images[mem.resource].conversion_format;
const auto& image = state.program.info.images[mem.resource];
const auto format = image.conversion_format;
if (format == Prospero::BufferFormat::kInvalid) return {};
const auto info = Format::GetFormatInfo(format);
EXIT_IF(Prospero::RemapTextureFormat(format) == format || info.component_count == 0u ||
info.component_count > 4u);
EXIT_IF(info.type != Format::ComponentType::Uscaled &&
(info.type != Format::ComponentType::Uint || !info.packed_bitfield ||
((info.type != Format::ComponentType::Uint &&
info.type != Format::ComponentType::Unorm) || !info.packed_bitfield ||
info.byte_size != sizeof(uint32_t)));
return info;
}
uint32_t ExtractPackedImageComponent(EmitterState& state, const Format::BufferFormatInfo& info,
uint32_t packed, uint32_t type, uint32_t component) {
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitFieldUExtract, type, result, packed,
ConstantU32(state, info.component_bit_offset[component]),
ConstantU32(state, info.component_bits[component]));
return result;
}
uint32_t ImageGatherSource(const EmitterState& state, const IR::MemoryInfo& mem) {
const auto component = ImageGatherComponent(mem.dmask);
const auto info = ImageConversionFormat(state, mem);
@ -422,13 +422,11 @@ uint32_t UnpackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint
}
uint32_t components[4] {};
for (uint32_t component = 0; component < info.component_count; component++) {
components[component] = ctx.state.builder.AllocateId();
if (info.packed_bitfield) {
ctx.state.builder.AddFunction(spv::OpBitFieldUExtract, scalar_type,
components[component], packed,
ConstantU32(ctx.state, info.component_bit_offset[component]),
ConstantU32(ctx.state, info.component_bits[component]));
components[component] =
ExtractPackedImageComponent(ctx.state, info, packed, scalar_type, component);
} else {
components[component] = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeExtract, scalar_type,
components[component], texel, component);
// UNorm backing preserves the guest's filtering; scaling precedes swizzle constants.
@ -488,21 +486,74 @@ uint32_t UnpackImageGather(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uin
return Binary(ctx.state, spv::OpFMul, vector_type, gathered, scales);
}
uint32_t values[4] {};
for (uint32_t lane = 0; lane < 4u; lane++) {
const auto packed = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), packed, gathered,
lane);
values[lane] = ctx.state.builder.AllocateId();
const auto physical = (selector - 4u) % info.component_count;
ctx.state.builder.AddFunction(spv::OpBitFieldUExtract, TypeU32(ctx.state), values[lane],
packed,
ConstantU32(ctx.state, info.component_bit_offset[physical]),
ConstantU32(ctx.state, info.component_bits[physical]));
return ExtractPackedImageComponent(ctx.state, info, gathered, TypeU32Vector(ctx.state, 4),
(selector - 4u) % info.component_count);
}
uint32_t SamplePackedUnormLz(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t coord) {
auto& state = ctx.state;
const auto& image = state.program.info.images[mem.resource];
const auto info = ImageConversionFormat(state, mem);
const auto sampled = MakeSampledImage(state, mem.resource,
LoadSamplerDescriptor(state, mem.sampler));
const auto gathered = state.builder.AllocateId();
state.builder.AddFunction(spv::OpImageGather, TypeU32Vector(state, 4), gathered, sampled,
coord, ConstantU32(state, 0u));
state.builder.RequireCapability(spv::CapabilityImageQuery);
const auto size = state.builder.AllocateId();
state.builder.AddFunction(spv::OpImageQuerySizeLod, TypeU32Vector(state, 2), size,
LoadImageDescriptor(state, mem.resource), ConstantU32(state, 0u));
const auto size_f32 = Unary(state, spv::OpConvertUToF, TypeF32Vector(state, 2), size);
const auto half = ConstantF32Value(state, 0.5f);
const auto halves = state.builder.Constant(spv::OpConstantComposite,
TypeF32Vector(state, 2), half, half);
const auto fractions = state.builder.AllocateId();
state.builder.AddFunction(
spv::OpExtInst, TypeF32Vector(state, 2), fractions, GlslStd450(state), GLSLstd450Fract,
Binary(state, spv::OpFSub, TypeF32Vector(state, 2),
Binary(state, spv::OpFMul, TypeF32Vector(state, 2), coord, size_f32), halves));
uint32_t fraction[2];
uint32_t complement[2];
for (uint32_t axis = 0; axis < 2u; axis++) {
fraction[axis] = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeExtract, TypeF32(state), fraction[axis],
fractions, axis);
complement[axis] = Binary(state, spv::OpFSub, TypeF32(state),
ConstantF32Value(state, 1.0f), fraction[axis]);
}
const auto result = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(ctx.state, 4), result,
values[0], values[1], values[2], values[3]);
// Gather order is (x0, y1), (x1, y1), (x1, y0), (x0, y0).
const auto weights = state.builder.AllocateId();
state.builder.AddFunction(
spv::OpCompositeConstruct, TypeF32Vector(state, 4), weights,
Binary(state, spv::OpFMul, TypeF32(state), complement[0], fraction[1]),
Binary(state, spv::OpFMul, TypeF32(state), fraction[0], fraction[1]),
Binary(state, spv::OpFMul, TypeF32(state), fraction[0], complement[1]),
Binary(state, spv::OpFMul, TypeF32(state), complement[0], complement[1]));
uint32_t decoded[4] {};
uint32_t selected[4] {ZeroF32(state), ZeroF32(state), ZeroF32(state), ZeroF32(state)};
for (uint32_t component = 0; component < 4u; component++) {
if ((mem.dmask & (1u << component)) == 0u) continue;
const auto selector = (image.shader_swizzle >> (component * 3u)) & 7u;
if (selector == 1u) {
selected[component] = ConstantF32Value(state, 1.0f);
} else if (selector >= 4u) {
const auto physical = (selector - 4u) % info.component_count;
if (decoded[physical] == 0u) {
const auto texels = Unary(
state, spv::OpConvertUToF, TypeF32Vector(state, 4),
ExtractPackedImageComponent(state, info, gathered,
TypeU32Vector(state, 4), physical));
decoded[physical] = Binary(
state, spv::OpFMul, TypeF32(state),
Binary(state, spv::OpDot, TypeF32(state), texels, weights),
ConstantF32Value(state, 1.0f / ((1u << info.component_bits[physical]) - 1u)));
}
selected[component] = decoded[physical];
}
}
const auto result = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeConstruct, TypeF32Vector(state, 4), result,
selected[0], selected[1], selected[2], selected[3]);
return result;
}
@ -548,47 +599,22 @@ uint32_t EmitOneDimensionalGatherLz(ValueEmitContext& ctx, const IR::MemoryInfo&
return result;
}
uint32_t PackImageTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t texel) {
const auto info = ImageConversionFormat(ctx.state, mem);
if (info.format == Prospero::BufferFormat::kInvalid) return texel;
EXIT_IF(info.type != Format::ComponentType::Uint || !info.packed_bitfield);
auto packed = ConstantU32(ctx.state, 0u);
for (uint32_t component = 0; component < info.component_count; component++) {
const auto value = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), value, texel,
component);
const auto maximum =
ConstantU32(ctx.state, info.component_bits[component] == 32u
? UINT32_MAX
: (1u << info.component_bits[component]) - 1u);
const auto within =
Binary(ctx.state, spv::OpULessThan, TypeBool(ctx.state), value, maximum);
const auto clamped = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpSelect, TypeU32(ctx.state), clamped, within, value,
maximum);
const auto shifted =
info.component_bit_offset[component] == 0u
? clamped
: Binary(ctx.state, spv::OpShiftLeftLogical, TypeU32(ctx.state), clamped,
ConstantU32(ctx.state, info.component_bit_offset[component]));
packed = Binary(ctx.state, spv::OpBitwiseOr, TypeU32(ctx.state), packed, shifted);
}
const auto zero = ConstantU32(ctx.state, 0u);
const auto result = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeU32Vector(ctx.state, 4), result,
packed, zero, zero, zero);
return result;
}
uint32_t StoreTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t data, bool integer) {
const auto swizzle = ctx.state.program.info.images[mem.resource].shader_swizzle;
uint32_t values[4] {};
const auto info = ImageConversionFormat(ctx.state, mem);
const bool packed = info.format != Prospero::BufferFormat::kInvalid;
EXIT_IF(packed && (!info.packed_bitfield ||
(info.type != Format::ComponentType::Uint &&
info.type != Format::ComponentType::Unorm)));
const auto zero = ConstantU32(ctx.state, 0);
uint32_t values[4] {zero, zero, zero, zero};
auto packed_value = zero;
const auto dmask = mem.dmask != 0u ? mem.dmask : 1u;
for (uint32_t component = 0; component < 4u; component++) {
for (uint32_t component = 0; component < (packed ? info.component_count : 4u); component++) {
const auto source = InverseSwizzle(swizzle, component);
uint32_t raw = ConstantU32(ctx.state, 0);
if (source < 4u && ((dmask >> source) & 1u) != 0u) {
const bool selected = source < 4u && ((dmask >> source) & 1u) != 0u;
uint32_t raw = zero;
if (selected) {
const auto packed_index = DmaskComponentIndex(dmask, source);
raw = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), raw, data,
@ -598,21 +624,39 @@ uint32_t StoreTexel(ValueEmitContext& ctx, const IR::MemoryInfo& mem, uint32_t d
raw = Binary(ctx.state, spv::OpShiftRightLogical, TypeU32(ctx.state), raw,
ConstantU32(ctx.state, 16u));
}
raw = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), raw,
ConstantU32(ctx.state, 0xffffu));
if (!packed) {
raw = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), raw,
ConstantU32(ctx.state, 0xffffu));
}
}
}
if (packed) {
if (!selected) continue;
auto value = EncodeFormattedStoreComponent(ctx.state, info, component, raw, mem.data_bits);
if (info.type == Format::ComponentType::Uint) {
value = EmitUMin32(ctx.state, value,
ConstantU32(ctx.state, (1u << info.component_bits[component]) - 1u));
}
if (info.component_bit_offset[component] != 0u) {
value = Binary(ctx.state, spv::OpShiftLeftLogical, TypeU32(ctx.state), value,
ConstantU32(ctx.state, info.component_bit_offset[component]));
}
packed_value = packed_value == zero ? value
: Binary(ctx.state, spv::OpBitwiseOr, TypeU32(ctx.state), packed_value, value);
continue;
}
values[component] = integer ? raw
: mem.data_bits == 16u
? EmitF16BitsToF32(ctx.state, raw)
: Unary(ctx.state, spv::OpBitcast, TypeF32(ctx.state), raw);
}
if (packed) values[0] = packed_value;
const auto texel = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeConstruct,
integer ? TypeU32Vector(ctx.state, 4)
: TypeF32Vector(ctx.state, 4),
texel, values[0], values[1], values[2], values[3]);
return PackImageTexel(ctx, mem, texel);
return texel;
}
template <typename Emit>
@ -699,6 +743,87 @@ uint32_t EmitImageAccess(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t r
result_type, EmitRun);
}
template <typename Emit>
uint32_t EmitImageResult(ValueEmitContext& ctx, const IR::Inst& inst, const Emit& emit) {
auto& state = ctx.state;
const auto& mem = ctx.Memory(inst);
const auto& image = state.program.info.images[mem.resource];
const bool gather = inst.GetOpcode() == IR::ValueOpcode::ImageGatherRaw;
const bool dref = HasFlag(mem, Decoder::ImageSampleFlagCompare);
const bool scalar = dref && !gather;
const bool mixed = std::ranges::any_of(image.indirect_resources, [&](uint32_t resource) {
return state.program.info.images[resource].numeric_class != image.numeric_class;
});
const auto native_type = [&](uint32_t resource) {
return scalar ? TypeF32(state) : ImageVectorType(
state, dref ? Prospero::TextureNumericClass::Float
: state.program.info.images[resource].numeric_class, 4);
};
const auto result_bits = [&](uint32_t resource, uint32_t value) {
auto candidate_mem = mem;
candidate_mem.resource = resource;
if (!scalar) {
value = gather ? UnpackImageGather(ctx, candidate_mem, value)
: UnpackImageTexel(ctx, candidate_mem, value);
}
return ResultVector(ctx, value,
dref ? Prospero::TextureNumericClass::Float
: state.program.info.images[resource].numeric_class,
scalar, candidate_mem, gather);
};
// RDNA returns register bits. Convert per typed run only when the join mixes
// numeric classes; homogeneous accesses keep their single conversion after it.
const auto value = EmitImageAccess(
ctx, inst, mixed ? TypeU32Vector(state, 4) : native_type(mem.resource),
[&](uint32_t resource, uint32_t array_index) {
const auto native = emit(resource, array_index, native_type(resource));
return mixed ? result_bits(resource, native) : native;
});
return mixed ? value : result_bits(mem.resource, value);
}
struct ImageSampler {
uint32_t id = 0;
bool dynamic = false;
};
std::array<ImageSampler, 2> LoadImageSamplers(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
const auto& mem = ctx.Memory(inst);
const auto& samplers = state.program.info.samplers;
const auto& root = samplers.at(mem.sampler);
std::array<ImageSampler, 2> result {};
const auto load = [&](const IR::ImageResource& image) {
const bool integer = image.numeric_class != Prospero::TextureNumericClass::Float;
auto& sampler = result[integer];
if (sampler.id != 0u) return;
uint32_t index = mem.sampler;
if (root.integer_border != integer) {
const auto found = std::ranges::find_if(samplers, [&](const auto& candidate) {
return candidate.snapshot_index == root.snapshot_index &&
candidate.integer_border == integer;
});
if (found == samplers.end()) ctx.Fail(inst, "has no typed sampler variant");
index = static_cast<uint32_t>(found - samplers.begin());
}
uint32_t array_index = 0;
const auto& candidates = samplers[index].indirect_resources;
if (!candidates.empty()) {
const auto* handle = inst.Arg(1).ResolveInstruction();
if (handle == nullptr || handle->GetOpcode() != IR::ValueOpcode::GetSamplerResource)
ctx.Fail(inst, "has invalid finite sampler key provenance");
array_index = EmitIndexSwitch(
state, ctx.Def(handle->Arg(0)), static_cast<uint32_t>(candidates.size()),
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, candidates[ordinal]); });
}
sampler = {LoadSamplerDescriptor(state, index, array_index), array_index != 0u};
};
const auto& image = state.program.info.images[mem.resource];
load(image);
for (const auto candidate: image.indirect_resources) load(state.program.info.images[candidate]);
return result;
}
} // namespace
void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
@ -715,6 +840,22 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
ctx.Define(inst, QueryDimensions(ctx, mem, *address));
return;
}
if (image.resource_class == IR::ImageResourceClass::Sampled &&
image.conversion_format == Prospero::BufferFormat::k11_11_10UNorm) {
if (op != IR::ValueOpcode::ImageSampleRaw ||
image.dimension != ImageDimension::Dim2D ||
mem.image_dimension != ImageDimension::Dim2D || mem.data_bits != 32u ||
mem.image_sample_flags != Decoder::ImageSampleFlagLevelZero ||
image.indirect_root != IR::ImageResource::NoIndirectImage ||
!state.program.info.samplers.at(mem.sampler).indirect_resources.empty()) {
ctx.Fail(inst, "packed UNorm sampling requires a direct 2D sample_lz");
return;
}
const auto coord = CoordF32(ctx, mem, *address, Layout(mem).coord, 2u);
ctx.Define(inst, ResultVector(ctx, SamplePackedUnormLz(ctx, mem, coord),
Prospero::TextureNumericClass::Float, false, mem));
return;
}
if (op == IR::ValueOpcode::ImageQueryLod) {
state.builder.RequireCapability(spv::CapabilityImageQuery);
const auto dimension = image.dimension;
@ -740,16 +881,14 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
return;
}
if (op == IR::ValueOpcode::ImageRead) {
const auto numeric_class = image.numeric_class;
const auto condition = ctx.Arg(inst, 2);
ctx.Define(
inst,
EmitValueOrDefaultIfCondition(
state, condition, TypeU32Vector(state, 4), ConstantU32CompositeZero(state, 4),
[&]() {
const auto color = EmitImageAccess(
ctx, inst, ImageVectorType(state, numeric_class, 4),
[&](uint32_t resource, uint32_t array_index) {
return EmitImageResult(
ctx, inst, [&](uint32_t resource, uint32_t array_index, uint32_t result_type) {
const auto& candidate = state.program.info.images[resource];
const auto dimension = candidate.dimension;
const auto& dimension_info = ImageDimensionInfoFor(dimension);
@ -758,19 +897,17 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto coord = CoordU32(ctx, mem, *address, dimension);
if (dimension_info.multisampled != 0u) {
state.builder.AddFunction(
spv::OpImageFetch, ImageVectorType(state, candidate.numeric_class, 4), fetched,
spv::OpImageFetch, result_type, fetched,
descriptor, coord, spv::ImageOperandsSampleMask,
AddressU32(ctx, mem, *address, dimension_info.coordinate_components));
} else {
state.builder.AddFunction(
spv::OpImageFetch, ImageVectorType(state, candidate.numeric_class, 4), fetched,
spv::OpImageFetch, result_type, fetched,
descriptor, coord, spv::ImageOperandsLodMask,
LodU32(ctx, mem, *address, mem.image_dimension));
}
return fetched;
});
return ResultVector(ctx, UnpackImageTexel(ctx, mem, color), numeric_class,
false, mem);
}));
return;
}
@ -794,21 +931,28 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto layout = Layout(mem);
const auto numeric_class = image.numeric_class;
const bool dref = HasFlag(mem, Decoder::ImageSampleFlagCompare);
IR::Value offset_value;
if (layout.offset != NoImageComponent) {
const auto value = address->Arg(layout.offset).Resolve();
offset_value = IR::ResolveActiveU32(value, inst.Arg(inst.NumArgs() - 1u).Resolve());
if (offset_value.IsEmpty()) offset_value = value;
}
if (dref && state.program.info.images[mem.resource].conversion_format !=
Prospero::BufferFormat::kInvalid) {
ctx.Fail(inst, "uses depth comparison with a converted image");
return;
}
if (op == IR::ValueOpcode::ImageGatherRaw) {
const auto coord = CoordF32(ctx, mem, *address, layout.coord,
dimension_info.coordinate_components, image.cube);
if (dimension == ImageDimension::Dim1D) {
if (dref || !HasFlag(mem, Decoder::ImageSampleFlagLevelZero) ||
if (image.indirect_root == mem.resource || dref ||
!HasFlag(mem, Decoder::ImageSampleFlagLevelZero) ||
HasFlag(mem, Decoder::ImageSampleFlagOffset) ||
HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
ctx.Fail(inst, "has an unsupported 1D gather variant");
return;
}
const auto coord = CoordF32(ctx, mem, *address, layout.coord,
dimension_info.coordinate_components, image.cube);
const auto sample = EmitOneDimensionalGatherLz(ctx, mem, coord, numeric_class);
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample),
numeric_class, false, mem, true));
@ -818,9 +962,6 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
ctx.Fail(inst, "has an unsupported 1D-array gather");
return;
}
const auto result_numeric_class = dref ? Prospero::TextureNumericClass::Float
: numeric_class;
const auto result_type = ImageVectorType(state, result_numeric_class, 4);
uint32_t component_or_dref;
if (dref) {
component_or_dref = layout.dref != NoImageComponent
@ -829,34 +970,40 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
} else {
component_or_dref = ConstantU32(state, ImageGatherSource(state, mem));
}
uint32_t operand_mask = 0;
uint32_t offset = 0;
if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
operand_mask = spv::ImageOperandsConstOffsetsMask;
offset = HorizontalOffsets(state, dimension);
} else if (layout.offset != NoImageComponent) {
operand_mask = spv::ImageOperandsOffsetMask;
offset = PackedOffset(ctx, mem, *address, layout, dimension);
}
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler);
const auto EmitGather = [&](uint32_t mip) {
const auto sampled = MakeSampledImage(state, mem.resource, sampler_id, mip);
const auto sample = state.builder.AllocateId();
const std::array operands {operand_mask, offset};
state.builder.AddFunction(
dref ? spv::OpImageDrefGather : spv::OpImageGather, result_type,
sample, sampled, coord, component_or_dref,
std::span(operands).first(operand_mask != 0u ? 2u : 0u));
return sample;
const auto samplers = LoadImageSamplers(ctx, inst);
const auto EmitGather = [&](uint32_t resource, uint32_t array_index, uint32_t result_type) {
const auto& candidate = state.program.info.images[resource];
const auto sampler = samplers[candidate.numeric_class != Prospero::TextureNumericClass::Float];
const auto dimension = candidate.dimension;
const auto coord = CoordF32(
ctx, mem, *address, layout.coord,
ImageDimensionInfoFor(dimension).coordinate_components, candidate.cube);
uint32_t operand_mask = 0;
uint32_t offset = 0;
if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
operand_mask = spv::ImageOperandsConstOffsetsMask;
offset = HorizontalOffsets(state, dimension);
} else if (layout.offset != NoImageComponent) {
offset = PackedOffset(ctx, offset_value, dimension, operand_mask);
}
const auto EmitMip = [&](uint32_t mip) {
const auto sampled = MakeSampledImage(state, resource, sampler.id, mip, array_index,
sampler.dynamic);
const auto sample = state.builder.AllocateId();
const std::array operands {operand_mask, offset};
state.builder.AddFunction(
dref ? spv::OpImageDrefGather : spv::OpImageGather, result_type,
sample, sampled, coord, component_or_dref,
std::span(operands).first(operand_mask != 0u ? 2u : 0u));
return sample;
};
return candidate.mip_mode == IR::ImageMipMode::Dynamic && candidate.mip_count > 1u
? EmitIndexSwitch(
state, GatherMip(ctx, inst, mem, *address, layout, candidate.mip_count),
candidate.mip_count, result_type, EmitMip)
: EmitMip(0u);
};
const auto sample = image.mip_mode == IR::ImageMipMode::Dynamic && image.mip_count > 1u
? EmitIndexSwitch(
state, GatherMip(ctx, inst, mem, *address, layout,
image.mip_count),
image.mip_count, result_type, EmitGather)
: EmitGather(0);
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample),
result_numeric_class, false, mem, true));
ctx.Define(inst, EmitImageResult(ctx, inst, EmitGather));
return;
}
const bool explicit_lod = HasFlag(mem, Decoder::ImageSampleFlagDerivative) ||
@ -869,16 +1016,14 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
} else if (dref) {
opcode = spv::OpImageSampleDrefImplicitLod;
}
uint32_t result_type = ImageVectorType(state, numeric_class, 4);
uint32_t dref_value = 0;
if (dref) {
result_type = TypeF32(state);
dref_value = ZeroF32(state);
if (layout.dref != NoImageComponent) {
dref_value = AddressF32(ctx, mem, *address, layout.dref);
}
}
std::array<uint32_t, 3> operands {};
std::array<uint32_t, 4> operands {};
size_t operand_count = 0;
if (HasFlag(mem, Decoder::ImageSampleFlagDerivative)) {
operands[0] = spv::ImageOperandsGradMask;
@ -898,36 +1043,34 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
operands[1] = AddressF32(ctx, mem, *address, layout.bias);
operand_count = 2;
}
const auto& samplers = state.program.info.samplers.at(mem.sampler).indirect_resources;
uint32_t sampler_index = 0;
if (!samplers.empty()) {
const auto* handle = inst.Arg(1).ResolveInstruction();
if (handle == nullptr || handle->GetOpcode() != IR::ValueOpcode::GetSamplerResource)
ctx.Fail(inst, "has invalid finite sampler key provenance");
sampler_index = EmitIndexSwitch(
state, ctx.Def(handle->Arg(0)), static_cast<uint32_t>(samplers.size()),
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, samplers[ordinal]); });
if (layout.offset != NoImageComponent &&
!offset_value.IsImmediate() && !state.maintenance8_enabled) {
ctx.Fail(inst, "requires maintenance8 for a dynamic sample texel offset");
}
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler, sampler_index);
const auto EmitSample = [&](uint32_t resource, uint32_t array_index) {
const auto samplers = LoadImageSamplers(ctx, inst);
const auto EmitSample = [&](uint32_t resource, uint32_t array_index, uint32_t result_type) {
const auto& candidate = state.program.info.images[resource];
const auto sampler = samplers[candidate.numeric_class != Prospero::TextureNumericClass::Float];
auto sample_operands = operands;
auto sample_operand_count = operand_count;
if (layout.offset != NoImageComponent) {
sample_operand_count = std::max(size_t{1}, sample_operand_count);
sample_operands[sample_operand_count++] = PackedOffset(
ctx, offset_value, candidate.dimension, sample_operands[0]);
}
const auto coord =
CoordF32(ctx, mem, *address, layout.coord,
ImageDimensionInfoFor(candidate.dimension).coordinate_components,
candidate.cube);
const auto sampled = MakeSampledImage(state, resource, sampler_id, 0u, array_index,
sampler_index != 0u);
const auto sampled = MakeSampledImage(state, resource, sampler.id, 0u, array_index,
sampler.dynamic);
const auto sample = state.builder.AllocateId();
state.builder.AddFunction(opcode, result_type, sample, sampled, coord,
std::span(&dref_value, dref ? 1u : 0u),
std::span<const uint32_t>(operands).first(operand_count));
std::span<const uint32_t>(sample_operands).first(sample_operand_count));
return sample;
};
auto result = EmitImageAccess(ctx, inst, result_type, EmitSample);
if (!dref) {
result = UnpackImageTexel(ctx, mem, result);
}
ctx.Define(inst, ResultVector(ctx, result, numeric_class, dref, mem));
ctx.Define(inst, EmitImageResult(ctx, inst, EmitSample));
return;
}
if (image_info.access == IR::ImageAccess::Atomic) {

View File

@ -5,15 +5,17 @@
#include <bit>
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
uint32_t EmitIdentity(ValueEmitContext& ctx, uint32_t value);
// Operand signatures distinguish SPIR-V IDs (uint32_t), raw literals/values
// (IR::Value), and instructions whose metadata or operand loads must stay lazy.
#define EMIT_NATIVE(name, opcode, type, ...) \
inline constexpr auto Emit##name = EmitNative<spv::opcode, IR::Type::type, __VA_ARGS__>;
EMIT_NATIVE(BitCastU16F16, OpBitcast, U32, uint32_t)
inline constexpr auto EmitBitCastU16F16 = EmitIdentity;
inline constexpr auto EmitBitCastF16U16 = EmitBitCastU16F16;
inline constexpr auto EmitConvertU32U16 = EmitBitCastU16F16;
inline constexpr auto EmitConvertU32U8 = EmitBitCastU16F16;
inline constexpr auto EmitBitCastU32F32 = EmitBitCastU16F16;
EMIT_NATIVE(BitCastU32F32, OpBitcast, U32, uint32_t)
EMIT_NATIVE(BitCastF32U32, OpBitcast, F32, uint32_t)
EMIT_NATIVE(BitCastU64F64, OpBitcast, U64, uint32_t)
EMIT_NATIVE(BitCastF64U64, OpBitcast, F64, uint32_t)
@ -207,7 +209,6 @@ uint32_t EmitFPSin(EmitterState& state, uint32_t arg0);
uint32_t EmitFPCos(EmitterState& state, uint32_t arg0);
#undef EMIT_NATIVE
uint32_t EmitIdentity(ValueEmitContext& ctx, uint32_t value);
void EmitVoid(ValueEmitContext&);
inline constexpr auto EmitReference = EmitVoid;
inline constexpr auto EmitReferenceU32 = EmitVoid;
@ -321,6 +322,8 @@ inline constexpr auto EmitSharedAtomicDec32 = EmitSharedIncDec;
inline constexpr auto EmitSharedAtomicSMin32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicUMin32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicSMax32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicUMax64 = EmitSharedAtomic64;
inline constexpr auto EmitSharedAtomicCmpSwap64 = EmitSharedAtomic64;
inline constexpr auto EmitSharedAtomicUMax32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicAnd32 = EmitAtomic32;
inline constexpr auto EmitSharedAtomicOr32 = EmitAtomic32;

View File

@ -73,9 +73,9 @@ struct ImageDefinition {
};
struct EmitterState {
EmitterState(IR::Program& program_, ShaderStageInputInfo input_info_)
EmitterState(IR::Program& program_, ShaderStageInputInfo input_info_, bool maintenance8_enabled_)
: builder(program_.stage == ShaderType::Mesh ? 0x00010400u : 0x00010300u),
program(program_), input_info(input_info_) {
program(program_), input_info(input_info_), maintenance8_enabled(maintenance8_enabled_) {
if (ShaderWorkgroupInput(program.stage, input_info) != nullptr) {
lds_storage_class = spv::StorageClassWorkgroup;
}
@ -88,6 +88,7 @@ struct EmitterState {
Builder builder;
IR::Program& program;
ShaderStageInputInfo input_info;
bool maintenance8_enabled;
uint32_t void_type = 0;
uint32_t bool_type = 0;
uint32_t u32_type = 0;
@ -398,6 +399,8 @@ uint32_t StorageBufferPackedStride(const EmitterState& state, const IR::MemoryIn
Prospero::BufferFormat StorageBufferFormat(const EmitterState& state, const IR::MemoryInfo& mem);
bool UsesSharedIndirectBufferStore(const EmitterState& state, uint32_t resource);
void EmitMemoryOffsets(EmitterState& state);
uint32_t LdsDwordCount(const EmitterState& state);
@ -409,6 +412,7 @@ struct MemoryResourceAccess {
uint32_t length = 0;
uint32_t byte_offset = 0;
uint32_t element_bits = 32;
bool non_uniform = false;
spv::MemoryAccessMask memory_access = spv::MemoryAccessMaskNone;
};
@ -418,7 +422,8 @@ MemoryResourceAccess PrepareMemoryResourceAccess(EmitterState& state, const IR::
MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
const IR::MemoryInfo& mem,
BufferDefinition& buffer);
BufferDefinition& buffer,
uint32_t array_index = 0);
uint32_t EmitMemoryElementInBounds(EmitterState& state, const MemoryResourceAccess& access,
uint32_t index);
@ -441,6 +446,9 @@ uint32_t EmitUFloatToF32Bits(EmitterState& state, uint32_t raw, uint32_t bits);
uint32_t NormalizeFormatComponent(EmitterState& state, const Format::BufferFormatInfo& info,
uint32_t component, uint32_t raw);
uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::BufferFormatInfo& info,
uint32_t component, uint32_t data, uint32_t data_bits);
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode);
uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t pointer,

View File

@ -249,10 +249,11 @@ uint32_t LoadBda(ValueEmitContext& ctx, uint32_t address, uint32_t active, uint3
});
}
uint32_t ByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem) {
uint32_t ByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
uint32_t prefix = 0) {
if (mem.kind == IR::ResourceKind::Buffer) {
const auto resource = ctx.state.program.info.buffers.at(mem.resource).descriptor_index;
const auto prefix = ctx.state.memory_byte_offsets[resource];
if (prefix == 0u) prefix = ctx.state.memory_byte_offsets[resource];
const auto address = Binary(ctx.state, spv::OpIAdd, TypeU32(ctx.state),
BufferByteAddress(ctx, inst, mem), prefix);
// The host binding prefix must not wrap an out-of-range guest address into the buffer.
@ -273,15 +274,10 @@ uint32_t ByteAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memo
return ScratchByteAddress(ctx, mem, ctx.Arg(inst, 1), ctx.Arg(inst, 2));
}
uint32_t DwordIndex(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem) {
auto address = ByteAddress(ctx, inst, mem);
if (mem.kind == IR::ResourceKind::Lds || mem.kind == IR::ResourceKind::Gds) {
// RDNA2 DS region addresses retain bits [15:2] after adding the byte offset.
address = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), address,
ConstantU32(ctx.state, 0xffffu));
}
uint32_t DwordIndex(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
uint32_t prefix = 0) {
return Binary(ctx.state, spv::OpShiftRightLogical, TypeU32(ctx.state),
address, ConstantU32(ctx.state, 2));
ByteAddress(ctx, inst, mem, prefix), ConstantU32(ctx.state, 2));
}
uint32_t LoadWordInBounds(ValueEmitContext& ctx, const MemoryResourceAccess& resource,
@ -540,8 +536,30 @@ void StoreLocalFlat(ValueEmitContext& ctx, const IR::Inst& inst) {
void StoreWordPrepared(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
const MemoryResourceAccess& resource, uint32_t data) {
const auto index = DwordIndex(ctx, inst, mem);
EmitIfCondition(ctx.state, EmitMemoryElementInBounds(ctx.state, resource, index), [&]() {
auto& state = ctx.state;
const auto source = mem.kind == IR::ResourceKind::Buffer
? state.program.info.buffers[mem.resource].source : UINT32_MAX;
const auto& sources = state.program.descriptor_sources;
const auto* indirect = source < sources.size() && sources[source].indirect_descriptor
? &*sources[source].indirect_descriptor : nullptr;
uint32_t index = 0;
uint32_t in_bounds = 0;
if (indirect != nullptr && indirect->indexed_store) {
// The resource plan proves stride-four, index-only stores. Check the original
// index before any byte multiplication can wrap it into the buffer.
const auto prefix = Binary(state, spv::OpShiftRightLogical, TypeU32(state),
resource.byte_offset, ConstantU32(state, 2));
const auto guest_index = ctx.Arg(inst, 1);
const auto records = Binary(state, spv::OpISub, TypeU32(state), resource.length, prefix);
in_bounds = AndCondition(state,
Binary(state, spv::OpULessThanEqual, TypeBool(state), prefix, resource.length),
Binary(state, spv::OpULessThan, TypeBool(state), guest_index, records));
index = EmitAddU32(state, guest_index, prefix);
} else {
index = DwordIndex(ctx, inst, mem, resource.byte_offset);
in_bounds = EmitMemoryElementInBounds(state, resource, index);
}
EmitIfCondition(state, in_bounds, [&]() {
StoreWordInBounds(ctx, resource, index, data);
});
}
@ -576,6 +594,15 @@ void StoreWord(ValueEmitContext& ctx, const IR::Inst& inst, IR::MemoryInfo mem)
const auto selected = EmitIndirectResourceIndex(
state, ctx.Arg(*handle, 0), buffer.indirect_mapping_offset,
buffer.indirect_search_iterations, UINT32_MAX);
if (UsesSharedIndirectBufferStore(state, mem.resource)) {
EmitIfCondition(state, Binary(state, spv::OpULessThan, TypeBool(state), selected,
ConstantU32(state, buffer.indirect_resources.size())), [&]() {
const auto slot = EmitAddU32(state, selected, ConstantU32(state, buffer.descriptor_index));
const auto access = PrepareStorageBufferResourceAccess(state, mem, state.storage_buffers[2], slot);
StoreWordPrepared(ctx, inst, mem, access, data);
});
return;
}
const auto merge = state.builder.AllocateId();
std::vector<uint32_t> labels(buffer.indirect_resources.size());
std::vector<uint32_t> branches {spv::OpSwitch, selected, merge};
@ -599,7 +626,12 @@ template <typename Fn>
uint32_t EmitAtomicAccess(ValueEmitContext& ctx, const IR::Inst& inst,
const IR::MemoryInfo& mem, Fn&& operation) {
return EmitValueOrZeroIfCondition(ctx.state, ctx.Arg(inst, inst.NumArgs() - 1), [&]() {
const auto index = DwordIndex(ctx, inst, mem);
auto index = DwordIndex(ctx, inst, mem);
if (mem.kind == IR::ResourceKind::Lds || mem.kind == IR::ResourceKind::Gds) {
// Keep region-address truncation local to DS atomics.
index = Binary(ctx.state, spv::OpBitwiseAnd, TypeU32(ctx.state), index,
ConstantU32(ctx.state, 0x3fffu));
}
const auto resource = PrepareMemoryResourceAccess(ctx.state, mem);
return EmitValueOrZeroIfCondition(
ctx.state, EmitMemoryElementInBounds(ctx.state, resource, index), [&]() {
@ -735,53 +767,6 @@ uint32_t FormattedLoad(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Me
});
}
uint32_t EncodeFormattedStoreComponent(ValueEmitContext& ctx,
const Format::BufferFormatInfo& info,
uint32_t component, uint32_t data, uint32_t data_bits) {
auto& state = ctx.state;
const auto bits = info.component_bits[component];
if (data_bits == 16u) {
if (info.type == Format::ComponentType::Uint) {
data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
} else if (info.type == Format::ComponentType::Sint) {
data = EmitBitFieldSExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
} else {
data = EmitBitCastU32F32(state, EmitF16BitsToF32(state, data));
}
}
const bool normalized = info.type == Format::ComponentType::Unorm ||
info.type == Format::ComponentType::Snorm;
const bool scaled = info.type == Format::ComponentType::Uscaled ||
info.type == Format::ComponentType::Sscaled;
if (normalized || scaled) {
// Two-bit packed channels are unsigned even in signed formats.
const bool is_signed = bits != 2u && (info.type == Format::ComponentType::Snorm ||
info.type == Format::ComponentType::Sscaled);
const auto max_value = static_cast<float>((1u << (bits - (is_signed ? 1u : 0u))) - 1u);
const auto lower = normalized ? (is_signed ? -1.0f : 0.0f)
: (is_signed ? -max_value - 1.0f : 0.0f);
const auto upper = normalized ? 1.0f : max_value;
auto value = Select(state, TypeF32(state), EmitClassifyF32Bits(state, data).nan,
ConstantF32Value(state, 0.0f), EmitBitCastF32U32(state, data));
value = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
state, value, ConstantF32Value(state, lower), ConstantF32Value(state, upper));
if (normalized) {
value = EmitFPRoundEven32(
state, EmitFPMul32(state, value, ConstantF32Value(state, max_value)));
}
// Float-to-integer conversion truncates scaled values toward zero.
value = Unary(state, is_signed ? spv::OpConvertFToS : spv::OpConvertFToU,
is_signed ? TypeI32(state) : TypeU32(state), value);
return is_signed ? Unary(state, spv::OpBitcast, TypeU32(state), value) : value;
}
if (bits == 16u && info.type == Format::ComponentType::Float) {
const auto pair = EmitCompositeConstructF32x2(
state, EmitBitCastF32U32(state, data), ConstantF32Value(state, 0.0f));
return EmitPackHalf2x16(state, pair);
}
return data;
}
void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst,
const IR::MemoryInfo& mem, const MemoryResourceAccess& resource,
const Format::BufferFormatInfo& info, uint32_t data,
@ -800,7 +785,7 @@ void StoreFormattedPrepared(ValueEmitContext& ctx, const IR::Inst& inst,
value, data, component);
}
}
value = EncodeFormattedStoreComponent(ctx, info, component, value, mem.data_bits);
value = EncodeFormattedStoreComponent(ctx.state, info, component, value, mem.data_bits);
const auto bits = info.component_bits[component];
if (info.packed_bitfield) {
packed = EmitBitFieldInsert(
@ -926,10 +911,9 @@ uint32_t IndirectBufferInBounds(EmitterState& state, const IndirectBufferAccess&
state, spv::OpIAdd, TypeU32(state), buffer.offset, ConstantU32(state, displacement));
const auto has_payload = [&](uint32_t size) {
return AndCondition(
state, Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), size,
ConstantU32(state, bytes)),
state, Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), size, bytes),
Binary(state, spv::OpULessThanEqual, TypeBool(state), offset,
Binary(state, spv::OpISub, TypeU32(state), size, ConstantU32(state, bytes))));
Binary(state, spv::OpISub, TypeU32(state), size, bytes)));
};
const auto structured = AndCondition(
state, buffer.index_in_bounds,
@ -970,7 +954,8 @@ uint32_t LoadIndirectBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_
ConstantU64(state, component * 4u));
values[component] = EmitValueOrZeroIfCondition(
state, AndCondition(state, valid_format,
IndirectBufferInBounds(state, buffer, component * 4u, 4u, false)),
IndirectBufferInBounds(state, buffer, component * 4u,
ConstantU32(state, 4u), false)),
[&]() { return LoadBdaDword(ctx, address); });
}
return ConstructU32Composite(state, components, values);
@ -980,9 +965,54 @@ uint32_t LoadIndirectFormattedX(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
return EmitValueOrZeroIfCondition(state, ctx.Arg(inst, inst.NumArgs() - 1), [&]() {
const auto buffer = PrepareIndirectBuffer(ctx, inst);
constexpr auto format_count =
static_cast<uint32_t>(Prospero::BufferFormat::k32_32_32_32Float) + 1u;
const auto geometry = EmitIndexSwitch(
state, buffer.format, format_count, TypeU32Composite(state, 4), [&](uint32_t format) {
const auto info = Format::GetFormatInfo(static_cast<Prospero::BufferFormat>(format));
return state.builder.Constant(
spv::OpConstantComposite, TypeU32Composite(state, 4),
ConstantU32(state, info.byte_size),
ConstantU32(state, std::max(1u, info.component_count)),
ConstantU32(state, info.packed_bitfield ? 0u : info.component_bits[0] / 8u),
ConstantU32(state, info.byte_size == 0u ? 0u : std::min(4u, info.byte_size) - 1u));
});
const auto field = [&](uint32_t index) {
const auto value = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), value, geometry, index);
return value;
};
const auto bytes = field(0);
const auto component = Binary(
state, spv::OpUMod, TypeU32(state),
Binary(state, spv::OpISub, TypeU32(state), buffer.selector, ConstantU32(state, 4)), field(1));
const auto alignment = Unary(state, spv::OpUConvert, TypeU64(state), field(3));
const auto base = Binary(state, spv::OpBitwiseAnd, TypeU64(state), buffer.address,
Unary(state, spv::OpNot, TypeU64(state), alignment));
const auto address = Binary(
state, spv::OpIAdd, TypeU64(state), base,
Unary(state, spv::OpUConvert, TypeU64(state),
Binary(state, spv::OpIMul, TypeU32(state), component, field(2))));
const auto memory = AndCondition(
state, Binary(state, spv::OpINotEqual, TypeBool(state), bytes, ConstantU32(state, 0)),
Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), buffer.selector,
ConstantU32(state, 4)));
const auto raw = EmitValueOrZeroIfCondition(
state, AndCondition(state, memory, IndirectBufferInBounds(state, buffer, 0u, bytes, true)),
[&]() {
// Format alignment and component spacing keep every selected component within one DWORD.
const auto word = LoadBdaDword(
ctx, Binary(state, spv::OpBitwiseAnd, TypeU64(state), address,
ConstantU64(state, ~uint64_t {3})));
const auto byte = Binary(
state, spv::OpBitwiseAnd, TypeU32(state),
Unary(state, spv::OpUConvert, TypeU32(state), address), ConstantU32(state, 3));
return Binary(state, spv::OpShiftRightLogical, TypeU32(state), word,
Binary(state, spv::OpShiftLeftLogical, TypeU32(state), byte,
ConstantU32(state, 3)));
});
return EmitIndexSwitch(
state, buffer.format, static_cast<uint32_t>(Prospero::BufferFormat::k32_32_32_32Float) + 1u,
TypeU32(state), [&](uint32_t format) {
state, buffer.format, format_count, TypeU32(state), [&](uint32_t format) {
const auto info = Format::GetFormatInfo(static_cast<Prospero::BufferFormat>(format));
if (info.type == Format::ComponentType::Unknown) return ConstantU32(state, 0);
const auto constant = Select(
@ -993,28 +1023,16 @@ uint32_t LoadIndirectFormattedX(ValueEmitContext& ctx, const IR::Inst& inst) {
state, Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), buffer.selector,
ConstantU32(state, 4)),
TypeU32(state), constant, [&]() {
const auto in_bounds = IndirectBufferInBounds(state, buffer, 0u, info.byte_size, true);
const auto base = Binary(
state, spv::OpBitwiseAnd, TypeU64(state), buffer.address,
ConstantU64(state, ~(uint64_t(std::min(4u, info.byte_size)) - 1u)));
const auto load = [&](uint32_t component, uint32_t bits) {
const auto offset = Format::GetFormatComponentByteOffset(info, component);
const auto address = offset == 0u
? base
: Binary(state, spv::OpIAdd, TypeU64(state),
base, ConstantU64(state, offset));
return LoadBda(ctx, address, in_bounds, bits);
};
const auto emit_component = [&](uint32_t component) {
return LoadFormattedComponent(
ctx, info, {FormattedSourceKind::Memory, component},
[&](uint32_t source) { return load(source, 32u); }, load);
[&](uint32_t) { return raw; },
[&](uint32_t, uint32_t bits) {
return Binary(state, spv::OpBitwiseAnd, TypeU32(state), raw,
ConstantU32(state, bits == 8u ? 0xffu : 0xffffu));
});
};
if (info.component_count == 1u) return emit_component(0u);
const auto component = Binary(
state, spv::OpUMod, TypeU32(state),
Binary(state, spv::OpISub, TypeU32(state), buffer.selector, ConstantU32(state, 4)),
ConstantU32(state, info.component_count));
if (!info.packed_bitfield) return emit_component(0u);
return EmitIndexSwitch(state, component, info.component_count, TypeU32(state),
emit_component);
});
@ -1222,32 +1240,48 @@ void EmitSharedAtomic64(ValueEmitContext& ctx, const IR::Inst& inst) {
auto& state = ctx.state;
const auto& mem = ctx.Memory(inst);
EnsureLdsStorage(state);
EmitIfCondition(state, ctx.Arg(inst, 2), [&]() {
const auto address = Binary(state, spv::OpBitwiseAnd, TypeU32(state),
ByteAddress(ctx, inst, mem), ConstantU32(state, 0xfff8u));
const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address,
ConstantU32(state, 3u));
const auto in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index,
ConstantU32(state, LdsDwordCount(state) / 2u));
EmitIfCondition(state, in_bounds, [&]() {
auto native_index = index;
auto semantics = spv::MemorySemanticsWorkgroupMemoryMask;
if (state.lds_storage_class == spv::StorageClassStorageBuffer) {
native_index = EmitAddU32(state, index, EmitBinaryU32(
state, spv::OpShiftRightLogical, state.lds_base_dwords, ConstantU32(state, 1)));
semantics = spv::MemorySemanticsUniformMemoryMask;
}
const auto pointer = state.builder.AllocateId();
const auto address = Binary(state, spv::OpBitwiseAnd, TypeU32(state),
ByteAddress(ctx, inst, mem), ConstantU32(state, 0xfff8u));
const auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), address,
ConstantU32(state, 3u));
const auto in_bounds = Binary(state, spv::OpULessThan, TypeBool(state), index,
ConstantU32(state, LdsDwordCount(state) / 2u));
const auto active = EmitLogicalAndBool(state, ctx.Arg(inst, inst.NumArgs() - 1), in_bounds);
const auto emit = [&]() {
auto native_index = index;
auto semantics = spv::MemorySemanticsWorkgroupMemoryMask;
if (state.lds_storage_class == spv::StorageClassStorageBuffer) {
native_index = EmitAddU32(state, index, EmitBinaryU32(
state, spv::OpShiftRightLogical, state.lds_base_dwords, ConstantU32(state, 1)));
semantics = spv::MemorySemanticsUniformMemoryMask;
}
const auto pointer = state.builder.AllocateId();
state.builder.AddFunction(
spv::OpAccessChain, TypePointer(state, state.lds_storage_class, TypeU64(state)),
pointer, state.lds_u64_variable, ConstantU32(state, 0), native_index);
const auto old = state.builder.AllocateId();
const auto opcode = SpirvAtomicOpcode(inst.GetOpcode());
const auto scope = ConstantU32(state, spv::ScopeWorkgroup);
const auto acquire_release =
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | semantics);
if (opcode == spv::OpAtomicCompareExchange) {
// RDNA2 DS compares DATA and stores DATA2, opposite to BUFFER atomics.
state.builder.AddFunction(
spv::OpAccessChain, TypePointer(state, state.lds_storage_class, TypeU64(state)),
pointer, state.lds_u64_variable, ConstantU32(state, 0), native_index);
const auto value = ctx.Arg(inst, 1);
state.builder.AddFunction(
SpirvAtomicOpcode(inst.GetOpcode()), TypeU64(state), state.builder.AllocateId(),
pointer, ConstantU32(state, spv::ScopeWorkgroup),
ConstantU32(state, spv::MemorySemanticsAcquireReleaseMask | semantics), value);
});
});
opcode, TypeU64(state), old, pointer, scope, acquire_release,
ConstantU32(state, spv::MemorySemanticsAcquireMask | semantics),
ctx.Arg(inst, 2), ctx.Arg(inst, 1));
} else {
state.builder.AddFunction(opcode, TypeU64(state), old, pointer, scope,
acquire_release, ctx.Arg(inst, 1));
}
return old;
};
if (inst.HasUses()) {
ctx.Define(inst, EmitValueOrDefaultIfCondition(
state, active, TypeU64(state), ConstantU64(state, 0), emit));
} else {
EmitIfCondition(state, active, emit);
}
}
uint32_t EmitBufferFloatAtomic(ValueEmitContext& ctx, const IR::Inst& inst) {

View File

@ -1,15 +1,14 @@
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h"
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInstructions.h"
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
uint32_t EmitShaderDataDwordLoad(EmitterState& state, uint32_t dword_index) {
static uint32_t EmitShaderDataDwordLoadAt(EmitterState& state, uint32_t dword_index) {
if (state.program.bindings.UsesPushData()) {
dword_index += state.program.bindings.push_data_start_dword;
const auto pointer = state.builder.AllocateId();
const auto value = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain, TypePushConstantElementPointer(state),
pointer, state.push_constant_variable, ConstantU32(state, 0),
ConstantU32(state, dword_index));
dword_index);
state.builder.AddFunction(spv::OpLoad, TypeU32(state), value, pointer);
return value;
}
@ -18,13 +17,20 @@ uint32_t EmitShaderDataDwordLoad(EmitterState& state, uint32_t dword_index) {
const auto value = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain, TypeStorageBufferElementPointer(state),
pointer, state.shader_data_storage_variable,
ConstantU32(state, 0), ConstantU32(state, dword_index));
ConstantU32(state, 0), dword_index);
state.builder.AddFunction(spv::OpLoad, TypeU32(state), value, pointer);
return value;
}
return ConstantU32(state, 0);
}
uint32_t EmitShaderDataDwordLoad(EmitterState& state, uint32_t dword_index) {
if (state.program.bindings.UsesPushData()) {
dword_index += state.program.bindings.push_data_start_dword;
}
return EmitShaderDataDwordLoadAt(state, ConstantU32(state, dword_index));
}
uint32_t EmitBinaryU32(EmitterState& state, spv::Op opcode, uint32_t lhs, uint32_t rhs) {
const auto ret = state.builder.AllocateId();
state.builder.AddFunction(opcode, TypeU32(state), ret, lhs, rhs);
@ -102,12 +108,38 @@ Prospero::BufferFormat StorageBufferFormat(const EmitterState& state, const IR::
return state.program.info.buffers[mem.resource].descriptor_format;
}
bool UsesSharedIndirectBufferStore(const EmitterState& state, uint32_t resource) {
const auto& buffers = state.program.info.buffers;
const auto& root = buffers[resource];
if (root.indirect_root != resource || root.descriptor_index == UINT32_MAX ||
root.packed_stride == 0u || root.indirect_resources.empty()) {
return false;
}
uint32_t slot = root.descriptor_index;
for (const auto candidate: root.indirect_resources) {
if (buffers[candidate].packed_stride != root.packed_stride ||
buffers[candidate].descriptor_index != slot++) return false;
}
return true;
}
void EmitMemoryOffsets(EmitterState& state) {
std::array<bool, IR::ShaderInfo::MaxBuffers> dynamic_offsets {};
for (uint32_t resource = 0; resource < state.program.info.buffers.size(); ++resource) {
if (!UsesSharedIndirectBufferStore(state, resource)) continue;
for (const auto candidate: state.program.info.buffers[resource].indirect_resources) {
dynamic_offsets[state.program.info.buffers[candidate].descriptor_index] = true;
}
}
const auto count = state.program.bindings.descriptor_counts[static_cast<size_t>(IR::DescriptorBindingKind::Buffers)];
for (uint32_t i = 0; i < count; i += 4u) {
const auto lanes = std::min(4u, count - i);
if (std::all_of(dynamic_offsets.begin() + i, dynamic_offsets.begin() + i + lanes,
[](bool dynamic) { return dynamic; })) continue;
const auto word =
EmitShaderDataDwordLoad(state, state.program.bindings.memory_offset_dword + i / 4u);
for (uint32_t lane = 0; lane < std::min(4u, count - i); ++lane) {
for (uint32_t lane = 0; lane < lanes; ++lane) {
if (dynamic_offsets[i + lane]) continue;
state.memory_byte_offsets[i + lane] = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
state, word, ConstantU32(state, lane * 8u), ConstantU32(state, 8u));
}
@ -152,7 +184,8 @@ void EnsureLdsStorage(EmitterState& state) {
MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
const IR::MemoryInfo& mem,
BufferDefinition& buffer) {
BufferDefinition& buffer,
uint32_t array_index) {
if (buffer.variable == 0) {
ExitDescriptorBindingFailure(state, IR::DescriptorBindingKind::Buffers, mem.resource,
"storage buffer descriptor array was not emitted");
@ -160,15 +193,34 @@ MemoryResourceAccess PrepareStorageBufferResourceAccess(EmitterState& state,
if (buffer.pointer_type == 0) {
buffer.pointer_type = TypePointer(state, spv::StorageClassStorageBuffer, buffer.type);
}
const auto array_index =
state.program.info.buffers.at(mem.resource).descriptor_index;
const bool non_uniform = array_index != 0u;
const auto static_index = state.program.info.buffers.at(mem.resource).descriptor_index;
if (!non_uniform) array_index = ConstantU32(state, static_index);
MemoryResourceAccess access {
.kind = mem.kind,
.non_uniform = non_uniform,
.memory_access = mem.coherent ? spv::MemoryAccessVolatileMask : spv::MemoryAccessMaskNone};
access.object_pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain, buffer.pointer_type, access.object_pointer,
buffer.variable, ConstantU32(state, array_index));
access.byte_offset = state.memory_byte_offsets[array_index];
buffer.variable, array_index);
if (non_uniform) {
state.builder.RequireExtension("SPV_EXT_descriptor_indexing");
state.builder.RequireCapability(spv::CapabilityShaderNonUniform);
state.builder.RequireCapability(spv::CapabilityStorageBufferArrayNonUniformIndexing);
state.builder.AddAnnotation(spv::OpDecorate, access.object_pointer, spv::DecorationNonUniform);
const auto offsets_start = state.program.bindings.memory_offset_dword +
(state.program.bindings.UsesPushData() ? state.program.bindings.push_data_start_dword : 0u);
const auto word = EmitShaderDataDwordLoadAt(state, EmitAddU32(
state, ConstantU32(state, offsets_start),
Binary(state, spv::OpShiftRightLogical, TypeU32(state), array_index, ConstantU32(state, 2))));
const auto shift = Binary(state, spv::OpShiftLeftLogical, TypeU32(state),
Binary(state, spv::OpBitwiseAnd, TypeU32(state), array_index, ConstantU32(state, 3)),
ConstantU32(state, 3));
access.byte_offset = EmitNative<spv::OpBitFieldUExtract, IR::Type::U32>(
state, word, shift, ConstantU32(state, 8));
} else {
access.byte_offset = state.memory_byte_offsets[static_index];
}
access.length = state.builder.AllocateId();
state.builder.AddFunction(spv::OpArrayLength, TypeU32(state), access.length,
access.object_pointer, 0);
@ -260,6 +312,9 @@ uint32_t EmitStorageBufferElementPointer(EmitterState& state,
const auto pointer = state.builder.AllocateId();
state.builder.AddFunction(spv::OpAccessChain, pointer_type, pointer, access.object_pointer,
ConstantU32(state, 0), index);
if (access.non_uniform) {
state.builder.AddAnnotation(spv::OpDecorate, pointer, spv::DecorationNonUniform);
}
return pointer;
}
@ -362,8 +417,54 @@ uint32_t NormalizeFormatComponent(EmitterState& state, const Format::BufferForma
}
}
uint32_t EncodeFormattedStoreComponent(EmitterState& state, const Format::BufferFormatInfo& info,
uint32_t component, uint32_t data, uint32_t data_bits) {
const auto bits = info.component_bits[component];
if (data_bits == 16u) {
if (info.type == Format::ComponentType::Uint) {
data = EmitBitFieldUExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
} else if (info.type == Format::ComponentType::Sint) {
data = EmitBitFieldSExtract(state, data, ConstantU32(state, 0), ConstantU32(state, 16));
} else {
data = EmitBitCastU32F32(state, EmitF16BitsToF32(state, data));
}
}
const bool normalized = info.type == Format::ComponentType::Unorm ||
info.type == Format::ComponentType::Snorm;
const bool scaled = info.type == Format::ComponentType::Uscaled ||
info.type == Format::ComponentType::Sscaled;
if (normalized || scaled) {
// Two-bit packed channels are unsigned even in signed formats.
const bool is_signed = bits != 2u && (info.type == Format::ComponentType::Snorm ||
info.type == Format::ComponentType::Sscaled);
const auto max_value = static_cast<float>((1u << (bits - (is_signed ? 1u : 0u))) - 1u);
const auto lower = normalized ? (is_signed ? -1.0f : 0.0f)
: (is_signed ? -max_value - 1.0f : 0.0f);
const auto upper = normalized ? 1.0f : max_value;
auto value = Select(state, TypeF32(state), EmitClassifyF32Bits(state, data).nan,
ConstantF32Value(state, 0.0f), EmitBitCastF32U32(state, data));
value = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
state, value, ConstantF32Value(state, lower), ConstantF32Value(state, upper));
if (normalized) {
value = EmitFPRoundEven32(
state, EmitFPMul32(state, value, ConstantF32Value(state, max_value)));
}
// Float-to-integer conversion truncates scaled values toward zero.
value = Unary(state, is_signed ? spv::OpConvertFToS : spv::OpConvertFToU,
is_signed ? TypeI32(state) : TypeU32(state), value);
return is_signed ? Unary(state, spv::OpBitcast, TypeU32(state), value) : value;
}
if (bits == 16u && info.type == Format::ComponentType::Float) {
const auto pair = EmitCompositeConstructF32x2(
state, EmitBitCastF32U32(state, data), ConstantF32Value(state, 0.0f));
return EmitPackHalf2x16(state, pair);
}
return data;
}
spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
switch (opcode) {
case IR::ValueOpcode::SharedAtomicCmpSwap64:
case IR::ValueOpcode::ImageAtomicCompareSwap32:
case IR::ValueOpcode::BufferAtomicCmpSwap32: return spv::OpAtomicCompareExchange;
case IR::ValueOpcode::ImageAtomicSwap32:
@ -391,7 +492,8 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
case IR::ValueOpcode::ImageAtomicUMax32:
case IR::ValueOpcode::ImageAtomicUMax64:
case IR::ValueOpcode::BufferAtomicUMax32:
case IR::ValueOpcode::SharedAtomicUMax32: return spv::OpAtomicUMax;
case IR::ValueOpcode::SharedAtomicUMax32:
case IR::ValueOpcode::SharedAtomicUMax64: return spv::OpAtomicUMax;
case IR::ValueOpcode::ImageAtomicAnd32:
case IR::ValueOpcode::ImageAtomicAnd64:
case IR::ValueOpcode::BufferAtomicAnd32:

View File

@ -351,6 +351,9 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
if (a16) {
inst.image_sample_flags |= ImageSampleFlagA16;
}
if ((sample != nullptr || gather != nullptr) && (word0 & (1u << 12u)) != 0u) {
inst.image_sample_flags |= ImageSampleFlagUnnormalized;
}
inst.image_dimension = dimension;
inst.image_r128 = r128;
inst.image_nsa_dwords = nsa_dwords;

View File

@ -116,7 +116,8 @@ constexpr MemoryOpcodeInfo DS_OPCODE_LIST[] = {
{0x3fu, Opcode::DS_ORDERED_COUNT, 1, 32},
{0x40u, Opcode::DS_ADD_U64, 2, 32}, {0x4au, Opcode::DS_OR_B64, 2, 32},
{0x4du, Opcode::DS_WRITE_B64, 2, 32}, {0x4eu, Opcode::DS_WRITE2_B64, 4, 32},
{0x4fu, Opcode::DS_WRITE2ST64_B64, 4, 32}, {0x76u, Opcode::DS_READ_B64, 2, 32},
{0x4fu, Opcode::DS_WRITE2ST64_B64, 4, 32}, {0x68u, Opcode::DS_MAX_RTN_U64, 2, 32},
{0x70u, Opcode::DS_CMPST_RTN_B64, 2, 32}, {0x76u, Opcode::DS_READ_B64, 2, 32},
{0x77u, Opcode::DS_READ2_B64, 4, 32}, {0x78u, Opcode::DS_READ2ST64_B64, 4, 32},
{0xa0u, Opcode::DS_WRITE_B8_D16_HI, 1, 8},
{0xa1u, Opcode::DS_WRITE_B16_D16_HI, 1, 16},
@ -192,6 +193,8 @@ bool IsDsAtomicOpcode(Opcode opcode) {
case Opcode::DS_MIN_RTN_U32:
case Opcode::DS_MAX_U32:
case Opcode::DS_MAX_RTN_U32:
case Opcode::DS_MAX_RTN_U64:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_AND_B32:
case Opcode::DS_AND_RTN_B32:
case Opcode::DS_OR_B32:
@ -211,6 +214,7 @@ uint32_t DsSourceCount(Opcode opcode) {
case Opcode::DS_WRITE2ST64_B32:
case Opcode::DS_WRITE2_B64:
case Opcode::DS_WRITE2ST64_B64:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_MSKOR_B32: return 3u;
case Opcode::DS_MIN_F32:
case Opcode::DS_MAX_F32:

View File

@ -708,6 +708,8 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::DS_MIN_RTN_U32:
case Opcode::DS_MAX_U32:
case Opcode::DS_MAX_RTN_U32:
case Opcode::DS_MAX_RTN_U64:
case Opcode::DS_CMPST_RTN_B64:
case Opcode::DS_AND_B32:
case Opcode::DS_AND_RTN_B32:
case Opcode::DS_OR_B32:

View File

@ -624,6 +624,8 @@ enum class Opcode {
DS_MIN_RTN_U32,
DS_MAX_U32,
DS_MAX_RTN_U32,
DS_MAX_RTN_U64,
DS_CMPST_RTN_B64,
DS_AND_B32,
DS_AND_RTN_B32,
DS_OR_B32,
@ -762,6 +764,7 @@ enum ImageSampleFlag : uint32_t {
ImageSampleFlagCd = 1u << 8u,
ImageSampleFlagGatherHorizontal = 1u << 9u,
ImageSampleFlagAdjust = 1u << 10u,
ImageSampleFlagUnnormalized = 1u << 11u,
};
enum class ImageDimension : uint32_t {

View File

@ -578,11 +578,13 @@ void Translator::DS_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode opc
bool returns_value) {
const auto memory = MemoryInfoFromDecoded(inst);
const auto address = ReadU32(inst.src0);
const IR::Value value =
memory.data_dwords == 2u ? IR::Value(ReadU64(inst.src1)) : IR::Value(ReadU32(inst.src1));
const auto read = [&](const Decoder::Operand& operand) -> IR::Value {
return memory.data_dwords == 2u ? IR::Value(ReadU64(operand)) : IR::Value(ReadU32(operand));
};
const auto value = read(inst.src1);
const auto flags = AddMemoryInfo(memory, inst.pc);
const auto result = inst.src_count == 3u
? ir.Emit(opcode, {address, value, ReadU32(inst.src2), ir.GetExec()}, flags)
? ir.Emit(opcode, {address, value, read(inst.src2), ir.GetExec()}, flags)
: ir.Emit(opcode, {address, value, ir.GetExec()}, flags);
if (returns_value) {
WriteOperand(inst.dst, result);
@ -687,7 +689,8 @@ void Translator::IMAGE_SAMPLE(const Decoder::Instruction& inst) {
const auto resource = GetImageResource(memory);
const auto sampler = GetSamplerResource(memory);
const auto address = MakeImageAddress(inst, MemorySourceAt(inst, 0));
const auto result = ir.Emit(IR::ValueOpcode::ImageSampleRaw, {resource, sampler, address},
const auto result = ir.Emit(IR::ValueOpcode::ImageSampleRaw,
{resource, sampler, address, ir.GetExec()},
AddMemoryInfo(memory, inst.pc));
const bool dref = (memory.image_sample_flags & Decoder::ImageSampleFlagCompare) != 0u;
if (dref && memory.data_bits != 16u) {
@ -714,7 +717,7 @@ void Translator::IMAGE_GATHER(const Decoder::Instruction& inst) {
has_lod ? resource.Instruction()->Arg(1) : IR::Value(0u),
has_lod ? resource.Instruction()->Arg(3) : IR::Value(0u),
has_lod ? sampler.Instruction()->Arg(1) : IR::Value(0u),
has_lod ? sampler.Instruction()->Arg(2) : IR::Value(0u)},
has_lod ? sampler.Instruction()->Arg(2) : IR::Value(0u), ir.GetExec()},
AddMemoryInfo(memory, inst.pc));
for (uint32_t index = 0; index < memory.data_dwords; index++) {
WriteOperand(OffsetOperand(inst.dst, index),
@ -1072,6 +1075,10 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax32, false);
case Decoder::Opcode::DS_MAX_RTN_U32:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax32, true);
case Decoder::Opcode::DS_MAX_RTN_U64:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicUMax64, true);
case Decoder::Opcode::DS_CMPST_RTN_B64:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicCmpSwap64, true);
case Decoder::Opcode::DS_AND_B32:
return DS_ATOMIC(inst, IR::ValueOpcode::SharedAtomicAnd32, false);
case Decoder::Opcode::DS_AND_RTN_B32:

View File

@ -948,9 +948,14 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
switch (inst.opcode) {
case Decoder::Opcode::DS_ADD_U64:
case Decoder::Opcode::DS_OR_B64:
case Decoder::Opcode::DS_MAX_RTN_U64:
case Decoder::Opcode::DS_WRITE_B64:
case Decoder::Opcode::DS_WRITE_B96:
case Decoder::Opcode::DS_WRITE_B128: include_vector(inst.src1, inst.data_dwords); break;
case Decoder::Opcode::DS_CMPST_RTN_B64:
include_vector(inst.src1, 2u);
include_vector(inst.src2, 2u);
break;
case Decoder::Opcode::DS_WRITE2_B32:
case Decoder::Opcode::DS_WRITE2ST64_B32:
case Decoder::Opcode::DS_WRITE2_B64:
@ -1163,9 +1168,11 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
}
entry_ir.SetExec(initial_exec);
const auto initial_mask = entry_ir.Emit(IR::ValueOpcode::Ballot, {initial_exec});
entry_ir.SetExecLo(entry_ir.CompositeExtract(initial_mask, 0));
entry_ir.SetExecHi(options.wave_size == 64u ? entry_ir.CompositeExtract(initial_mask, 1)
: IR::U32(IR::Value(0u)));
const auto initial_lo = entry_ir.CompositeExtract(initial_mask, 0);
const auto initial_hi = options.wave_size == 64u ? entry_ir.CompositeExtract(initial_mask, 1)
: IR::U32(IR::Value(0u));
entry_ir.SetExecLo(initial_lo);
entry_ir.SetExecHi(initial_hi);
if (options.stage == ShaderType::Compute) {
const auto* cs = options.input_info.compute;
const auto thread_ids =
@ -1381,11 +1388,16 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
}
} else if (options.stage == ShaderType::Vertex) {
// Vulkan owns primitive assembly; each vertex subgroup is one NGG wave.
// Keep its full lane extent: mbcnt(-1) uses lane ordinals, not active counts.
// ESVertCount describes input vertices, independently of lane ordinals.
auto vertex_count = IR::U32(entry_ir.Emit(IR::ValueOpcode::BitCount32, {initial_lo}));
if (options.wave_size == 64u) {
vertex_count = entry_ir.IAdd(vertex_count, IR::U32(entry_ir.Emit(
IR::ValueOpcode::BitCount32, {initial_hi})));
}
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(2),
IR::U32(IR::Value(options.wave_size << 12u)));
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3),
IR::U32(IR::Value((1u << 28u) | options.wave_size)));
entry_ir.BitwiseOr(vertex_count, IR::U32(IR::Value(1u << 28u))));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
builtin(IR::StageInputKind::VertexIndex));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8),

View File

@ -3,6 +3,7 @@
#include "graphics/shader/recompiler/BufferFormat.h"
#include <fmt/format.h>
#include <xbyak/xbyak.h>
#include <map>
#include <new>
#include <unordered_map>
@ -99,6 +100,9 @@ bool EquivalentValue(const ResourcePlan& program, Value left, Value right,
} // namespace
ResourcePlan::ResourcePlan() = default;
ResourcePlan::ResourcePlan(ResourcePlan&&) noexcept = default;
ResourcePlan::~ResourcePlan() {
for (auto& inst: value_storage) {
inst.Invalidate();

View File

@ -19,10 +19,20 @@
#include <memory>
#include <optional>
#include <string_view>
#include <unordered_map>
#include <vector>
namespace Xbyak {
class CodeGenerator;
}
namespace Libs::Graphics::ShaderRecompiler::IR {
class SrtWalker;
// Finite descriptor domains also bound the arithmetic used to prove their indices.
constexpr uint32_t MaxIndirectDescriptorProbes = 65536u;
enum class ResourceKind {
None,
ScalarBuffer,
@ -497,9 +507,23 @@ struct ShaderInfo {
struct DescriptorSource {
struct IndirectDescriptor {
struct SelectorRead {
struct CountedRows {
uint32_t source = UINT32_MAX;
uint32_t stride = 0; // Byte spacing between selector rows, not their length.
bool operator==(const CountedRows& other) const = default;
};
uint32_t source = UINT32_MAX;
uint32_t stride = 0;
uint32_t offset = 0;
uint32_t maximum = UINT32_MAX;
uint32_t bit_offset = 0;
uint32_t bit_mask = UINT32_MAX;
bool global = false;
// Four DWORD bitmap words per record; set-bit ranks subtract from the last key.
bool bitset = false;
std::optional<CountedRows> rows;
std::vector<std::pair<uint32_t, uint32_t>> byte_ranges;
bool operator==(const SelectorRead& other) const = default;
};
@ -510,8 +534,11 @@ struct DescriptorSource {
uint32_t table_stride = 0;
uint32_t table_record_bytes = 0;
bool table_scalar = false;
bool indexed_store = false;
bool signed_count = false;
uint32_t workgroup_axis = UINT32_MAX;
Value key_count;
// First selector index, or first descriptor key for a direct or bitmap range.
Value selector_first;
Value selector_mask;
std::vector<uint32_t> sources;
@ -571,14 +598,17 @@ struct ResourcePlan {
std::vector<Entry> values;
uint64_t generation = 0;
const SrtWalker* session = nullptr;
Value active_mask;
uint8_t read_policy = 0;
};
ResourcePlan() = default;
ResourcePlan();
~ResourcePlan();
ResourcePlan(const ResourcePlan&) = delete;
ResourcePlan& operator=(const ResourcePlan&) = delete;
ResourcePlan(ResourcePlan&&) noexcept = default;
ResourcePlan(ResourcePlan&&) noexcept;
ResourcePlan& operator=(ResourcePlan&& other) noexcept;
ShaderType stage = ShaderType::Unknown;
@ -589,6 +619,7 @@ struct ResourcePlan {
std::vector<MemoryInfo> memory_info;
std::vector<DescriptorSource> descriptor_sources;
std::vector<ResourceBlock> control_flow;
std::unordered_map<const Inst*, std::vector<uint32_t>> control_phi_predecessors;
std::vector<SrtRead> srt_reads;
bool requires_specialization_memory = false;
std::vector<std::pair<uint64_t, uint64_t>> source_reads;
@ -597,12 +628,13 @@ struct ResourcePlan {
bool resource_tracking_complete = false;
ShaderInfo info;
UniformFillPlan uniform_fill;
std::unique_ptr<Xbyak::CodeGenerator> srt_code;
// GPU-thread scratch for nested clean/EXEC memos, activity and material keys.
mutable std::deque<EvaluationContext> evaluation_contexts;
mutable uint32_t evaluation_value_count = 0;
mutable uint32_t evaluation_depth = 0;
mutable std::vector<uint8_t> active_sources;
mutable std::vector<uint8_t> visited_blocks;
mutable std::vector<uint8_t> reachable_blocks;
mutable std::vector<uint32_t> pending_blocks;
mutable std::vector<uint32_t> material_keys;
};

View File

@ -175,6 +175,8 @@ SharedAccess SharedAccessOf(ValueOpcode opcode) {
case ValueOpcode::SharedAtomicUMin32:
case ValueOpcode::SharedAtomicSMax32:
case ValueOpcode::SharedAtomicUMax32:
case ValueOpcode::SharedAtomicUMax64:
case ValueOpcode::SharedAtomicCmpSwap64:
case ValueOpcode::SharedAtomicAnd32:
case ValueOpcode::SharedAtomicOr32:
case ValueOpcode::SharedAtomicOr64:
@ -190,6 +192,8 @@ uint32_t SharedComponentCount(ValueOpcode opcode) {
switch (opcode) {
case ValueOpcode::SharedAtomicIAdd64:
case ValueOpcode::SharedAtomicOr64:
case ValueOpcode::SharedAtomicUMax64:
case ValueOpcode::SharedAtomicCmpSwap64:
case ValueOpcode::LoadSharedU32x2:
case ValueOpcode::WriteSharedU32x2: return 2u;
case ValueOpcode::LoadSharedU32x3:

View File

@ -303,6 +303,8 @@ VALUE_OPCODE(SharedAtomicSMin32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicUMin32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicSMax32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicUMax32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicUMax64, U64, U32, U64, U1)
VALUE_OPCODE(SharedAtomicCmpSwap64, U64, U32, U64, U64, U1)
VALUE_OPCODE(SharedAtomicAnd32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicOr64, Void, U32, U64, U1)
@ -317,8 +319,8 @@ VALUE_OPCODE(BvhIntersect, U32x4, U32x4, ImageAddress, U1)
VALUE_OPCODE(ImageQueryLod, U32x4, ImageResource, SamplerResource, ImageAddress)
VALUE_OPCODE(ImageRead, U32x4, ImageResource, ImageAddress, U1)
VALUE_OPCODE(ImageWrite, Void, ImageResource, ImageAddress, U32x4, U1)
VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress)
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U32, U32, U32, U32)
VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U1)
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U32, U32, U32, U32, U1)
VALUE_OPCODE(ImageAtomicCompareSwap32, U32, ImageResource, ImageAddress, U32, U32, U1)
VALUE_OPCODE(ImageAtomicSwap32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicSwap64, U64, ImageResource, ImageAddress, U64, U1)

View File

@ -22,6 +22,66 @@ void Replace(Inst& inst, Value value) {
inst.ReplaceUsesWith(value.Resolve());
}
bool HasOneUse(const Inst& inst) {
// Replacement aliases preserve external Values but emit no additional operation.
return std::ranges::count_if(inst.Uses(), [](const Use& use) {
return use.user->GetOpcode() != ValueOpcode::Identity;
}) == 1;
}
class FoldWorklist {
public:
struct Location {
Block::iterator instruction;
uint64_t visited = 0;
bool queued = false;
};
void Register(Block::iterator instruction) {
locations.emplace(&*instruction, Location {instruction});
Queue(&*instruction);
}
void Queue(Inst* inst) {
const auto found = locations.find(inst);
if (found != locations.end() && !found->second.queued) {
found->second.queued = true;
pending.push_back(inst);
}
}
void QueueUsers(const std::vector<Use>& users) {
++generation;
affected.clear();
for (const auto& use: users) affected.push_back(use.user);
// Pattern folds inspect nested operands even when intermediate operations
// stay dynamic. Visit consumers once per change, including cyclic Phi edges.
for (size_t i = 0; i < affected.size(); ++i) {
auto* inst = affected[i];
const auto found = locations.find(inst);
if (found == locations.end() || found->second.visited == generation) continue;
found->second.visited = generation;
Queue(inst);
for (const auto& use: inst->Uses()) affected.push_back(use.user);
}
}
Block::iterator Prepend(Block& block, Block::iterator before, ValueOpcode opcode,
std::initializer_list<Value> args = {}) {
const auto result = block.PrependNewInst(before, opcode, args);
Register(result);
return result;
}
std::unordered_map<Inst*, Location> locations;
std::vector<Inst*> pending;
std::unordered_set<Inst*> lowered_ancillary;
private:
std::vector<Inst*> affected;
uint64_t generation = 0;
};
template <typename Function>
bool FoldU32(Inst& inst, Function function) {
const auto lhs = Arg(inst, 0);
@ -195,8 +255,42 @@ bool FoldCompositeExtract(Inst& inst, ValueOpcode construct, size_t components)
return false;
}
void FoldZeroComparison(Block& block, Block::iterator instruction, FoldWorklist& worklist) {
auto& inst = *instruction;
Value value;
for (uint32_t arg = 0; arg < 2u; ++arg)
if (Arg(inst, arg) == Value(0u)) value = Arg(inst, arg ^ 1u);
const auto* source = value.TryInstruction();
if (source == nullptr) return;
const bool equal = inst.GetOpcode() == ValueOpcode::IEqual32;
const auto boolean_select = [](const Inst* select) {
return select != nullptr && select->GetOpcode() == ValueOpcode::SelectU32 &&
IsImmediate(Arg(*select, 1), Type::U32) &&
IsImmediate(Arg(*select, 2), Type::U32);
};
if (boolean_select(source)) {
const bool when_true = (Arg(*source, 1).U32() == 0u) == equal;
const bool when_false = (Arg(*source, 2).U32() == 0u) == equal;
if (when_true == when_false) Replace(inst, Value(when_true));
else if (when_true) Replace(inst, Arg(*source, 0));
else Replace(inst, Value(&*worklist.Prepend(block, instruction, ValueOpcode::LogicalNot,
{Arg(*source, 0)})));
} else if (source->GetOpcode() == ValueOpcode::BitwiseOr32 &&
(boolean_select(Arg(*source, 0).TryInstruction()) ||
boolean_select(Arg(*source, 1).TryInstruction()))) {
// SCC encoded as a DWORD needs no select or bitwise operation when only
// its zero predicate is consumed. Keep arbitrary DWORDs as zero comparisons.
const auto lhs = worklist.Prepend(block, instruction, inst.GetOpcode(),
{Arg(*source, 0), Value(0u)});
const auto rhs = worklist.Prepend(block, instruction, inst.GetOpcode(),
{Arg(*source, 1), Value(0u)});
Replace(inst, Value(&*worklist.Prepend(block, instruction,
equal ? ValueOpcode::LogicalAnd : ValueOpcode::LogicalOr, {Value(&*lhs), Value(&*rhs)})));
}
}
void FoldInstruction(Block& block, Block::iterator instruction,
std::unordered_set<Inst*>& lowered_ancillary) {
FoldWorklist& worklist) {
auto& inst = *instruction;
switch (inst.GetOpcode()) {
case ValueOpcode::StoreBufferU32: {
@ -208,7 +302,7 @@ void FoldInstruction(Block& block, Block::iterator instruction,
case ValueOpcode::SelectU1:
if (!FoldSelect(inst) && IsImmediate(Arg(inst, 2), Type::U1) &&
!Arg(inst, 2).U1()) {
auto result = block.PrependNewInst(instruction, ValueOpcode::LogicalAnd,
auto result = worklist.Prepend(block, instruction, ValueOpcode::LogicalAnd,
{Arg(inst, 0), Arg(inst, 1)});
Replace(inst, Value(&*result));
}
@ -224,8 +318,8 @@ void FoldInstruction(Block& block, Block::iterator instruction,
case 0u: Replace(inst, Value(false)); break;
case 0x3ffu: Replace(inst, Value(true)); break;
case 3u: {
const auto result = block.PrependNewInst(
instruction, ValueOpcode::FPIsNan32, {Arg(inst, 0)});
const auto result = worklist.Prepend(
block, instruction, ValueOpcode::FPIsNan32, {Arg(inst, 0)});
Replace(inst, Value(&*result));
break;
}
@ -280,12 +374,12 @@ void FoldInstruction(Block& block, Block::iterator instruction,
if (offset.U32() >= field.start && offset.U32() < field.end &&
count.U32() <= field.end - offset.U32()) {
// Preserve extraction and sign extension while exposing only the used field.
const auto input = block.PrependNewInst(
instruction, ValueOpcode::GetBuiltin,
const auto input = worklist.Prepend(
block, instruction, ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(field.kind)), Value(0u)});
inst.SetArg(0, Value(&*input));
inst.SetArg(1, Value(offset.U32() - field.start));
lowered_ancillary.insert(source);
worklist.lowered_ancillary.insert(source);
return;
}
}
@ -510,8 +604,20 @@ void FoldInstruction(Block& block, Block::iterator instruction,
});
return;
case ValueOpcode::BitwiseAnd32:
if (!FoldU32(inst, [](uint32_t a, uint32_t b) { return a & b; })) {
ReplaceBinaryIdentity(inst, Type::U32, 0xffffffffu);
if (FoldU32(inst, [](uint32_t a, uint32_t b) { return a & b; }) ||
ReplaceBinaryIdentity(inst, Type::U32, UINT32_MAX)) return;
for (uint32_t arm = 0; arm < 2u; ++arm) {
const auto mask = Arg(inst, arm);
const auto* shifted = Arg(inst, arm ^ 1u).TryInstruction();
if (!IsImmediate(mask, Type::U32) || shifted == nullptr ||
shifted->GetOpcode() != ValueOpcode::ShiftLeftLogical32 || !HasOneUse(*shifted)) continue;
const auto shift = Arg(*shifted, 1);
if (!IsImmediate(shift, Type::U32) || shift.U32() >= 32u) continue;
const auto masked = worklist.Prepend(block, instruction, ValueOpcode::BitwiseAnd32,
{Arg(*shifted, 0), Value(mask.U32() >> shift.U32())});
Replace(inst, Value(&*worklist.Prepend(block, instruction, ValueOpcode::ShiftLeftLogical32,
{Value(&*masked), shift})));
return;
}
return;
case ValueOpcode::BitwiseAnd64:
@ -577,10 +683,12 @@ void FoldInstruction(Block& block, Block::iterator instruction,
FoldU32(inst, [](uint32_t a, uint32_t b) { return std::max(a, b); });
return;
case ValueOpcode::IEqual32:
FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a == b; });
if (!FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a == b; }))
FoldZeroComparison(block, instruction, worklist);
return;
case ValueOpcode::INotEqual32:
FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a != b; });
if (!FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a != b; }))
FoldZeroComparison(block, instruction, worklist);
return;
case ValueOpcode::ULessThan32:
FoldU32Compare(inst, [](uint32_t a, uint32_t b) { return a < b; });
@ -642,19 +750,43 @@ void FoldInstruction(Block& block, Block::iterator instruction,
const auto lhs = Arg(inst, 0);
const auto rhs = Arg(inst, 1);
const auto simplify = [&](Value assumption, Value expression) {
const auto* disjunction = expression.TryInstruction();
if (disjunction == nullptr || disjunction->GetOpcode() != ValueOpcode::LogicalOr) {
return false;
const auto* test = expression.TryInstruction();
if (test == nullptr) return false;
const auto replace = [&](Value value) {
inst.SetArg(assumption == lhs ? 1u : 0u, value);
return true;
};
const auto negate = [&](Value value) {
return Value(&*worklist.Prepend(block, instruction, ValueOpcode::LogicalNot, {value}));
};
const bool inverse = test->GetOpcode() == ValueOpcode::LogicalNot;
if (inverse) {
test = Arg(*test, 0).TryInstruction();
if (test == nullptr) return false;
if (test->GetOpcode() == ValueOpcode::LogicalAnd && HasOneUse(*test))
for (uint32_t arg = 0; arg < 2u; ++arg)
if (Arg(*test, arg) == assumption)
return replace(negate(Arg(*test, arg ^ 1u)));
}
for (uint32_t i = 0; i < 2u; ++i) {
const auto* inverse = disjunction->Arg(i).Resolve().TryInstruction();
if (inverse != nullptr && inverse->GetOpcode() == ValueOpcode::LogicalNot &&
inverse->Arg(0).Resolve() == assumption) {
inst.SetArg(assumption == lhs ? 1u : 0u,
disjunction->Arg(i ^ 1u));
return true;
if (test->GetOpcode() == ValueOpcode::IEqual32 ||
test->GetOpcode() == ValueOpcode::INotEqual32) {
for (uint32_t arg = 0; arg < 2u; ++arg) {
if (Arg(*test, arg) != Value(0u)) continue;
const auto original = Arg(*test, arg ^ 1u);
const auto active = ResolveActiveU32(original, assumption);
if (!active.IsEmpty() && active != original) {
const auto comparison = Value(&*worklist.Prepend(block, instruction,
test->GetOpcode(), {active, Value(0u)}));
return replace(inverse ? negate(comparison) : comparison);
}
}
}
if (inverse || test->GetOpcode() != ValueOpcode::LogicalOr) return false;
for (uint32_t arg = 0; arg < 2u; ++arg) {
const auto* negation = Arg(*test, arg).TryInstruction();
if (negation != nullptr && negation->GetOpcode() == ValueOpcode::LogicalNot &&
Arg(*negation, 0) == assumption) return replace(Arg(*test, arg ^ 1u));
}
return false;
};
if (simplify(lhs, rhs) || simplify(rhs, lhs)) return;
@ -673,6 +805,25 @@ void FoldInstruction(Block& block, Block::iterator instruction,
Replace(inst, lhs.U1() ? lhs : rhs);
} else if (IsImmediate(rhs, Type::U1)) {
Replace(inst, rhs.U1() ? rhs : lhs);
} else {
const auto* left = lhs.TryInstruction();
const auto* right = rhs.TryInstruction();
if (left == nullptr || right == nullptr ||
left->GetOpcode() != ValueOpcode::LogicalAnd ||
right->GetOpcode() != ValueOpcode::LogicalAnd ||
(!HasOneUse(*left) && !HasOneUse(*right))) return;
// Factor a shared execution predicate without retaining more operations.
for (uint32_t a = 0; a < 2u; ++a) {
for (uint32_t b = 0; b < 2u; ++b) {
const auto common = Arg(*left, a);
if (common != Arg(*right, b)) continue;
const auto either = Value(&*worklist.Prepend(block, instruction,
ValueOpcode::LogicalOr, {Arg(*left, a ^ 1u), Arg(*right, b ^ 1u)}));
Replace(inst, Value(&*worklist.Prepend(block, instruction,
ValueOpcode::LogicalAnd, {common, either})));
return;
}
}
}
}
return;
@ -706,14 +857,13 @@ void FoldInstruction(Block& block, Block::iterator instruction,
}
}
// In wave32, extracting this invocation's bit from a ballot recovers its predicate.
// Keep that identity through scalar EXEC operations and their loop-carried mask Phis.
// Full and empty masks have a constant predicate in either wave size. In wave32,
// ballot projection also survives scalar EXEC operations and loop-carried mask Phis.
class LaneMaskProjection {
public:
explicit LaneMaskProjection(std::unordered_set<Inst*>& lowered_ancillary)
: m_lowered_ancillary(lowered_ancillary) {}
explicit LaneMaskProjection(FoldWorklist& worklist): m_worklist(worklist) {}
void Fold(Inst& inst) {
void Fold(Inst& inst, uint32_t wave_size) {
if (inst.GetOpcode() != ValueOpcode::INotEqual32 || !Immediate(Arg(inst, 1), 0u)) return;
const auto* bit = Arg(inst, 0).TryInstruction();
if (bit == nullptr || bit->GetOpcode() != ValueOpcode::BitwiseAnd32 ||
@ -725,9 +875,15 @@ public:
!Immediate(Arg(*index, 1), 31u)) return;
const auto* lane = Arg(*index, 0).TryInstruction();
if (lane == nullptr || lane->GetOpcode() != ValueOpcode::LaneId) return;
const auto mask = Arg(*shift, 0);
if (Immediate(mask, 0u) || Immediate(mask, UINT32_MAX)) {
Replace(inst, Value(mask.U32() != 0u));
return;
}
if (wave_size != 32u) return;
m_visited.clear();
m_grounded = false;
if (CanProject(Arg(*shift, 0)) && m_grounded) Replace(inst, Project(Arg(*shift, 0)));
if (CanProject(mask) && m_grounded) Replace(inst, Project(mask));
}
private:
@ -772,19 +928,18 @@ private:
if (const auto found = m_values.find(source); found != m_values.end()) return found->second.Resolve();
if (const auto predicate = BallotPredicate(*source); !predicate.IsEmpty()) return predicate;
auto* block = source->Parent();
const auto where = std::find_if(block->begin(), block->end(),
[source](const Inst& candidate) { return &candidate == source; });
const auto where = m_worklist.locations.at(source).instruction;
const auto op = source->GetOpcode() == ValueOpcode::BitwiseAnd32 ? ValueOpcode::LogicalAnd
: source->GetOpcode() == ValueOpcode::BitwiseOr32 ? ValueOpcode::LogicalOr
: source->GetOpcode() == ValueOpcode::BitwiseNot32 ? ValueOpcode::LogicalNot
: source->GetOpcode() == ValueOpcode::SelectU32 ? ValueOpcode::SelectU1
: ValueOpcode::Phi;
auto result = op == ValueOpcode::Phi ? block->PrependNewInst(where, op)
auto result = op == ValueOpcode::Phi ? m_worklist.Prepend(*block, where, op)
: op == ValueOpcode::LogicalNot
? block->PrependNewInst(where, op, {Value(false)})
? m_worklist.Prepend(*block, where, op, {Value(false)})
: op == ValueOpcode::SelectU1
? block->PrependNewInst(where, op, {Arg(*source, 0), Value(false), Value(false)})
: block->PrependNewInst(where, op, {Value(false), Value(false)});
? m_worklist.Prepend(*block, where, op, {Arg(*source, 0), Value(false), Value(false)})
: m_worklist.Prepend(*block, where, op, {Value(false), Value(false)});
m_values.emplace(source, Value(&*result));
if (op == ValueOpcode::Phi) result->SetFlags(Type::U1);
for (size_t arg = op == ValueOpcode::SelectU1 ? 1u : 0u; arg < source->NumArgs(); ++arg) {
@ -792,11 +947,11 @@ private:
if (op == ValueOpcode::Phi) result->AddPhiOperand(source->PhiBlock(arg), projected);
else result->SetArg(arg, projected);
}
FoldInstruction(*block, result, m_lowered_ancillary);
FoldInstruction(*block, result, m_worklist);
return Value(&*result).Resolve();
}
std::unordered_set<Inst*>& m_lowered_ancillary;
FoldWorklist& m_worklist;
std::unordered_set<const Inst*> m_visited;
std::unordered_map<const Inst*, Value> m_values;
bool m_grounded = false;
@ -805,29 +960,59 @@ private:
} // namespace
void ConstantPropagationPass(const BlockList& blocks, uint32_t wave_size) {
std::unordered_set<Inst*> lowered_ancillary;
LaneMaskProjection mask_projection(lowered_ancillary);
for (auto* block: blocks) {
for (auto inst = block->begin(); inst != block->end(); ++inst) {
if (wave_size == 32u) mask_projection.Fold(*inst);
FoldInstruction(*block, inst, lowered_ancillary);
FoldWorklist worklist;
LaneMaskProjection mask_projection(worklist);
for (auto* block: blocks)
for (auto inst = block->begin(); inst != block->end(); ++inst) worklist.Register(inst);
std::vector<Use> users;
std::vector<Value> operands;
const auto drain = [&] {
for (size_t i = 0; i < worklist.pending.size(); ++i) {
auto* inst = worklist.pending[i];
auto& location = worklist.locations.at(inst);
location.queued = false;
const auto opcode = inst->GetOpcode();
if (opcode == ValueOpcode::Identity || opcode == ValueOpcode::Void) continue;
const auto where = location.instruction;
users.assign(inst->Uses().begin(), inst->Uses().end());
operands.clear();
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) operands.push_back(inst->Arg(arg));
mask_projection.Fold(*inst, wave_size);
FoldInstruction(*inst->Parent(), where, worklist);
bool changed = opcode != inst->GetOpcode() || operands.size() != inst->NumArgs();
for (size_t arg = 0; !changed && arg < operands.size(); ++arg)
changed = operands[arg] != inst->Arg(arg);
if (changed) {
worklist.QueueUsers(users);
worklist.Queue(inst);
}
}
}
worklist.pending.clear();
};
drain();
// Normalize retained PHI/select values only after every supported field read has
// been lowered; direct raw consumers remain unsupported.
for (auto* source: lowered_ancillary) {
const bool retained_only = std::ranges::all_of(source->Uses(), [](const Use& use) {
const auto& user = *use.user;
if (!user.HasUses() && !user.MayHaveSideEffects()) {
return true;
bool normalized;
do {
normalized = false;
for (auto* source: worklist.lowered_ancillary) {
if (source->GetOpcode() == ValueOpcode::Identity) continue;
const bool retained_only = std::ranges::all_of(source->Uses(), [](const Use& use) {
const auto& user = *use.user;
if (!user.HasUses() && !user.MayHaveSideEffects()) {
return true;
}
return user.GetOpcode() == ValueOpcode::Phi ||
(user.GetOpcode() == ValueOpcode::SelectU32 && use.operand == 2u);
});
if (retained_only) {
worklist.QueueUsers(source->Uses());
Replace(*source, Value(0u));
normalized = true;
}
return user.GetOpcode() == ValueOpcode::Phi ||
(user.GetOpcode() == ValueOpcode::SelectU32 && use.operand == 2u);
});
if (retained_only) {
Replace(*source, Value(0u));
}
}
drain();
} while (normalized);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR

View File

@ -19,7 +19,6 @@ namespace Libs::Graphics::ShaderRecompiler::IR {
namespace {
constexpr uint64_t AddressMask = 0x0000ffffffffffffull;
constexpr uint64_t MaxIndirectDescriptorProbes = 65536u;
bool SpecializationFail(std::string_view message) {
std::fprintf(stderr, "shader resource specialization failed: %.*s\n",
@ -106,16 +105,46 @@ Prospero::BufferFormat ImageConversionFormat(Prospero::BufferFormat format) {
: Prospero::BufferFormat::kInvalid;
}
enum class SamplerClass : uint8_t { Float, Integer, PointInteger };
bool ValidatePackedUnormSampling(const ResourcePlan& program, const ResourceSnapshot& snapshot,
uint32_t image_index) {
ShaderTextureResource texture;
std::copy_n(snapshot.images[image_index].dwords.begin(), std::size(texture.fields),
texture.fields);
if (texture.Type() != Prospero::ImageType::kColor2D ||
texture.BaseLevel() != texture.LastLevel() || texture.MinLod() != 0u ||
texture.CornerSample()) {
return SpecializationFail("packed UNorm sampling requires a centered, single-mip 2D image");
}
for (const auto& pair: program.info.sampled_pairs) {
if (pair.image != image_index) continue;
if (pair.sampler >= snapshot.samplers.size() ||
!program.info.samplers[pair.sampler].indirect_resources.empty()) {
return SpecializationFail("packed UNorm sampling requires a direct sampler");
}
ShaderSamplerResource sampler;
std::copy_n(snapshot.samplers[pair.sampler].dwords.begin(), std::size(sampler.fields),
sampler.fields);
const auto linear = static_cast<uint32_t>(Prospero::SamplerFilter::kBilinear);
const auto clamp = static_cast<uint32_t>(Prospero::SamplerClampMode::kClampLastTexel);
if (sampler.XyMagFilter() != linear || sampler.XyMinFilter() != linear ||
sampler.ClampX() != clamp || sampler.ClampY() != clamp ||
sampler.ForceUnormCoords() || sampler.MaxAnisoRatio() != 0u ||
sampler.FilterMode() != 0u || sampler.MinLod() != 0u || sampler.ForceSrgb() ||
sampler.TruncCoord() || (sampler.fields[0] & (1u << 19u)) != 0u) { // MC_COORD_TRUNC
return SpecializationFail(fmt::format(
"packed UNorm image {} requires normalized bilinear clamp sampling (sampler {})",
image_index, pair.sampler));
}
}
return true;
}
enum class SamplerClass : uint8_t { Float, Integer };
SamplerClass ClassifySampler(const ImageResource& image) {
if (image.numeric_class == Prospero::TextureNumericClass::Sint ||
(image.numeric_class == Prospero::TextureNumericClass::Uint &&
image.conversion_format != Prospero::BufferFormat::kInvalid)) {
return SamplerClass::PointInteger;
}
return image.numeric_class == Prospero::TextureNumericClass::Uint ? SamplerClass::Integer
: SamplerClass::Float;
return image.numeric_class == Prospero::TextureNumericClass::Uint ||
image.numeric_class == Prospero::TextureNumericClass::Sint
? SamplerClass::Integer : SamplerClass::Float;
}
bool DescriptorIsCube(const DescriptorValue& descriptor) {
@ -176,16 +205,14 @@ void MarkCleanReads(const std::unordered_set<const Inst*>& planned_reads,
}
}
bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t dynamic_offset,
const SrtRuntime& runtime, std::span<uint32_t> words) {
const auto offset = dynamic_offset & ~uint64_t {3};
bool ReadTable(uint64_t base, uint64_t size, uint64_t offset,
const SrtRuntime& runtime, std::span<uint32_t> words) {
const auto count = std::min<uint64_t>(words.size(), offset < size ? (size - offset) / 4u : 0u);
std::ranges::fill(words.subspan(count), 0u);
if (count == 0u) {
return true;
}
base &= AddressMask & ~uint64_t {3};
if (offset > AddressMask - base) {
if (base > AddressMask || offset > AddressMask - base) {
return false;
}
const auto address = base + offset;
@ -195,15 +222,24 @@ bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t dynamic_offset,
runtime.read_specialization_memory(runtime.userdata, address, prefix);
}
bool NormalizeIndirectStoreBuffer(DescriptorValue& value) {
bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t offset,
const SrtRuntime& runtime, std::span<uint32_t> words) {
return ReadTable(base & AddressMask & ~uint64_t {3}, size, offset & ~uint64_t {3}, runtime, words);
}
bool NormalizeIndirectStoreBuffer(DescriptorValue& value, bool indexed, bool formatted) {
ShaderBufferResource descriptor;
if (!DecodeBufferDescriptor(value, descriptor)) return false;
if (descriptor.Type() != 0u || descriptor.Format() == Prospero::BufferFormat::kInvalid ||
descriptor.Base48() == 0u || descriptor.GetSize() == 0u) {
value.dwords.fill(0u);
} else if (descriptor.Stride() != 1u || descriptor.SwizzleEnabled() || descriptor.AddTid() ||
descriptor.OutOfBounds() != 3u) {
return SpecializationFail("indirect buffer stores require linear byte bounds");
} else if (descriptor.Stride() != (indexed ? 4u : 1u) || descriptor.SwizzleEnabled() ||
descriptor.AddTid() || descriptor.OutOfBounds() != (indexed ? 0u : 3u)) {
return SpecializationFail("indirect buffer stores require exact linear DWORD or byte bounds");
} else if (formatted && (descriptor.Format() != Prospero::BufferFormat::k32UInt ||
descriptor.DstSelXYZW() != DstSel(4, 0, 0, 0) ||
(descriptor.Base48() & 3u) != 0u)) {
return SpecializationFail("indirect formatted X stores require an aligned 32-bit UInt identity mapping");
}
return true;
}
@ -230,6 +266,11 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
auto& keys = program.material_keys;
const auto children_begin = descriptors.size();
const auto root_resource = specializations[resource_index];
const auto evaluate_count = [&](uint32_t& count) {
if (!clean.Evaluate(indirect.key_count, count)) return false;
if (indirect.signed_count && std::bit_cast<int32_t>(count) < 0) count = 0;
return true;
};
const auto intern_candidate = [&](const DescriptorValue& candidate, uint32_t& ordinal) {
ordinal = 0u;
if (candidate == descriptors[resource_index]) return true;
@ -244,14 +285,19 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
}
return true;
};
const auto read_keys = [&](const ShaderBufferResource& material, uint64_t first,
const auto read_selector = [&](uint64_t base, uint64_t size, uint64_t offset,
std::span<uint32_t> words) {
return selector->global ? ReadTable(base, size, offset, runtime, words)
: ReadScalarTable(base, size, offset, runtime, words);
};
const auto read_keys = [&](uint64_t base, uint64_t size, uint64_t first,
uint64_t step, uint64_t count) {
keys.resize(count);
if (step == 4u) {
return ReadScalarTable(material.Base48(), material.GetSize(), first, runtime, keys);
return read_selector(base, size, first, keys);
}
for (auto& key: keys) {
if (!ReadScalarTable(material.Base48(), material.GetSize(), first, runtime, {&key, 1}))
if (!read_selector(base, size, first, {&key, 1}))
return false;
first += step;
}
@ -320,29 +366,133 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
}
if (selector == nullptr) {
uint32_t key_count = 0;
uint32_t first_key = 0;
if (indirect.workgroup_axis != UINT32_MAX) {
if (indirect.workgroup_axis >= runtime.workgroup_counts.size()) return false;
key_count = runtime.workgroup_counts[indirect.workgroup_axis];
if (key_count == 0u) return false;
} else {
if (table_value.dword_count != 2u || !clean.Evaluate(indirect.key_count, key_count))
if (table_value.dword_count != 2u || !evaluate_count(key_count))
return false;
if (std::bit_cast<int32_t>(key_count) <= 0) key_count = 0;
}
if (!indirect.selector_first.IsEmpty() && !clean.Evaluate(indirect.selector_first, first_key))
return false;
if (indirect.table_stride == 0u || key_count > MaxIndirectDescriptorProbes ||
(key_count != 0u && uint64_t {indirect.table_offset} + indirect.table_immediate +
(indirect.selector_first.IsEmpty() && key_count != 0u &&
uint64_t {indirect.table_offset} + indirect.table_immediate +
uint64_t {key_count - 1u} * indirect.table_stride + descriptor_bytes >
UINT32_MAX + 1ull)) {
return false;
}
keys.resize(key_count);
std::iota(keys.begin(), keys.end(), 0u);
} else if (!indirect.selector_first.IsEmpty()) {
std::iota(keys.begin(), keys.end(), first_key);
if (!keys.empty() && keys.back() < keys.front())
std::rotate(keys.begin(), keys.begin() + (UINT32_MAX - first_key + 1ull), keys.end());
} else if (selector->rows) {
const auto& rows = *selector->rows;
DescriptorValue count_value;
ShaderBufferResource counts, material;
if (!clean.EvaluateDescriptor(rows.source, count_value) ||
!DecodeBufferDescriptor(count_value, counts) ||
!DecodeBufferDescriptor(material_value, material) || selector->stride != 4u ||
selector->offset != 0u || rows.stride == 0u || (rows.stride & 3u) != 0u ||
counts.GetSize() / 4u > MaxIndirectDescriptorProbes) return false;
std::vector<uint32_t> lengths(counts.GetSize() / 4u);
if (!ReadScalarTable(counts.Base48(), counts.GetSize(), 0u, runtime, lengths)) return false;
uint32_t total = 0;
for (size_t row = 0; row < lengths.size(); ++row) {
const auto length = lengths[row];
if (length > MaxIndirectDescriptorProbes - total ||
(length != 0u && uint64_t {row} * rows.stride + uint64_t {length} * 4u >
UINT32_MAX + 1ull)) return false;
total += length;
}
keys.reserve(total);
std::vector<uint32_t> batch;
for (size_t row = 0; row < lengths.size();) {
if (lengths[row] == 0u) { ++row; continue; }
const auto first = uint64_t {row} * rows.stride / 4u;
auto end = first + lengths[row];
size_t next = row + 1u;
for (; next < lengths.size(); ++next) {
if (lengths[next] == 0u) continue;
const auto candidate_end =
std::max(end, uint64_t {next} * rows.stride / 4u + lengths[next]);
if (candidate_end - first > MaxIndirectDescriptorProbes) break;
end = candidate_end;
}
// Coalesce small gaps, but interpret only selected prefixes. The strict
// reader captures the entire fetched span for writable-alias rejection.
batch.resize(end - first);
if (!ReadScalarTable(material.Base48(), material.GetSize(), first * 4u, runtime, batch))
return false;
for (; row < next; ++row) {
if (lengths[row] == 0u) continue;
const auto begin = batch.begin() + (uint64_t {row} * rows.stride / 4u - first);
keys.insert(keys.end(), begin, begin + lengths[row]);
}
}
} else if (selector->bitset) {
ShaderBufferResource material;
uint32_t first = 0, count = 0;
std::array<uint32_t, 4> masks {};
uint32_t first = 0, count = 0;
if (!DecodeBufferDescriptor(material_value, material) ||
!clean.Evaluate(indirect.selector_first, first) ||
!clean.Evaluate(indirect.key_count, count) || count > MaxIndirectDescriptorProbes)
!evaluate_count(count) || count != masks.size() * 32u ||
selector->stride != sizeof(masks) || selector->offset != 0u ||
material.Type() != 0u || material.Stride() != sizeof(masks) ||
material.SwizzleEnabled() || material.AddTid() || material.OutOfBounds() != 0u ||
(material.Base48() & 3u) != 0u) return false;
if (material.Format() != Prospero::BufferFormat::kInvalid) {
const auto words = material.GetSize() / sizeof(uint32_t);
// Bitmap storage can grow with resolution while its candidate domain stays
// at 128 keys. Bound scratch memory, preserving each coherent read's capture.
for (uint64_t first_word = 0; first_word < words; first_word += keys.size()) {
keys.resize(std::min<uint64_t>(words - first_word, MaxIndirectDescriptorProbes));
if (!ReadScalarTable(material.Base48(), material.GetSize(), first_word * 4u,
runtime, keys)) return false;
for (size_t word = 0; word < keys.size(); ++word) masks[word % masks.size()] |= keys[word];
if (std::ranges::all_of(masks, [](uint32_t mask) { return mask == UINT32_MAX; })) break;
}
keys.clear();
for (uint32_t word = 0; word < masks.size(); ++word) {
auto mask = masks[word];
while (mask != 0u) {
const auto rank = word * 32u + std::countr_zero(mask);
keys.push_back(first + count - 1u - rank);
mask &= mask - 1u;
}
}
}
} else if (!selector->byte_ranges.empty()) {
if (material_value.dword_count != 2u) return false;
const auto base = (uint64_t {material_value.dwords[1]} << 32u) | material_value.dwords[0];
std::array<bool, 256> selected {};
for (const auto [offset, size]: selector->byte_ranges) {
if (size > MaxIndirectDescriptorProbes || ((offset | size) & 3u) != 0u) return false;
keys.resize(size / 4u);
if (!ReadScalarTable(base, UINT64_MAX, offset, runtime, keys)) return false;
for (const auto word: keys)
for (uint32_t shift = 0; shift < 32u; shift += 8u)
selected[(word >> shift) & 255u] = true;
}
keys.clear();
for (uint32_t key = 0; key < selected.size(); ++key)
if (selected[key]) keys.push_back(key);
} else if (!indirect.selector_first.IsEmpty()) {
ShaderBufferResource material;
uint64_t base = 0, size = UINT64_MAX;
uint32_t first = 0, count = 0;
if (material_value.dword_count == 2u) {
if (selector->stride == 0u) return false;
base = (uint64_t {material_value.dwords[1]} << 32u) | material_value.dwords[0];
} else {
if (!DecodeBufferDescriptor(material_value, material)) return false;
base = material.Base48();
size = material.GetSize();
}
if (!clean.Evaluate(indirect.selector_first, first) ||
!evaluate_count(count) || count > MaxIndirectDescriptorProbes)
return false;
// SMEM uses its instruction's byte stride; V# stride only supplies its bounds.
const auto step = selector->stride != 0u ? selector->stride : material.Stride();
@ -352,16 +502,16 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
uint64_t {first} + count > material.NumRecords())) return false;
const auto end = count != 0u
? (uint64_t {first} + count - 1u) * step + selector->offset + 4u : 0u;
if (end > uint64_t {UINT32_MAX} + 1u || end > material.GetSize())
if (end > uint64_t {UINT32_MAX} + 1u || end > size)
return false;
if (!read_keys(material, uint64_t {first} * step + selector->offset, step, count))
if (!read_keys(base, size, uint64_t {first} * step + selector->offset, step, count))
return false;
} else if (!indirect.selector_mask.IsEmpty()) {
uint32_t mask = 0;
uint32_t count = 0;
if (material_value.dword_count != 2u || table_value.dword_count != 2u ||
!clean.Evaluate(indirect.selector_mask, mask) ||
!clean.Evaluate(indirect.key_count, count) || count == 0u || count > 32u) {
!evaluate_count(count) || count == 0u || count > 32u) {
return false;
}
if (count < 32u) mask &= (1u << count) - 1u;
@ -374,13 +524,31 @@ bool MaterializeIndirectDescriptor(const ResourcePlan& p
static_cast<uint64_t>(index) * selector->stride;
if (offset > UINT32_MAX) return false;
uint32_t key = 0;
if (!ReadScalarTable(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
runtime, {&key, 1})) return false;
if (!read_selector(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
{&key, 1})) return false;
keys.push_back(key);
mask &= mask - 1u;
}
} else return false;
if (selector != nullptr) {
} else {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material) || selector->stride < 4u ||
!std::has_single_bit(selector->stride) || (selector->offset & 3u) != 0u)
return false;
// Empty selector bounds denote the complete U32 congruence class. SMEM
// adds its aligned immediate after the dynamic offset, without U32 wrap.
const auto domain_count = (uint64_t {1} << 32u) / selector->stride;
const auto available = material.GetSize() > selector->offset
? material.GetSize() - selector->offset : 0u;
const auto count = std::min(domain_count,
available < 4u ? 0u : (available - 4u) / selector->stride + 1u);
if (count > MaxIndirectDescriptorProbes ||
!read_keys(material.Base48(), material.GetSize(), selector->offset, selector->stride, count)) return false;
if (count < domain_count) keys.push_back(0u); // An out-of-bounds scalar read returns zero.
}
if (selector != nullptr && selector->byte_ranges.empty()) {
if (selector->bit_offset != 0u || selector->bit_mask != UINT32_MAX || selector->maximum != UINT32_MAX)
for (auto& key: keys)
key = std::min((key >> selector->bit_offset) & selector->bit_mask, selector->maximum);
std::ranges::sort(keys);
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
}
@ -433,7 +601,7 @@ struct SamplerPlan {
uint32_t source;
SamplerClass type;
};
std::array<std::array<uint32_t, 3>, ShaderInfo::MaxSamplers> mapping;
std::array<std::array<uint32_t, 2>, ShaderInfo::MaxSamplers> mapping;
std::array<Binding, ShaderInfo::MaxSamplers> bindings;
uint32_t sampler_count = 0;
};
@ -520,6 +688,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
if (base.atomic &&
(base.atomic64 ? format != Prospero::BufferFormat::k32_32UInt
: format != Prospero::BufferFormat::k32UInt &&
format != Prospero::BufferFormat::k32SInt &&
format != Prospero::BufferFormat::k32Float)) {
return SpecializationFail(
fmt::format("atomic image descriptor {} uses unsupported format {}", i,
@ -540,8 +709,15 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
image.shader_swizzle = DescriptorImageSwizzle(descriptor);
}
const bool raw_sint_storage = storage && format == Prospero::BufferFormat::k32SInt &&
base.written && !base.read && !base.atomic;
image.numeric_class = Prospero::SampledTextureNumericClass(format);
base.written && (base.atomic ? !base.atomic64 : !base.read);
image.numeric_class = Prospero::SampledTextureNumericClass(
Prospero::RemapTextureFormat(format));
if (!storage && format == Prospero::BufferFormat::k11_11_10UNorm) {
if (image.indirect_root != ImageResource::NoIndirectImage) {
return SpecializationFail("packed UNorm sampling requires a direct image");
}
if (!ValidatePackedUnormSampling(program, snapshot, i)) return false;
}
if (storage) {
if ((!raw_sint_storage && image.numeric_class == Prospero::TextureNumericClass::Sint) ||
image.numeric_class == Prospero::TextureNumericClass::Unsupported) {
@ -609,8 +785,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
}
const bool same_coordinates = image.dimension == image_class.dimension &&
image.cube == image_class.cube;
if (image.numeric_class != image_class.numeric_class ||
(!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension))) ||
if ((!same_coordinates && !(is_2d(image.dimension) && is_2d(image_class.dimension))) ||
image.mip_count != image_class.mip_count ||
image.conversion_format != image_class.conversion_format ||
image.shader_swizzle != image_class.shader_swizzle) {
@ -635,6 +810,10 @@ bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
return false;
}
usage[pair.sampler] |= 1u << static_cast<uint32_t>(ClassifySampler(base.images[pair.image]));
for (const auto candidate: base.images[pair.image].indirect_resources) {
if (candidate >= base.images.size()) return false;
usage[pair.sampler] |= 1u << static_cast<uint32_t>(ClassifySampler(base.images[candidate]));
}
for (const auto candidate: base.samplers[pair.sampler].indirect_resources) {
if (candidate >= base.samplers.size()) return false;
usage[candidate] |= usage[pair.sampler];
@ -751,55 +930,16 @@ static bool IsFullDispatchPredicate(Value value, uint32_t depth = 0) {
return axis.IsImmediate() && axis.U32() < 3 && extent->Arg(0).Resolve() == axis;
}
// Nonnegative affine coefficients for constant, local and workgroup coordinates. Reject modular
// arithmetic that could wrap; runtime coverage also bounds the largest invocation index.
static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t axis, Value guard,
uint32_t depth = 0) {
value = ResolveActiveU32(value, guard);
if (depth > 32 || value.GetType() != Type::U32) {
return {};
}
if (value.IsImmediate()) {
return std::array<uint64_t, 3> {value.U32(), 0, 0};
}
const auto* inst = value.TryInstruction();
if (inst == nullptr) {
return {};
}
const auto op = inst->GetOpcode();
if (op == ValueOpcode::GetBuiltin && inst->Arg(1) == Value(axis)) {
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId))) {
return std::array<uint64_t, 3> {0, 1, 0};
}
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::WorkgroupId))) {
return std::array<uint64_t, 3> {0, 0, 1};
}
}
if (op != ValueOpcode::IAdd32 && op != ValueOpcode::IMul32 &&
op != ValueOpcode::ShiftLeftLogical32) {
return {};
}
auto left = FillIndex(inst->Arg(0), axis, guard, depth + 1);
auto right = FillIndex(inst->Arg(1), axis, guard, depth + 1);
if (!left || !right) {
return {};
}
if (op == ValueOpcode::IMul32 && ((*right)[1] != 0 || (*right)[2] != 0)) {
std::swap(left, right);
}
if (op != ValueOpcode::IAdd32 && ((*right)[1] != 0 || (*right)[2] != 0)) {
return {};
}
if (op == ValueOpcode::ShiftLeftLogical32) {
if ((*right)[0] >= 32) return {};
(*right)[0] = uint64_t {1} << (*right)[0];
}
for (uint32_t i = 0; i < left->size(); ++i) {
(*left)[i] =
op == ValueOpcode::IAdd32 ? (*left)[i] + (*right)[i] : (*left)[i] * (*right)[0];
if ((*left)[i] > UINT32_MAX) return {};
}
return left;
static std::optional<std::array<uint64_t, 3>> FillIndex(Value value, uint32_t axis, Value guard) {
return AffineIndex<3>(value, [&](Value at) { return ResolveActiveU32(at, guard); },
[&](Value at) -> std::optional<size_t> {
const auto* inst = at.TryInstruction();
if (inst == nullptr || inst->GetOpcode() != ValueOpcode::GetBuiltin ||
inst->Arg(1) != Value(axis)) return {};
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId))) return 1u;
if (inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::WorkgroupId))) return 2u;
return {};
});
}
static UniformFillPlan AnalyzeUniformFill(const Program& program) {
@ -985,43 +1125,79 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
for (const auto& read: program.srt_reads) {
plan.srt_reads.push_back({Clone(read.value), read.flat_offset});
}
// A proven uniform factor can decide a branch even when its other lanes are unknown.
// Keep only that Boolean structure, never the varying shader dependency graph.
Value unknown;
// Retain integer control expressions with opaque varying leaves. Their identities
// matter when an EXEC factor also guards a selected value; never clone float work.
bool capture_control_reads = false;
bool uniform_control = false;
const auto control_value_count = plan.value_storage.size();
struct ControlValue {
Value value;
bool complete = false;
};
std::unordered_map<const Inst*, ControlValue> predicates;
std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value {
value = value.Resolve();
if (value.IsEmpty()) return {};
const auto* inst = value.TryInstruction();
if (inst == nullptr) {
uniform_control |= value.GetType() == Type::U1;
return ValidateRuntimeValue(program, value, RuntimeValueType::Integer) ? value : Value {};
}
const auto [found, inserted] = predicates.try_emplace(inst);
auto& entry = found->second;
auto& result = entry.value;
const auto opaque = [&]() -> Value {
if (value.GetType() != Type::U1 && value.GetType() != Type::U32) return {};
return result = Value(&plan.value_storage.emplace_back(
value.GetType() == Type::U1 ? ValueOpcode::UndefU1 : ValueOpcode::UndefU32));
};
// A recursive projection remains opaque, including unstructured loop Phis.
if (!inserted) return entry.complete || !result.IsEmpty() ? result : opaque();
const auto finish = [&](Value projected) {
entry.complete = true;
return result = projected;
};
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) {
uniform_control = true;
capture_control_reads |=
!ValidateRuntimeValue(program, value, RuntimeValueType::ImmutableInteger);
return Clone(value);
return finish(Clone(value));
}
const auto* inst = value.TryInstruction();
if (inst == nullptr) return {};
const auto op = inst->GetOpcode();
if (op == ValueOpcode::ConditionRef || op == ValueOpcode::LogicalNot) {
const auto operand = ClonePredicate(inst->Arg(0));
if (operand.IsEmpty() || op == ValueOpcode::ConditionRef) return operand;
auto& node = plan.value_storage.emplace_back(op);
node.SetArg(0, operand);
return Value(&node);
}
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr) return {};
auto left = ClonePredicate(inst->Arg(0));
auto right = ClonePredicate(inst->Arg(1));
if (left.IsEmpty() && right.IsEmpty()) return {};
if (left.IsEmpty() || right.IsEmpty()) {
if (unknown.IsEmpty()) unknown = Value(&plan.value_storage.emplace_back(ValueOpcode::UndefU1));
if (left.IsEmpty()) left = unknown;
if (right.IsEmpty()) right = unknown;
if (op == ValueOpcode::ConditionRef) return finish(ClonePredicate(inst->Arg(0)));
if (op == ValueOpcode::Phi) {
if (inst->Parent() == nullptr || inst->Parent()->terminator.loop_header ||
inst->NumPhiBlocks() != inst->NumArgs() || inst->NumArgs() == 0u ||
(value.GetType() != Type::U1 && value.GetType() != Type::U32)) return finish(opaque());
std::vector<uint32_t> predecessors;
for (size_t index = 0; index < inst->NumArgs(); ++index) {
const auto predecessor = std::ranges::find(program.blocks, inst->PhiBlock(index));
if (predecessor == program.blocks.end()) return finish(opaque());
predecessors.push_back(static_cast<uint32_t>(predecessor - program.blocks.begin()));
}
auto& node = plan.value_storage.emplace_back(op, inst->Flags<uint64_t>());
plan.control_phi_predecessors.emplace(&node, std::move(predecessors));
for (size_t index = 0; index < inst->NumArgs(); ++index)
node.AddPhiOperand(nullptr, ClonePredicate(inst->Arg(index)));
return finish(Value(&node));
}
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr &&
op != ValueOpcode::LogicalNot && op != ValueOpcode::IEqual32 &&
op != ValueOpcode::INotEqual32 && op != ValueOpcode::ULessThanEqual32 &&
op != ValueOpcode::SelectU1 && op != ValueOpcode::SelectU32) return finish(opaque());
auto& node = plan.value_storage.emplace_back(op);
node.SetArg(0, left);
node.SetArg(1, right);
return Value(&node);
for (size_t index = 0; index < inst->NumArgs(); ++index)
node.SetArg(index, ClonePredicate(inst->Arg(index)));
return finish(Value(&node));
};
plan.control_flow = ResourceControlFlow(program, ClonePredicate);
if (!uniform_control) {
plan.control_flow.clear();
plan.control_phi_predecessors.clear();
auto first = std::next(plan.value_storage.begin(), control_value_count);
for (auto it = first; it != plan.value_storage.end(); ++it) it->Invalidate();
plan.value_storage.erase(first, plan.value_storage.end());
}
plan.uniform_fill = AnalyzeUniformFill(program);
for (uint32_t i = 0; i < plan.uniform_fill.fill.words; ++i) {
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
@ -1041,8 +1217,11 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
for (const auto& source: plan.descriptor_sources) {
if (!source.indirect_descriptor.has_value()) continue;
const auto& indirect = *source.indirect_descriptor;
if (indirect.selector)
if (indirect.selector) {
MarkCleanReads(planned_reads, Source(plan, indirect.selector->source));
if (indirect.selector->rows)
MarkCleanReads(planned_reads, Source(plan, indirect.selector->rows->source));
}
MarkCleanReads(planned_reads, nullptr, indirect.selector_mask);
MarkCleanReads(planned_reads, nullptr, indirect.selector_first);
MarkCleanReads(planned_reads, nullptr, indirect.key_count);
@ -1064,6 +1243,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
}
if (capture_indirect_reads || !plan.source_reads.empty())
plan.resource_tracking_complete &= !program.has_address_writes;
CompileSrtWalker(plan);
return plan;
}
@ -1125,7 +1305,10 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
if (!MaterializeIndirectDescriptor(
program, *source->indirect_descriptor, i, 4u, observed, clean, snapshot,
snapshot.buffers, specialization.buffers, ShaderInfo::MaxBuffers,
NormalizeIndirectStoreBuffer))
[&](DescriptorValue& value) {
return NormalizeIndirectStoreBuffer(
value, source->indirect_descriptor->indexed_store, base.formatted);
}))
return false;
}
} else if (!evaluate(base.source, snapshot.buffers[i], base.written)) {
@ -1300,7 +1483,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
samplers.push_back(samplers[binding.source]);
}
samplers[index].snapshot_index = binding.source;
samplers[index].force_point_filtering = binding.type == SamplerClass::PointInteger;
samplers[index].force_point_filtering = binding.type == SamplerClass::Integer;
samplers[index].integer_border = binding.type != SamplerClass::Float;
}
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
@ -1328,6 +1511,16 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
inst.SetFlags(image_remap[inst.Flags<uint32_t>()]);
continue;
}
if (inst.GetOpcode() == ValueOpcode::StoreBufferU32) {
auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
if (memory.kind == ResourceKind::Buffer && memory.formatted &&
program.descriptor_sources[buffers[memory.resource].source].indirect_descriptor) {
// Candidate validation proved an index-only k32UInt identity store.
// Reuse finite raw-store dispatch and its original-index bounds check.
memory.formatted = false;
}
continue;
}
if (BufferAccessOf(inst.GetOpcode()) == BufferAccess::Read) {
const auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
if (memory.kind == ResourceKind::Buffer &&
@ -1405,6 +1598,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
}
EXIT_IF(image.indirect_root == memory.resource &&
inst.GetOpcode() != ValueOpcode::ImageSampleRaw &&
inst.GetOpcode() != ValueOpcode::ImageGatherRaw &&
inst.GetOpcode() != ValueOpcode::ImageRead);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,8 @@
namespace Libs::Graphics::ShaderRecompiler::IR {
// Resolves native descriptor sources, plans their scalar reads, and assigns dense resource bindings.
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg);
void TrackResources(Program& program, const Decoder::Program& decoded, const CFG::Graph& native_cfg,
ShaderStageInputInfo input = {});
} // namespace Libs::Graphics::ShaderRecompiler::IR

View File

@ -1,12 +1,17 @@
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
#include "common/abi.h"
#include "common/assert.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include <xbyak/xbyak.h>
#include <algorithm>
#include <bit>
#include <cmath>
#include <cstring>
#include <map>
#include <unordered_map>
#include <unordered_set>
namespace Libs::Graphics::ShaderRecompiler::IR {
@ -136,6 +141,7 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
case ValueOpcode::IMul64:
case ValueOpcode::UMulHi:
case ValueOpcode::UMin32:
case ValueOpcode::UMax32:
case ValueOpcode::ShiftLeftLogical32:
case ValueOpcode::ShiftLeftLogical64:
case ValueOpcode::ShiftRightLogical32:
@ -172,8 +178,9 @@ bool IsRuntimeUniformOp(ValueOpcode op) {
class RuntimeValidator {
public:
explicit RuntimeValidator(const ResourcePlan& program, RuntimeValueType type)
: m_program(program), m_type(type) {}
explicit RuntimeValidator(const ResourcePlan& program, RuntimeValueType type,
bool wave_uniform = false)
: m_program(program), m_type(type), m_wave_uniform(wave_uniform) {}
bool Run(Value value) { return Validate(value); }
@ -272,6 +279,12 @@ private:
}
return finish(true);
}
if (m_wave_uniform && op == ValueOpcode::GetBuiltin) {
const auto axis = inst->NumArgs() == 2u ? inst->Arg(1).Resolve() : Value {};
return finish(m_program.stage == ShaderType::Compute && inst->NumArgs() == 2u &&
inst->Arg(0) == Value(static_cast<uint32_t>(StageInputKind::WorkgroupId)) &&
axis.IsImmediate() && axis.GetType() == Type::U32 && axis.U32() < 3u);
}
if (op == ValueOpcode::Phi) {
if (m_type != RuntimeValueType::Any && !ValidateArguments(*inst, false)) {
return finish(false);
@ -351,6 +364,7 @@ private:
const ResourcePlan& m_program;
RuntimeValueType m_type;
bool m_wave_uniform;
Value m_active_mask;
std::unordered_set<const Inst*> m_visiting;
std::unordered_set<const Inst*> m_validated_dependencies;
@ -359,13 +373,150 @@ private:
} // namespace
SrtWalker::SrtWalker(const ResourcePlan& program, const SrtRuntime& runtime,
SrtWalker* clean_evaluator, Value active_mask)
: m_program(program), m_runtime(runtime), m_clean_evaluator(clean_evaluator),
m_active_mask(active_mask.Resolve()),
m_context(AcquireContext(program)) {}
void CompileSrtWalker(ResourcePlan& program) {
if (!program.resource_tracking_complete || !program.srt_plan_complete ||
program.capture_specialization_reads || !program.control_flow.empty() ||
program.srt_reads.empty()) return;
SrtWalker::~SrtWalker() { --m_program.evaluation_depth; }
using Reads = std::map<int32_t, const SrtRead*>;
std::map<uint32_t, Reads> roots;
std::unordered_map<const Inst*, Reads> pointer_uses;
std::unordered_map<const Inst*, int32_t> offsets;
for (const auto& read: program.srt_reads) {
const auto* inst = read.value.Resolve().TryInstruction();
if (inst == nullptr || inst->GetOpcode() != ValueOpcode::LoadAddressU32 ||
inst->NumArgs() < 2 || !IsRawRead(program, *inst) ||
read.flat_offset >= program.srt_reads.size()) return;
const auto offset = inst->Arg(1).Resolve();
if (!offset.IsImmediate() || offset.GetType() != Type::U32) return;
const auto& memory = program.memory_info[inst->Flags<SrtReadFlags>().index];
const auto relative =
(static_cast<int64_t>(static_cast<int32_t>(memory.offset)) & ~int64_t {3}) +
static_cast<int64_t>(offset.U32() & ~3u);
if (relative < INT32_MIN || relative > INT32_MAX ||
!offsets.emplace(inst, static_cast<int32_t>(relative)).second) return;
}
uint32_t user_data_count = 0;
for (const auto& read: program.srt_reads) {
const auto* inst = read.value.ResolveInstruction();
const auto* handle = inst->Arg(0).Resolve().TryInstruction();
if (handle == nullptr || handle->GetOpcode() != ValueOpcode::GetAddressResource ||
handle->NumArgs() != 2) return;
const auto* low = handle->Arg(0).Resolve().TryInstruction();
const auto* high = handle->Arg(1).Resolve().TryInstruction();
if (low == nullptr || high == nullptr) return;
Reads* uses = nullptr;
if (low->GetOpcode() == ValueOpcode::GetUserData &&
high->GetOpcode() == ValueOpcode::GetUserData && low->NumArgs() == 1 &&
high->NumArgs() == 1 && low->Arg(0).GetType() == Type::ScalarReg &&
high->Arg(0).GetType() == Type::ScalarReg) {
const auto reg = RegIndex(low->Arg(0).ScalarRegister());
if (reg < program.user_data_base ||
RegIndex(high->Arg(0).ScalarRegister()) != reg + 1u) return;
const auto base = reg - program.user_data_base;
user_data_count = std::max(user_data_count, base + 2u);
uses = &roots[base];
} else {
const auto lo = offsets.find(low);
const auto hi = offsets.find(high);
if (lo == offsets.end() || hi == offsets.end() ||
static_cast<int64_t>(lo->second) + 4 != hi->second ||
!EquivalentValue(program, low->Arg(0), high->Arg(0))) return;
uses = &pointer_uses[low];
}
if (!uses->emplace(offsets.at(inst), &read).second) return;
}
std::unordered_set<const Inst*> visited;
const auto reachable = [&](auto&& self, const Reads& reads) -> bool {
for (const auto& [offset, read]: reads) {
const auto* inst = read->value.ResolveInstruction();
const auto child = pointer_uses.find(inst);
if (child != pointer_uses.end() &&
(!visited.insert(inst).second || !self(self, child->second))) return false;
}
return true;
};
for (const auto& [reg, reads]: roots) {
if (!reachable(reachable, reads)) return;
}
if (visited.size() != pointer_uses.size()) return;
program.srt_code = std::make_unique<Xbyak::CodeGenerator>(4096, Xbyak::AutoGrow);
auto& code = *program.srt_code;
Xbyak::Label failed, finished;
code.push(code.rbp);
code.mov(code.rbp, code.rsp);
code.cmp(code.r8, user_data_count);
code.jb(failed, Xbyak::CodeGenerator::T_NEAR);
code.mov(code.r9, AddressMask & ~uint64_t {3});
// Follow shadPS4's VisitPointer: copy one table level in offset order, then
// descend through its child pointers. Seed the descriptor evaluator's memo.
const auto visit = [&](auto&& self, int32_t pointer_offset, const Reads& reads) -> void {
code.push(code.rdi);
code.mov(code.rdi, code.qword[code.rdi + pointer_offset]);
code.and_(code.rdi, code.r9);
const auto first = reads.begin()->first;
const auto last = reads.rbegin()->first;
if (first < 0) {
code.mov(code.r10, -static_cast<int64_t>(first));
code.cmp(code.rdi, code.r10);
code.jb(failed, Xbyak::CodeGenerator::T_NEAR);
}
if (last > 0) {
code.mov(code.r10, AddressMask - last);
code.cmp(code.rdi, code.r10);
code.ja(failed, Xbyak::CodeGenerator::T_NEAR);
}
for (const auto& [offset, read]: reads) {
const auto index = read->value.ResolveInstruction()->EvaluationIndex(program.evaluation_value_count);
using Entry = ResourcePlan::EvaluationContext::Entry;
const auto slot = code.rdx + index * sizeof(Entry);
code.mov(code.r10d, code.dword[code.rdi + offset]);
code.mov(code.dword[code.rsi + read->flat_offset * sizeof(uint32_t)], code.r10d);
code.mov(code.qword[slot + offsetof(Entry, value)], code.r10);
code.mov(code.qword[slot + offsetof(Entry, generation)], code.rcx);
}
for (const auto& [offset, read]: reads) {
const auto child = pointer_uses.find(read->value.ResolveInstruction());
if (child == pointer_uses.end()) continue;
self(self, offset, child->second);
}
code.pop(code.rdi);
};
for (const auto& [reg, reads]: roots) {
visit(visit, reg * sizeof(uint32_t), reads);
}
code.mov(code.eax, 1);
code.jmp(finished, Xbyak::CodeGenerator::T_NEAR);
code.L(failed);
code.xor_(code.eax, code.eax);
code.L(finished);
code.mov(code.rsp, code.rbp);
code.pop(code.rbp);
code.ret();
code.readyRE();
EXIT_IF(Xbyak::GetError() != 0);
}
SrtWalker::SrtWalker(const ResourcePlan& program, const SrtRuntime& runtime,
SrtWalker* clean_evaluator)
: m_program(program), m_runtime(runtime), m_clean_evaluator(clean_evaluator),
m_session(this),
m_read_policy(clean_evaluator != nullptr ? 2u : 0u), m_context(AcquireContext()) {}
SrtWalker::SrtWalker(SrtWalker& parent, Value active_mask, bool clean,
SrtWalker* clean_evaluator)
: m_program(parent.m_program), m_runtime(clean ? CleanRuntime(parent.m_runtime) : parent.m_runtime),
m_clean_evaluator(clean_evaluator), m_active_mask(active_mask.Resolve()),
m_reachable_blocks(parent.m_reachable_blocks), m_session(parent.m_session),
m_read_policy((parent.m_read_policy & 1u) | (clean ? 1u : 0u) |
(clean_evaluator != nullptr ? 2u : 0u)), m_context(AcquireContext()) {}
SrtWalker::~SrtWalker() {
if (m_session != this) return;
for (auto& context: m_program.evaluation_contexts)
if (context.session == this) context.session = nullptr;
}
bool SrtWalker::Evaluate(Value value, uint32_t& result) {
uint64_t wide = 0;
@ -376,15 +527,29 @@ bool SrtWalker::Evaluate(Value value, uint32_t& result) {
return true;
}
ResourcePlan::EvaluationContext& SrtWalker::AcquireContext(const ResourcePlan& program) {
if (program.evaluation_depth == program.evaluation_contexts.size()) {
program.evaluation_contexts.emplace_back();
ResourcePlan::EvaluationContext& SrtWalker::AcquireContext() {
ResourcePlan::EvaluationContext* unused = nullptr;
for (auto& context: m_program.evaluation_contexts) {
if (context.session == m_session && context.active_mask == m_active_mask &&
context.read_policy == m_read_policy) return context;
if (context.session == nullptr && unused == nullptr) unused = &context;
}
auto& context = program.evaluation_contexts[program.evaluation_depth++];
context.generation += 2;
auto& context = unused != nullptr ? *unused : m_program.evaluation_contexts.emplace_back();
context.session = m_session;
context.active_mask = m_active_mask;
context.read_policy = m_read_policy;
context.generation += 4;
return context;
}
void SrtWalker::ResetUnknowns() {
for (auto& context: m_program.evaluation_contexts) {
if (context.session != m_session) continue;
for (auto& entry: context.values)
if (entry.generation == (context.generation | 2u)) entry.generation = 0;
}
}
float SrtWalker::Float32(uint64_t bits) {
return std::bit_cast<float>(static_cast<uint32_t>(bits));
}
@ -422,8 +587,10 @@ bool SrtWalker::EvaluateWide(Value value, uint64_t& result) {
result = m_context.values[index].value;
return true;
}
// The low generation bit marks an instruction that is still being evaluated.
if (m_context.values[index].generation == (m_context.generation | 1u)) {
// Visiting and completed-unknown entries both stop repeated evaluation in this
// context. Only completed unknowns are retried when reachability becomes tighter.
if (m_context.values[index].generation == (m_context.generation | 1u) ||
m_context.values[index].generation == (m_context.generation | 2u)) {
return false;
}
m_context.values[index].generation = m_context.generation | 1u;
@ -432,7 +599,7 @@ bool SrtWalker::EvaluateWide(Value value, uint64_t& result) {
// Recursive evaluation may grow the dense memo vector.
auto& memo = m_context.values[index];
if (!evaluated) {
memo.generation = 0;
memo.generation = m_context.generation | 2u;
return false;
}
memo.value = out;
@ -446,6 +613,19 @@ bool SrtWalker::Arg(const Inst& inst, size_t index, uint64_t& result) {
}
bool SrtWalker::EvaluatePhi(const Inst& inst, uint64_t& result) {
if (const auto control = m_program.control_phi_predecessors.find(&inst);
control != m_program.control_phi_predecessors.end()) {
bool found = false;
for (size_t index = 0; index < inst.NumArgs(); ++index) {
if (m_reachable_blocks != nullptr &&
!m_reachable_blocks->at(control->second[index])) continue;
uint64_t incoming = 0;
if (!Arg(inst, index, incoming) || (found && incoming != result)) return false;
result = incoming;
found = true;
}
return found;
}
const auto value = ResolveInvariantPhi(m_program, Value(const_cast<Inst*>(&inst)));
return !value.IsEmpty() && EvaluateWide(value, result);
}
@ -543,7 +723,8 @@ bool SrtWalker::EvaluateRawRead(const Inst& inst, uint64_t& result) {
? static_cast<uint64_t>(static_cast<uint32_t>(records))
: static_cast<uint64_t>(stride) * static_cast<uint32_t>(records);
if (byte_offset > size || size - byte_offset < sizeof(uint32_t)) {
return false;
result = 0u;
return true;
}
address = (base & ~uint64_t {3}) + byte_offset;
} else {
@ -591,9 +772,8 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
if (!m_active_mask.IsEmpty() && inst.Arg(1).Resolve() == m_active_mask) {
return EvaluateWide(inst.Arg(0), result);
}
const auto clean_runtime = CleanRuntime(m_runtime);
SrtWalker clean_active(m_program, clean_runtime, nullptr, inst.Arg(1));
SrtWalker active(m_program, m_runtime, &clean_active, inst.Arg(1));
SrtWalker clean_active(*this, inst.Arg(1), true);
SrtWalker active(*this, inst.Arg(1), false, &clean_active);
return active.EvaluateWide(inst.Arg(0), result);
}
case ValueOpcode::BitCastU32F32:
@ -663,8 +843,11 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
}
return false;
case ValueOpcode::UMin32:
case ValueOpcode::UMax32:
if (binary()) {
result = std::min(static_cast<uint32_t>(a), static_cast<uint32_t>(b));
result = inst.GetOpcode() == ValueOpcode::UMin32
? std::min(static_cast<uint32_t>(a), static_cast<uint32_t>(b))
: std::max(static_cast<uint32_t>(a), static_cast<uint32_t>(b));
return true;
}
return false;
@ -877,7 +1060,13 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
}
return false;
case ValueOpcode::ULessThanEqual32:
if (binary()) {
// A zero lower bound accepts every U32, including a varying lane value.
if (!Arg(inst, 0, a)) return false;
if (a == 0u) {
result = 1u;
return true;
}
if (Arg(inst, 1, b)) {
result = static_cast<uint32_t>(a) <= static_cast<uint32_t>(b);
return true;
}
@ -901,9 +1090,21 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
result = 0u;
return true;
}
if (!Arg(inst, 1, b) || (b != 0u && !left)) return false;
result = b != 0u;
return true;
if (Arg(inst, 1, b) && (b == 0u || left)) {
result = b != 0u;
return true;
}
if (!left) {
// Only lanes satisfying the left EXEC factor can make the conjunction
// true. Use a separate memo so this assumption cannot escape its RHS.
SrtWalker active(m_clean_evaluator != nullptr ? *m_clean_evaluator : *this,
inst.Arg(0));
if (active.EvaluateWide(inst.Arg(1), b) && b == 0u) {
result = 0u;
return true;
}
}
return false;
}
case ValueOpcode::LogicalOr: {
const bool left = Arg(inst, 0, a);
@ -954,6 +1155,15 @@ bool SrtWalker::EvaluateDescriptor(uint32_t source, DescriptorValue& result) {
bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
if (!m_program.srt_plan_complete) return false;
if (m_program.srt_code && m_runtime.read_memory == nullptr) {
flat.resize(m_program.srt_reads.size());
m_program.active_sources.clear();
m_context.values.resize(m_program.evaluation_value_count);
using Walker = bool KYTY_SYSV_ABI (*)(const uint32_t*, uint32_t*,
ResourcePlan::EvaluationContext::Entry*, uint64_t, size_t);
return m_program.srt_code->getCode<Walker>()(m_runtime.user_data.data(), flat.data(),
m_context.values.data(), m_context.generation, m_runtime.user_data.size());
}
const auto refresh = [&](uint32_t slot) {
if (slot >= m_program.srt_reads.size()) return false;
const auto& read = m_program.srt_reads[slot];
@ -973,35 +1183,54 @@ bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
}
return true;
}
auto& visited = m_program.visited_blocks;
auto& reachable = m_program.reachable_blocks;
auto& pending = m_program.pending_blocks;
visited.assign(m_program.control_flow.size(), 1u);
ResetUnknowns();
m_reachable_blocks = &visited;
if (m_clean_evaluator != nullptr) {
m_clean_evaluator->ResetUnknowns();
m_clean_evaluator->m_reachable_blocks = &visited;
}
for (;;) {
reachable.assign(m_program.control_flow.size(), 0u);
pending.clear();
pending.push_back(0u);
while (!pending.empty()) {
const auto index = pending.back();
pending.pop_back();
if (reachable.at(index)) continue;
reachable[index] = 1u;
const auto& block = m_program.control_flow[index];
uint32_t condition = 0;
auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this;
if (!block.condition.IsEmpty() &&
(m_runtime.read_specialization_memory != nullptr || !m_program.capture_specialization_reads) &&
predicate.Evaluate(block.condition, condition)) {
pending.push_back(block.successors[condition != 0u ? 0u : 1u]);
} else {
pending.insert(pending.end(), block.successors.begin(), block.successors.end());
}
}
if (reachable == visited) break;
visited.swap(reachable);
if (m_program.control_phi_predecessors.empty()) break;
// Only unknown values need another evaluation: successful Phi facts remain
// true as their possible predecessors shrink. Keep uniform-read memos stable.
ResetUnknowns();
if (m_clean_evaluator != nullptr) m_clean_evaluator->ResetUnknowns();
}
flat.assign(m_program.srt_reads.size(), 0u);
active.assign(m_program.descriptor_sources.size(), 1u);
for (const auto& block: m_program.control_flow) {
for (const auto& block: m_program.control_flow)
for (const auto source: block.sources) active.at(source) = 0u;
}
auto& visited = m_program.visited_blocks;
auto& pending = m_program.pending_blocks;
visited.assign(m_program.control_flow.size(), 0u);
pending.clear();
pending.push_back(0u);
while (!pending.empty()) {
const auto index = pending.back();
pending.pop_back();
if (visited.at(index)) continue;
visited[index] = 1u;
for (uint32_t index = 0; index < visited.size(); ++index) {
if (!visited[index]) continue;
const auto& block = m_program.control_flow[index];
for (const auto source: block.sources) active[source] = 1u;
for (const auto slot: block.srt_reads) {
for (const auto slot: block.srt_reads)
if (!refresh(slot)) return false;
}
uint32_t condition = 0;
auto& predicate = m_clean_evaluator != nullptr ? *m_clean_evaluator : *this;
if (!block.condition.IsEmpty() &&
(m_runtime.read_specialization_memory != nullptr || !m_program.capture_specialization_reads) &&
predicate.Evaluate(block.condition, condition)) {
pending.push_back(block.successors[condition != 0u ? 0u : 1u]);
} else {
pending.insert(pending.end(), block.successors.begin(), block.successors.end());
}
}
return true;
}
@ -1010,5 +1239,9 @@ bool ValidateRuntimeValue(const ResourcePlan& program, Value value, RuntimeValue
return RuntimeValidator(program, type).Run(value);
}
bool ValidateWaveUniformValue(const ResourcePlan& program, Value value) {
return RuntimeValidator(program, RuntimeValueType::Any, true).Run(value);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR

View File

@ -3,6 +3,7 @@
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include <algorithm>
#include <span>
namespace Libs::Graphics::ShaderRecompiler::IR {
@ -45,14 +46,60 @@ enum class RuntimeValueType { Any, Integer, ImmutableInteger };
bool ValidateRuntimeValue(const ResourcePlan& program, Value value,
RuntimeValueType type = RuntimeValueType::Any);
// Uniform across a compute wave, including values derived from its workgroup ID.
// This does not imply that the host can evaluate the value.
bool ValidateWaveUniformValue(const ResourcePlan& program, Value value);
void CompileSrtWalker(ResourcePlan& program);
// Uses the strict reader for values that affect shader specialization.
SrtRuntime CleanRuntime(SrtRuntime runtime);
// Nonnegative affine coefficients: element zero is constant, remaining elements are
// caller-selected leaves. Runtime range proofs must additionally exclude U32 wrap.
template <size_t Size>
std::optional<std::array<uint64_t, Size>> AffineIndex(
Value value, const auto& resolve, const auto& leaf, uint32_t depth = 0) {
value = resolve(value);
if (depth > 32u || value.GetType() != Type::U32) return {};
std::array<uint64_t, Size> result {};
if (value.IsImmediate()) {
result[0] = value.U32();
return result;
}
if (const auto index = leaf(value)) {
result[*index] = 1u;
return result;
}
const auto* inst = value.TryInstruction();
if (inst == nullptr) return {};
const auto op = inst->GetOpcode();
if (op != ValueOpcode::IAdd32 && op != ValueOpcode::IMul32 &&
op != ValueOpcode::ShiftLeftLogical32) return {};
auto left = AffineIndex<Size>(inst->Arg(0), resolve, leaf, depth + 1u);
auto right = AffineIndex<Size>(inst->Arg(1), resolve, leaf, depth + 1u);
if (!left || !right) return {};
const auto constant = [](const auto& coefficients) {
return std::ranges::all_of(std::span(coefficients).subspan(1u),
[](uint64_t coefficient) { return coefficient == 0u; });
};
if (op == ValueOpcode::IMul32 && !constant(*right)) std::swap(left, right);
if (op != ValueOpcode::IAdd32 && !constant(*right)) return {};
if (op == ValueOpcode::ShiftLeftLogical32) {
if ((*right)[0] >= 32u) return {};
(*right)[0] = uint64_t {1} << (*right)[0];
}
for (size_t index = 0; index < Size; ++index) {
(*left)[index] = op == ValueOpcode::IAdd32
? (*left)[index] + (*right)[index] : (*left)[index] * (*right)[0];
if ((*left)[index] > UINT32_MAX) return {};
}
return left;
}
// One memoized evaluation session shared by the entire shader resource refresh.
class SrtWalker {
public:
SrtWalker(const ResourcePlan& program, const SrtRuntime& runtime,
SrtWalker* clean_evaluator = nullptr, Value active_mask = {});
SrtWalker* clean_evaluator = nullptr);
~SrtWalker();
SrtWalker(const SrtWalker&) = delete;
SrtWalker& operator=(const SrtWalker&) = delete;
@ -63,7 +110,10 @@ public:
bool RefreshFlatBuffer(std::vector<uint32_t>& flat);
private:
static ResourcePlan::EvaluationContext& AcquireContext(const ResourcePlan& program);
SrtWalker(SrtWalker& parent, Value active_mask, bool clean = false,
SrtWalker* clean_evaluator = nullptr);
ResourcePlan::EvaluationContext& AcquireContext();
void ResetUnknowns();
static float Float32(uint64_t bits);
bool EvaluateWide(Value value, uint64_t& result);
bool Arg(const Inst& inst, size_t index, uint64_t& result);
@ -76,6 +126,9 @@ private:
SrtRuntime m_runtime;
SrtWalker* m_clean_evaluator = nullptr;
Value m_active_mask;
const std::vector<uint8_t>* m_reachable_blocks = nullptr;
const SrtWalker* m_session;
uint8_t m_read_policy;
ResourcePlan::EvaluationContext& m_context;
};

View File

@ -72,6 +72,9 @@ constexpr uint64_t DEFAULT_FLEXIBLE_MEMORY_SIZE = 1ull * 1024ull * 1024ull * 102
static uint64_t g_flexible_memory_size = DEFAULT_FLEXIBLE_MEMORY_SIZE;
static bool g_flexible_memory_size_frozen = false;
static Graphics::RenderContext* g_gpu_resources = nullptr;
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
static void (*g_test_gpu_range)(bool, uint64_t, size_t) = nullptr;
#endif
static Graphics::RenderContext& GetGpuResources() {
EXIT_IF(g_gpu_resources == nullptr);
@ -83,6 +86,9 @@ static bool IsGpuAddressRange(uint64_t vaddr, uint64_t size) {
}
static void MapGpuRange(uint64_t vaddr, uint64_t size) {
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
if (g_test_gpu_range != nullptr) g_test_gpu_range(true, vaddr, size);
#endif
if (g_gpu_resources == nullptr || !IsGpuAddressRange(vaddr, size)) {
return;
}
@ -90,6 +96,9 @@ static void MapGpuRange(uint64_t vaddr, uint64_t size) {
}
static void UnmapGpuRange(uint64_t vaddr, uint64_t size) {
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
if (g_test_gpu_range != nullptr) g_test_gpu_range(false, vaddr, size);
#endif
if (g_gpu_resources == nullptr || !IsGpuAddressRange(vaddr, size)) {
return;
}
@ -962,7 +971,11 @@ static bool IsInPrtAperture(uint64_t address, uint64_t size = 1) {
}
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) {
return g_guest_address_space->TryReadSparseBacking(vaddr, data, size);
return g_guest_address_space->CopySparseMemory<false>(vaddr, data, size);
}
bool CopySparseMemory(uint64_t vaddr, void* data, uint64_t size) {
return g_guest_address_space->CopySparseMemory<true>(vaddr, data, size);
}
static bool SelfTestSub64SharedPlaceholderAlias() {
@ -2524,20 +2537,7 @@ static bool FreeGuestMemoryOwner(uint64_t vaddr, uint64_t size) {
g_virtual_ranges->Remove(vaddr, size);
}
static int UnmapMemoryRange(uint64_t vaddr, size_t len) {
if (len == 0 || UINT64_MAX - vaddr < len) {
return KERNEL_ERROR_EINVAL;
}
VirtualRanges::Range range {};
if (!g_virtual_ranges->Query(vaddr, 0, &range)) {
return KERNEL_ERROR_EACCES;
}
const auto chunk_len = std::min<uint64_t>(len, range.size - (vaddr - range.start));
if (chunk_len < len) {
const int ret = UnmapMemoryRange(vaddr, chunk_len);
return ret == OK ? UnmapMemoryRange(vaddr + chunk_len, len - chunk_len) : ret;
}
static int UnmapMemoryOwner(const VirtualRanges::Range& range, uint64_t vaddr, size_t len) {
if (IsReservedRangeType(range.type)) {
if (!g_guest_address_space->ReleaseFree(vaddr, len)) {
return KERNEL_ERROR_EACCES;
@ -2601,6 +2601,13 @@ static int UnmapMemoryRange(uint64_t vaddr, size_t len) {
return OK;
}
static int UnmapMemoryRange(const VirtualRanges::Range& range, uint64_t vaddr, size_t len) {
UnmapGpuRange(vaddr, len);
const int result = UnmapMemoryOwner(range, vaddr, len);
if (result != OK && IsCommittedRangeType(range.type)) MapGpuRange(vaddr, len);
return result;
}
// The ABI entry point and batch operations hold g_memory_operation_mutex.
static int UnmapMemory(uint64_t vaddr, size_t len) {
LOGF("\t start = 0x%016" PRIx64 "\n"
@ -2614,8 +2621,11 @@ static int UnmapMemory(uint64_t vaddr, size_t len) {
if (!g_virtual_ranges->QuerySpan(vaddr, len, &ranges)) {
return KERNEL_ERROR_EACCES;
}
UnmapGpuRange(vaddr, len);
return UnmapMemoryRange(vaddr, len);
for (const auto& range: ranges) {
const int result = UnmapMemoryRange(range, range.start, range.size);
if (result != OK) return result;
}
return OK;
}
int KYTY_SYSV_ABI KernelMunmap(uint64_t vaddr, size_t len) {
@ -2624,6 +2634,31 @@ int KYTY_SYSV_ABI KernelMunmap(uint64_t vaddr, size_t len) {
return UnmapMemory(vaddr, len);
}
static bool IsAmmMemoryRange(uint64_t vaddr, size_t size) {
constexpr uint64_t page_size = 0x4000;
return size != 0 && ((vaddr | size) & (page_size - 1)) == 0 &&
vaddr >= kExtendedMemoryBase && vaddr < kExtendedMemoryBase + kExtendedMemorySize &&
size <= kExtendedMemoryBase + kExtendedMemorySize - vaddr;
}
int UnmapAmmMemory(uint64_t vaddr, size_t size) {
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
if (!IsAmmMemoryRange(vaddr, size)) return KERNEL_ERROR_EINVAL;
const auto end = vaddr + size;
while (vaddr < end) {
VirtualRanges::Range range {};
// AMM unmap accepts holes, including a completely unmapped range.
if (!g_virtual_ranges->Query(vaddr, 1, &range) || range.start >= end) break;
vaddr = std::max(vaddr, range.start);
const auto chunk = std::min(end, range.start + range.size) - vaddr;
const int result = UnmapMemoryRange(range, vaddr, chunk);
if (result != OK) return result;
vaddr += chunk;
}
return OK;
}
size_t KYTY_SYSV_ABI KernelGetDirectMemorySize() {
PRINT_NAME();
@ -3225,11 +3260,7 @@ int KYTY_SYSV_ABI KernelMapDirectMemory2(void** addr, size_t len, int type, int
int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot) {
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
constexpr uint64_t page_size = 0x4000;
if (size == 0 || (vaddr & (page_size - 1)) != 0 || (size & (page_size - 1)) != 0 ||
vaddr < kExtendedMemoryBase || vaddr >= kExtendedMemoryBase + kExtendedMemorySize ||
size > kExtendedMemoryBase + kExtendedMemorySize - vaddr) {
return KERNEL_ERROR_EINVAL;
}
if (!IsAmmMemoryRange(vaddr, size)) return KERNEL_ERROR_EINVAL;
PhysicalMemory::PhysicalRanges ranges;
if (!g_physical_memory->ReserveAutomatic(size, &ranges)) {
@ -3567,6 +3598,10 @@ int KYTY_SYSV_ABI KernelReserveVirtualRange(void** addr, size_t len, int flags,
}
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
void TestSetGpuRangeCallback(void (*callback)(bool, uint64_t, size_t)) {
g_test_gpu_range = callback;
}
void TestBeforeNextBackingMap(callback_func_t callback) {
g_test_before_backing_map = callback;
}

View File

@ -112,10 +112,12 @@ void SetFlexibleMemorySize(uint64_t size);
int AllocateDirectMemory(int64_t search_start, int64_t search_end, size_t size, size_t alignment,
int memory_type, int64_t* phys_addr_out, bool automatic = false);
int MapAutomaticMemory(uint64_t vaddr, size_t size, int type, int prot);
int UnmapAmmMemory(uint64_t vaddr, size_t size);
bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size);
bool TryReadBacking(uint64_t vaddr, void* data, uint64_t size);
bool TryReadBufferBacking(uint64_t vaddr, void* data, uint64_t size);
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size);
bool CopySparseMemory(uint64_t vaddr, void* data, uint64_t size);
[[nodiscard]] uint64_t ClampRangeSize(uint64_t vaddr, uint64_t size);
void WriteBacking(uint64_t vaddr, const void* data, uint64_t size) noexcept;
void InvalidateMemory(uint64_t vaddr, uint64_t size);
@ -191,6 +193,7 @@ bool ProtectGuestHostMemory(uint64_t vaddr, uint64_t size, Common::VirtualMemory
bool FreeGuestMemory(uint64_t vaddr, uint64_t size);
#if defined(KYTY_VIRTUAL_MEMORY_ALLOCATION_TESTS)
void TestSetGpuRangeCallback(void (*callback)(bool mapped, uint64_t addr, size_t size));
void TestBeforeNextBackingMap(callback_func_t callback);
void TestSetBackingReadCallback(callback_func_t callback);
void TestFailNextPhysicalMemoryUnmap();

View File

@ -28,6 +28,35 @@ static bool CoalescePlaceholders(uint64_t vaddr, uint64_t size) {
}
#endif
template <typename Regions, typename Source, typename Gap>
static void CopySparseRange(const Regions& regions, uint64_t vaddr, void* data, uint64_t size,
Source&& source, Gap&& gap) {
const auto end = vaddr + size;
auto mapping = regions.lower_bound(vaddr);
if (mapping != regions.begin()) {
auto previous = std::prev(mapping);
if (previous->second.vaddr + previous->second.size > vaddr) {
mapping = previous;
}
}
auto* destination = static_cast<uint8_t*>(data);
auto cursor = vaddr;
for (; mapping != regions.end() && mapping->second.vaddr < end; ++mapping) {
const auto& entry = mapping->second;
const auto start = std::max(vaddr, entry.vaddr);
const auto bytes = std::min(end, entry.vaddr + entry.size) - start;
if (cursor < start) {
gap(cursor, destination + (cursor - vaddr), start - cursor);
}
std::memcpy(destination + (start - vaddr), source(entry, start), bytes);
cursor = start + bytes;
}
if (cursor < end) {
gap(cursor, destination + (cursor - vaddr), end - cursor);
}
}
class GuestBackingStore {
public:
enum class FailureReason {
@ -247,7 +276,8 @@ public:
return true;
}
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) {
template <typename Gap>
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size, Gap&& gap) {
if (!IsAvailable() || data == nullptr || size == 0 || UINT64_MAX - vaddr < size) {
return false;
}
@ -258,31 +288,11 @@ public:
g_test_backing_read(vaddr, size);
}
#endif
const auto end = vaddr + size;
auto mapping = m_maps.lower_bound(vaddr);
if (mapping != m_maps.begin()) {
auto previous = std::prev(mapping);
if (previous->second.vaddr + previous->second.size > vaddr) {
mapping = previous;
}
}
auto* destination = static_cast<uint8_t*>(data);
auto cursor = vaddr;
for (; mapping != m_maps.end() && mapping->second.vaddr < end; ++mapping) {
const auto& entry = mapping->second;
const auto start = std::max(vaddr, entry.vaddr);
const auto bytes = std::min(end, entry.vaddr + entry.size) - start;
const auto offset = entry.backing_offset + start - entry.vaddr;
if (cursor < start) {
std::memset(destination + (cursor - vaddr), 0, start - cursor);
}
std::memcpy(destination + (start - vaddr), m_backing_base + offset, bytes);
cursor = start + bytes;
}
if (cursor < end) {
std::memset(destination + (cursor - vaddr), 0, end - cursor);
}
CopySparseRange(m_maps, vaddr, data, size,
[this](const auto& entry, uint64_t start) {
return m_backing_base + (entry.backing_offset + start - entry.vaddr);
},
gap);
return true;
}
@ -773,22 +783,41 @@ public:
return m_backing->TryReadBacking(vaddr, data, size);
}
bool TryReadSparseBacking(uint64_t vaddr, void* data, uint64_t size) {
if (!Owns(vaddr, size)) {
template <bool copy_private>
bool CopySparseMemory(uint64_t vaddr, void* data, uint64_t size) {
if (data == nullptr || !Owns(vaddr, size)) {
return false;
}
std::shared_lock lock(m_mutex);
auto mapping = m_mapped.upper_bound(vaddr);
if (mapping != m_mapped.begin()) {
--mapping;
}
for (; mapping != m_mapped.end() && mapping->second.vaddr < vaddr + size; ++mapping) {
const auto& region = mapping->second;
if (region.type == MappingType::Private && region.vaddr + region.size > vaddr) {
return false;
if constexpr (!copy_private) {
auto mapping = m_mapped.upper_bound(vaddr);
if (mapping != m_mapped.begin()) {
--mapping;
}
for (; mapping != m_mapped.end() && mapping->second.vaddr < vaddr + size; ++mapping) {
const auto& region = mapping->second;
if (region.type == MappingType::Private && region.vaddr + region.size > vaddr) {
return false;
}
}
}
return m_backing->TryReadSparseBacking(vaddr, data, size);
return m_backing->TryReadSparseBacking(
vaddr, data, size, [&](uint64_t start, uint8_t* destination, uint64_t bytes) {
const auto zero = [](uint64_t, uint8_t* output, uint64_t length) {
std::memset(output, 0, length);
};
if constexpr (copy_private) {
// Backing gaps can contain private mappings or reservations only.
CopySparseRange(m_mapped, start, destination, bytes,
[](const auto& region, uint64_t address) {
EXIT_IF(region.type != MappingType::Private);
return reinterpret_cast<const void*>(address);
},
zero);
} else {
zero(start, destination, bytes);
}
});
}
bool BackingContains(uint64_t vaddr, uint64_t size) { return m_backing->Contains(vaddr, size); }

View File

@ -1,5 +1,6 @@
find_package(Qt6 COMPONENTS Concurrent Network Widgets REQUIRED)
add_subdirectory("${KYTY_SOURCE_DIR}/updater" "${CMAKE_CURRENT_BINARY_DIR}/updater")
set(CMAKE_AUTOUIC_SEARCH_PATHS forms)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
@ -42,7 +43,13 @@ elseif(LINUX)
set_target_properties(launcher PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS}")
endif()
add_dependencies(launcher kyty_emulator)
add_custom_target(stage_updater
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:launcher>
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:kyty_updater>
$<TARGET_FILE_DIR:launcher>/$<TARGET_FILE_NAME:kyty_updater>
DEPENDS kyty_updater
VERBATIM)
add_dependencies(launcher kyty_emulator stage_updater)
if(KYTY_CLANG_CL)
add_custom_command(TARGET launcher POST_BUILD
@ -96,6 +103,7 @@ install(TARGETS launcher
if(APPLE)
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
install(TARGETS kyty_updater DESTINATION "KytyPS5.app/Contents/MacOS")
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION "KytyPS5.app/Contents/Resources/assets/sounds")
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
@ -112,6 +120,10 @@ if(APPLE)
")
endif()
if(NOT APPLE)
install(TARGETS kyty_updater RUNTIME DESTINATION .)
endif()
if(WIN32)
install(CODE "
execute_process(

View File

@ -16,394 +16,446 @@
<property name="locale">
<locale language="English" country="UnitedStates"/>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="horizontalSpacing">
<number>8</number>
</property>
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="sectionsLayout" columnstretch="1,1">
<property name="horizontalSpacing">
<number>8</number>
</property>
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="0">
<widget class="QGroupBox" name="user_profile_group">
<property name="title">
<string>User profile</string>
<layout class="QVBoxLayout" name="dialogLayout">
<property name="spacing"><number>8</number></property>
<item>
<widget class="QTabWidget" name="settings_tabs">
<property name="currentIndex"><number>0</number></property>
<widget class="QWidget" name="settings_tab">
<attribute name="title"><string>General</string></attribute>
<layout class="QGridLayout" name="gridLayout">
<property name="horizontalSpacing">
<number>8</number>
</property>
<layout class="QFormLayout" name="userProfileLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_user_name">
<property name="text"><string>User name:</string></property>
</widget>
</item>
<item row="0" column="1">
<widget class="MandatoryLineEdit" name="lineEdit_user_name">
<property name="toolTip"><string>Local user name exposed to games</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_user_id">
<property name="text"><string>User ID:</string></property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spinBox_user_id">
<property name="toolTip"><string>Local user ID exposed to games</string></property>
<property name="maximum"><number>2147483647</number></property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_console_language">
<property name="text"><string>Console language:</string></property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_console_language">
<property name="toolTip"><string>Language</string></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="audio_group">
<property name="title"><string>Audio</string></property>
<layout class="QFormLayout" name="audioLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_audio_input_device">
<property name="text"><string>Microphone:</string></property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_audio_input_device"/>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" rowspan="3">
<widget class="QGroupBox" name="graphics_group">
<property name="title">
<string>Graphics &amp;&amp; display</string>
<property name="verticalSpacing">
<number>8</number>
</property>
<layout class="QFormLayout" name="graphicsLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_screen_resolution">
<property name="text"><string>Screen resolution:</string></property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_screen_resolution">
<property name="toolTip"><string>Window resolution</string></property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_present_mode">
<property name="text"><string>Present mode:</string></property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_present_mode">
<property name="toolTip"><string>Vulkan swapchain presentation mode</string></property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_vblank_frequency">
<property name="text"><string>Vblank frequency:</string></property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="spinBox_vblank_frequency">
<property name="toolTip"><string>Virtual display refresh rate used for frame pacing</string></property>
<property name="minimum"><number>30</number></property>
<property name="maximum"><number>360</number></property>
<property name="value"><number>60</number></property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_shader_optimization">
<property name="text"><string>Shader optimization:</string></property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_shader_optimization_type">
<property name="toolTip"><string>Optimize shaders for code size or performance</string></property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_gpu">
<property name="text"><string>GPU:</string></property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="comboBox_gpu">
<property name="toolTip"><string>Graphics device used by the emulator</string></property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="checkBox_fullscreen">
<property name="toolTip"><string>Run in borderless desktop fullscreen</string></property>
<property name="text"><string>Fullscreen</string></property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="checkBox_readback">
<property name="toolTip"><string>Read back writable linear images</string></property>
<property name="text"><string>Enable readback</string></property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="checkBox_tessellation">
<property name="toolTip"><string>Draw tessellated geometry. Disabled by default; unsupported tessellation may cause crashes.</string></property>
<property name="text"><string>Enable tessellation support (experimental)</string></property>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="checkBox_hide_cursor">
<property name="toolTip"><string>Hide the host cursor after 2 seconds without mouse activity</string></property>
<property name="text"><string>Auto-hide cursor</string></property>
</widget>
</item>
<item row="9" column="1">
<widget class="QCheckBox" name="checkBox_skip_notice_screen">
<property name="toolTip"><string>Skip startup logos and notices in supported games.</string></property>
<property name="text"><string>Skip boot logos</string></property>
</widget>
</item>
<item row="10" column="1">
<widget class="QCheckBox" name="checkBox_sync_raw_image_buffers">
<property name="toolTip"><string>Can fix missing textures and incorrect transparency in some games. May reduce performance.</string></property>
<property name="text"><string>Accurate texture synchronization</string></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0">
<widget class="QGroupBox" name="notifications_group">
<property name="title"><string>Notifications</string></property>
<layout class="QVBoxLayout" name="notificationsLayout">
<item>
<widget class="QCheckBox" name="checkBox_trophy_notifications">
<property name="toolTip"><string>Show trophy unlock notifications and play their sound.
To use your own sound, replace the emulator's assets/sounds/trophy-unlock.wav with a WAV file and restart the game.</string></property>
<property name="text"><string>Enable trophy notifications</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QGroupBox" name="debug_group">
<property name="title">
<string>Debugging &amp;&amp; logs</string>
</property>
<layout class="QGridLayout" name="debugLayout" columnstretch="0,1,0,1">
<property name="horizontalSpacing"><number>8</number></property>
<property name="verticalSpacing"><number>6</number></property>
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_vulkan_validation">
<property name="toolTip"><string>Enable Vulkan validation layers</string></property>
<property name="text"><string>Vulkan validation</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
<item row="0" column="2" colspan="2">
<widget class="QCheckBox" name="checkBox_shader_validation">
<property name="toolTip"><string>Validate SPIR-V binary</string></property>
<property name="text"><string>Shader validation</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_renderdoc_capture">
<property name="toolTip"><string>Enable RenderDoc capture</string></property>
<property name="text"><string>RenderDoc capture</string></property>
</widget>
</item>
<item row="1" column="2" colspan="2">
<widget class="QCheckBox" name="checkBox_cmd_dump">
<property name="toolTip"><string>Dump command buffers</string></property>
<property name="text"><string>Command buffer dump</string></property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_shader_log_direction">
<property name="text"><string>Shader logging:</string></property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_shader_log_direction">
<property name="toolTip"><string>Dump shaders to file or console window. If enabled may decrease emulator performance</string></property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="label_shader_log_folder">
<property name="text"><string>Shader log folder:</string></property>
</widget>
</item>
<item row="2" column="3">
<widget class="MandatoryLineEdit" name="lineEdit_shader_log_folder">
<property name="toolTip"><string>Specify directory to dump shaders</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_printf_direction">
<property name="text"><string>Printf output:</string></property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_printf_direction">
<property name="toolTip"><string>Print logs to file or console window. If enabled may decrease emulator performance</string></property>
</widget>
</item>
<item row="3" column="2">
<widget class="QLabel" name="label_printf_file">
<property name="text"><string>Printf output file:</string></property>
</widget>
</item>
<item row="3" column="3">
<widget class="MandatoryLineEdit" name="lineEdit_printf_file">
<property name="toolTip"><string>Specify file to dump logs</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="checkBox_profiler">
<property name="toolTip"><string>Enable the Tracy profiler. Profiling may decrease emulator performance</string></property>
<property name="text"><string>Tracy profiler</string></property>
</widget>
</item>
<item row="4" column="2">
<widget class="QLabel" name="label_cmd_dump_folder">
<property name="text"><string>Command buffer folder:</string></property>
</widget>
</item>
<item row="4" column="3">
<widget class="MandatoryLineEdit" name="lineEdit_cmd_dump_folder">
<property name="toolTip"><string>Specify directory to dump command buffers</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="compatibility_group">
<property name="title">
<string>Compatibility</string>
</property>
<layout class="QVBoxLayout" name="compatibilityLayout">
<item>
<widget class="QCheckBox" name="checkBox_amd_cpu">
<property name="toolTip"><string>Patch CPU instructions that differ between AMD and the host CPU.</string></property>
<property name="text"><string>AMD CPU patch (experimental)</string></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_red_zone_protection">
<property name="toolTip"><string>Helps prevent crashes in some games on Windows</string></property>
<property name="text"><string>Windows SysV red zone crash protection (experimental)</string></property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QGroupBox" name="controller_group">
<property name="title"><string>Controller</string></property>
<layout class="QFormLayout" name="controllerLayout">
<property name="fieldGrowthPolicy"><enum>QFormLayout::AllNonFixedFieldsGrow</enum></property>
<item row="0" column="0">
<widget class="QLabel" name="label_controller_color">
<property name="text"><string>DualSense lightbar:</string></property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="controllerColorLayout">
<item>
<widget class="QPushButton" name="button_controller_color">
<property name="toolTip"><string>Choose and preview a fixed LED color. Bluetooth DualSense may take several seconds to update.</string></property>
<property name="text"><string>Custom</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_controller_color_reset">
<property name="toolTip"><string>Let the game control the lightbar</string></property>
<property name="text"><string>Default</string></property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_controller_vibration">
<property name="text"><string>Vibration intensity:</string></property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="controllerVibrationLayout">
<item>
<widget class="QSlider" name="slider_controller_vibration">
<property name="toolTip"><string>Scale DualSense vibration over USB and Bluetooth</string></property>
<property name="maximum"><number>100</number></property>
<property name="value"><number>100</number></property>
<property name="orientation"><enum>Qt::Horizontal</enum></property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_controller_vibration_value">
<property name="minimumSize"><size><width>40</width><height>0</height></size></property>
<property name="text"><string>100%</string></property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_controller_volume">
<property name="text"><string>Speaker volume:</string></property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="controllerVolumeLayout">
<item>
<widget class="QSlider" name="slider_controller_volume">
<property name="toolTip"><string>Scale the DualSense speaker over USB and Bluetooth. 50% is the previous default; 100% adds 6 dB of gain.</string></property>
<property name="maximum"><number>100</number></property>
<property name="value"><number>50</number></property>
<property name="orientation"><enum>Qt::Horizontal</enum></property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_controller_volume_value">
<property name="minimumSize"><size><width>40</width><height>0</height></size></property>
<property name="text"><string>50%</string></property>
</widget>
</item>
</layout>
</item>
</layout>
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="sectionsLayout" columnstretch="1,1">
<property name="horizontalSpacing">
<number>8</number>
</property>
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="0">
<widget class="QGroupBox" name="user_profile_group">
<property name="title">
<string>User profile</string>
</property>
<layout class="QFormLayout" name="userProfileLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_user_name">
<property name="text"><string>User name:</string></property>
</widget>
</item>
<item row="0" column="1">
<widget class="MandatoryLineEdit" name="lineEdit_user_name">
<property name="toolTip"><string>Local user name exposed to games</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_user_id">
<property name="text"><string>User ID:</string></property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spinBox_user_id">
<property name="toolTip"><string>Local user ID exposed to games</string></property>
<property name="maximum"><number>2147483647</number></property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_console_language">
<property name="text"><string>Console language:</string></property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_console_language">
<property name="toolTip"><string>Language</string></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="audio_group">
<property name="title"><string>Audio</string></property>
<layout class="QFormLayout" name="audioLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_audio_input_device">
<property name="text"><string>Microphone:</string></property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_audio_input_device"/>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" rowspan="3">
<widget class="QGroupBox" name="graphics_group">
<property name="title">
<string>Graphics &amp;&amp; display</string>
</property>
<layout class="QFormLayout" name="graphicsLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_screen_resolution">
<property name="text"><string>Screen resolution:</string></property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_screen_resolution">
<property name="toolTip"><string>Window resolution</string></property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_present_mode">
<property name="text"><string>Present mode:</string></property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_present_mode">
<property name="toolTip"><string>Vulkan swapchain presentation mode</string></property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_vblank_frequency">
<property name="text"><string>Vblank frequency:</string></property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="spinBox_vblank_frequency">
<property name="toolTip"><string>Virtual display refresh rate used for frame pacing</string></property>
<property name="minimum"><number>30</number></property>
<property name="maximum"><number>360</number></property>
<property name="value"><number>60</number></property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_shader_optimization">
<property name="text"><string>Shader optimization:</string></property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_shader_optimization_type">
<property name="toolTip"><string>Optimize shaders for code size or performance</string></property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_gpu">
<property name="text"><string>GPU:</string></property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="comboBox_gpu">
<property name="toolTip"><string>Graphics device used by the emulator</string></property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="checkBox_readback">
<property name="toolTip"><string>Read back writable linear images</string></property>
<property name="text"><string>Enable readback</string></property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="checkBox_tessellation">
<property name="toolTip"><string>Draw tessellated geometry. Disabled by default; unsupported tessellation may cause crashes.</string></property>
<property name="text"><string>Enable tessellation support (experimental)</string></property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="checkBox_sync_raw_image_buffers">
<property name="toolTip"><string>Can fix missing textures and incorrect transparency in some games. May reduce performance.</string></property>
<property name="text"><string>Accurate texture synchronization</string></property>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="checkBox_hdr">
<property name="toolTip"><string>Enable HDR10 output for supported games. Requires HDR enabled on the host display.</string></property>
<property name="text"><string>Enable HDR display support</string></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0">
<widget class="QGroupBox" name="notifications_group">
<property name="title"><string>Notifications</string></property>
<layout class="QVBoxLayout" name="notificationsLayout">
<item>
<widget class="QCheckBox" name="checkBox_trophy_notifications">
<property name="toolTip"><string>Show trophy unlock notifications and play their sound.
To use your own sound, replace the emulator's assets/sounds/trophy-unlock.wav with a WAV file and restart the game.</string></property>
<property name="text"><string>Enable trophy notifications</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="1">
<widget class="QGroupBox" name="general_group">
<property name="title"><string>General</string></property>
<layout class="QVBoxLayout" name="generalLayout">
<item>
<widget class="QCheckBox" name="checkBox_fullscreen">
<property name="toolTip"><string>Run in borderless desktop fullscreen</string></property>
<property name="text"><string>Run in fullscreen</string></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_hide_cursor">
<property name="toolTip"><string>Hide the host cursor after 2 seconds without mouse activity</string></property>
<property name="text"><string>Auto-hide cursor</string></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_skip_notice_screen">
<property name="toolTip"><string>Skip startup logos and notices in supported games.</string></property>
<property name="text"><string>Skip boot logos</string></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="compatibility_group">
<property name="title">
<string>Compatibility</string>
</property>
<layout class="QVBoxLayout" name="compatibilityLayout">
<item>
<widget class="QCheckBox" name="checkBox_amd_cpu">
<property name="toolTip"><string>Patch CPU instructions that differ between AMD and the host CPU.</string></property>
<property name="text"><string>AMD CPU patch (experimental)</string></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_red_zone_protection">
<property name="toolTip"><string>Helps prevent crashes in some games on Windows</string></property>
<property name="text"><string>Windows SysV red zone crash protection (experimental)</string></property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QGroupBox" name="controller_group">
<property name="title"><string>Controller</string></property>
<layout class="QFormLayout" name="controllerLayout">
<property name="fieldGrowthPolicy"><enum>QFormLayout::AllNonFixedFieldsGrow</enum></property>
<item row="0" column="0">
<widget class="QLabel" name="label_controller_color">
<property name="text"><string>DualSense lightbar:</string></property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="controllerColorLayout">
<item>
<widget class="QPushButton" name="button_controller_color">
<property name="toolTip"><string>Choose and preview a fixed LED color. Bluetooth DualSense may take several seconds to update.</string></property>
<property name="text"><string>Custom</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_controller_color_reset">
<property name="toolTip"><string>Let the game control the lightbar</string></property>
<property name="text"><string>Default</string></property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_controller_vibration">
<property name="text"><string>Vibration intensity:</string></property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="controllerVibrationLayout">
<item>
<widget class="QSlider" name="slider_controller_vibration">
<property name="toolTip"><string>Scale DualSense vibration over USB and Bluetooth</string></property>
<property name="maximum"><number>100</number></property>
<property name="value"><number>100</number></property>
<property name="orientation"><enum>Qt::Horizontal</enum></property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_controller_vibration_value">
<property name="minimumSize"><size><width>40</width><height>0</height></size></property>
<property name="text"><string>100%</string></property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_controller_volume">
<property name="text"><string>Speaker volume:</string></property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="controllerVolumeLayout">
<item>
<widget class="QSlider" name="slider_controller_volume">
<property name="toolTip"><string>Scale the DualSense speaker over USB and Bluetooth. 50% is the previous default; 100% adds 6 dB of gain.</string></property>
<property name="maximum"><number>100</number></property>
<property name="value"><number>50</number></property>
<property name="orientation"><enum>Qt::Horizontal</enum></property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_controller_volume_value">
<property name="minimumSize"><size><width>40</width><height>0</height></size></property>
<property name="text"><string>50%</string></property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="network_tab">
<attribute name="title"><string>Network</string></attribute>
<layout class="QVBoxLayout" name="networkTabLayout">
<item>
<widget class="QCheckBox" name="checkBox_network_connected">
<property name="toolTip"><string>Let games use your computer's local network connection.</string></property>
<property name="text"><string>Enable network connection</string></property>
</widget>
</item>
<item>
<spacer name="networkSpacer">
<property name="orientation"><enum>Qt::Vertical</enum></property>
<property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="debug_tab">
<attribute name="title"><string>Debug</string></attribute>
<layout class="QVBoxLayout" name="debugTabLayout">
<item>
<widget class="QGroupBox" name="debug_group">
<property name="title">
<string>Debugging &amp;&amp; logs</string>
</property>
<layout class="QGridLayout" name="debugLayout" columnstretch="0,1,0,1">
<property name="horizontalSpacing"><number>8</number></property>
<property name="verticalSpacing"><number>6</number></property>
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_vulkan_validation">
<property name="toolTip"><string>Enable Vulkan validation layers</string></property>
<property name="text"><string>Vulkan validation</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
<item row="0" column="2" colspan="2">
<widget class="QCheckBox" name="checkBox_shader_validation">
<property name="toolTip"><string>Validate SPIR-V binary</string></property>
<property name="text"><string>Shader validation</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_renderdoc_capture">
<property name="toolTip"><string>Enable RenderDoc capture</string></property>
<property name="text"><string>RenderDoc capture</string></property>
</widget>
</item>
<item row="1" column="2" colspan="2">
<widget class="QCheckBox" name="checkBox_cmd_dump">
<property name="toolTip"><string>Dump command buffers</string></property>
<property name="text"><string>Command buffer dump</string></property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_shader_log_direction">
<property name="text"><string>Shader logging:</string></property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_shader_log_direction">
<property name="toolTip"><string>Dump shaders to file or console window. If enabled may decrease emulator performance</string></property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="label_shader_log_folder">
<property name="text"><string>Shader log folder:</string></property>
</widget>
</item>
<item row="2" column="3">
<widget class="MandatoryLineEdit" name="lineEdit_shader_log_folder">
<property name="toolTip"><string>Specify directory to dump shaders</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_printf_direction">
<property name="text"><string>Printf output:</string></property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_printf_direction">
<property name="toolTip"><string>Print logs to file or console window. If enabled may decrease emulator performance</string></property>
</widget>
</item>
<item row="3" column="2">
<widget class="QLabel" name="label_printf_file">
<property name="text"><string>Printf output file:</string></property>
</widget>
</item>
<item row="3" column="3">
<widget class="MandatoryLineEdit" name="lineEdit_printf_file">
<property name="toolTip"><string>Specify file to dump logs</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="checkBox_profiler">
<property name="toolTip"><string>Enable the Tracy profiler. Profiling may decrease emulator performance</string></property>
<property name="text"><string>Tracy profiler</string></property>
</widget>
</item>
<item row="4" column="2">
<widget class="QLabel" name="label_cmd_dump_folder">
<property name="text"><string>Command buffer folder:</string></property>
</widget>
</item>
<item row="4" column="3">
<widget class="MandatoryLineEdit" name="lineEdit_cmd_dump_folder">
<property name="toolTip"><string>Specify directory to dump command buffers</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="debugSpacer">
<property name="orientation"><enum>Qt::Vertical</enum></property>
<property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item>
<layout class="QHBoxLayout" name="buttonLayout">
<item>
<widget class="QPushButton" name="clear_button">

View File

@ -117,12 +117,14 @@ public:
PresentMode present_mode = PresentMode::Mailbox;
int gpu_index = -1;
bool fullscreen_enabled = false;
bool hdr_enabled = false;
bool hide_cursor_enabled = false;
bool readback_linear_images = false;
bool sync_raw_image_buffers = false;
bool tessellation_enabled = false;
bool trophy_enabled = true;
bool skip_notice_screen = false;
bool network_connected = false;
int vblank_frequency = 60;
int console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@ -152,12 +154,14 @@ public:
present_mode = other.present_mode;
gpu_index = other.gpu_index;
fullscreen_enabled = other.fullscreen_enabled;
hdr_enabled = other.hdr_enabled;
hide_cursor_enabled = other.hide_cursor_enabled;
readback_linear_images = other.readback_linear_images;
sync_raw_image_buffers = other.sync_raw_image_buffers;
tessellation_enabled = other.tessellation_enabled;
trophy_enabled = other.trophy_enabled;
skip_notice_screen = other.skip_notice_screen;
network_connected = other.network_connected;
vblank_frequency = other.vblank_frequency;
console_language = other.console_language;
vulkan_validation_enabled = other.vulkan_validation_enabled;

View File

@ -1,12 +1,21 @@
#ifndef UPDATE_CHECKER_H
#define UPDATE_CHECKER_H
#include "updateRelease.h"
#include <QCryptographicHash>
#include <QFile>
#include <QNetworkAccessManager>
#include <QObject>
#include <QString>
#include <QUrl>
#include <QPointer>
#include <QProcess>
class QByteArray;
#include <functional>
#include <memory>
class QNetworkReply;
class QProgressDialog;
class QTemporaryDir;
class QWidget;
class UpdateChecker final: public QObject {
@ -14,28 +23,43 @@ class UpdateChecker final: public QObject {
public:
explicit UpdateChecker(QWidget* parent);
~UpdateChecker() override;
[[nodiscard]] static bool IsSupported();
void Check(bool manual);
static void ShowPreviousResult(QWidget* parent);
void Check(bool manual);
void SetGameRunning(bool running) { m_game_running = running; }
[[nodiscard]] bool IsInstalling() const { return m_installing; }
signals:
void CheckingChanged(bool checking);
void InstallingChanged(bool installing);
private:
struct UpdateInfo;
void FetchRelease(const QUrl& url, std::function<void(UpdateRelease::Info)> done);
void CheckCurrent(const UpdateRelease::Info& current, bool manual);
void OfferUpdate();
void Download();
void ReadDownload(QNetworkReply* reply);
void PreparePackage();
void RunPreparation(const QString& program, const QStringList& arguments, bool validate);
void Restart();
void Finish(QString error = {}, bool report = true);
static UpdateInfo ParseUpdateInfo(const QByteArray& data);
void FetchUpdateInfo(const char* url, bool fallback, bool manual);
void ShowUpdateResult(const UpdateInfo& info, bool manual);
QWidget* m_parent = nullptr;
QNetworkAccessManager m_network;
bool m_checking_updates = false;
// Answer from the primary update feed, kept while the GitHub fallback feed is
// being consulted, so it can still be shown if the fallback request fails.
QString m_primary_tag;
QUrl m_primary_page_url;
QWidget* m_parent;
QNetworkAccessManager m_network;
QPointer<QNetworkReply> m_reply;
QPointer<QProgressDialog> m_progress;
QProcess m_prepare;
std::unique_ptr<QTemporaryDir> m_workspace;
QFile m_download;
QCryptographicHash m_hash {QCryptographicHash::Sha256};
UpdateRelease::Info m_release;
QString m_download_error;
bool m_busy = false;
bool m_installing = false;
bool m_game_running = false;
bool m_canceled = false;
};
#endif // UPDATE_CHECKER_H

View File

@ -0,0 +1,38 @@
#ifndef UPDATE_RELEASE_H
#define UPDATE_RELEASE_H
#include <QByteArray>
#include <QDateTime>
#include <QString>
#include <QUrl>
namespace UpdateRelease {
inline const QString LATEST_URL =
QStringLiteral("https://api.github.com/repos/KytyPS5/KytyPS5/releases/latest");
inline const QString TAG_URL =
QStringLiteral("https://api.github.com/repos/KytyPS5/KytyPS5/releases/tags/%1");
inline const QString PAGE_URL =
QStringLiteral("https://github.com/KytyPS5/KytyPS5/releases/tag/%1");
inline const QString DOWNLOAD_URL =
QStringLiteral("https://github.com/KytyPS5/KytyPS5/releases/download/%1/%2");
struct Info {
QString tag;
QUrl page_url;
QUrl download_url;
QByteArray sha256;
qint64 size = 0;
QDateTime published_at;
QString error;
};
enum class VersionRelation { Current, Newer, Older, Unknown };
[[nodiscard]] QString PlatformSuffix();
[[nodiscard]] Info Parse(const QByteArray& data, const QString& platform_suffix);
[[nodiscard]] VersionRelation Compare(const Info& latest, const Info& current);
} // namespace UpdateRelease
#endif // UPDATE_RELEASE_H

View File

@ -10,12 +10,14 @@ QVariantMap Configuration::GameSettings() const {
KYTY_CFG_SET(present_mode);
KYTY_CFG_SET(gpu_index);
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(hdr_enabled);
KYTY_CFG_SET(hide_cursor_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(sync_raw_image_buffers);
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(trophy_enabled);
KYTY_CFG_SET(skip_notice_screen);
KYTY_CFG_SET(network_connected);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
@ -55,12 +57,14 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
present_mode = PresentMode::Mailbox;
}
KYTY_CFG_GET(fullscreen_enabled);
KYTY_CFG_GET(hdr_enabled);
KYTY_CFG_GET(hide_cursor_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(sync_raw_image_buffers);
KYTY_CFG_GET(tessellation_enabled);
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
KYTY_CFG_GET(skip_notice_screen);
KYTY_CFG_GET(network_connected);
vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt();
console_language = s.value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {

View File

@ -273,11 +273,13 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
info.gpu_index >= 0 && info.gpu_index < m_ui->comboBox_gpu->count() - 1 ? info.gpu_index + 1
: 0);
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
m_ui->checkBox_hdr->setChecked(info.hdr_enabled);
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen);
m_ui->checkBox_network_connected->setChecked(info.network_connected);
m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->comboBox_console_language->clear();
@ -434,11 +436,13 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
TextToEnum<Configuration::PresentMode>(ui.comboBox_present_mode->currentText());
info.gpu_index = ui.comboBox_gpu->currentIndex() - 1;
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
info.hdr_enabled = ui.checkBox_hdr->isChecked();
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked();
info.network_connected = ui.checkBox_network_connected->isChecked();
info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.console_language = ui.comboBox_console_language->currentIndex();
@ -463,6 +467,14 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
}
void ConfigurationEditDialog::save() {
for (auto* field: {m_ui->lineEdit_shader_log_folder, m_ui->lineEdit_printf_file,
m_ui->lineEdit_cmd_dump_folder}) {
if (field->isEnabled() && field->text().isEmpty()) {
m_ui->settings_tabs->setCurrentWidget(m_ui->debug_tab);
field->setFocus();
break;
}
}
if (MandatoryLineEdit::FindEmpty(this)) {
QMessageBox::critical(this, tr("Save failed"), tr("Please fill all mandatory fields"));
return;

View File

@ -136,6 +136,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
[this](const QString&) { m_update_checker->Check(true); });
connect(m_update_checker, &UpdateChecker::CheckingChanged, m_ui->check_updates_link,
&QLabel::setDisabled);
connect(m_update_checker, &UpdateChecker::InstallingChanged, this, &MainDialogPrivate::Update);
connect(m_ui->check_updates_on_startup, &QCheckBox::toggled, this, [this](bool checked) {
g_check_updates_on_startup = checked;
m_ui->widget->WriteSettings();
@ -146,6 +147,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
});
connect(&m_process, &QProcess::stateChanged, this, [this](QProcess::ProcessState state) {
m_update_checker->SetGameRunning(state != QProcess::NotRunning);
if (state == QProcess::NotRunning) {
if (m_running_item != nullptr) {
m_running_item->SetRunning(false);
@ -162,6 +164,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
}
void MainDialogPrivate::FindInterpreter() {
UpdateChecker::ShowPreviousResult(m_main_dialog);
QDir search_dir(QApplication::applicationDirPath());
m_interpreter = search_dir.absoluteFilePath(EMULATOR_EXE);
@ -242,6 +245,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
if (info.fullscreen_enabled) {
args << "--fullscreen";
}
if (info.hdr_enabled) {
args << "--hdr";
}
if (info.hide_cursor_enabled) {
args << "--hide-cursor";
}
@ -249,6 +255,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--sync-raw-image-buffers" << BoolArg(info.sync_raw_image_buffers);
args << "--trophy-notifications" << BoolArg(info.trophy_enabled);
args << "--skip-notice-screen" << BoolArg(info.skip_notice_screen);
args << "--network-connected" << BoolArg(info.network_connected);
if (info.tessellation_enabled) {
args << "--tessellation";
}
@ -509,6 +516,9 @@ void MainDialogPrivate::ReadSettings(QSettings& s) {
}
void MainDialogPrivate::Run() {
if (m_update_checker->IsInstalling()) {
return;
}
m_running_item = m_ui->widget->GetSelectedItem();
if (m_running_item == nullptr) {
return;
@ -526,7 +536,8 @@ void MainDialogPrivate::Run() {
void MainDialogPrivate::Update() {
const auto* item = m_ui->widget->GetSelectedItem();
bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr);
bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr &&
!m_update_checker->IsInstalling());
if (run_enabled) {
const auto& info = item->GetInfo();

View File

@ -2,143 +2,418 @@
#include "kytyGitVersion.h"
#include <QByteArray>
#include <QAbstractButton>
#include <QApplication>
#include <QDebug>
#include <QDesktopServices>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDir>
#include <QFileInfo>
#include <QMessageBox>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QString>
#include <QUrl>
#include <QWidget>
#include <QProcessEnvironment>
#include <QProgressDialog>
#include <QTemporaryDir>
namespace {
constexpr char DEFAULT_FEED_URL[] = "https://kytyps5.github.io/data/updates.json";
constexpr char FALLBACK_FEED_URL[] =
"https://api.github.com/repos/KytyPS5/KytyPS5/releases/latest";
constexpr qint64 MAX_METADATA_SIZE = 2 * 1024 * 1024;
QString LauncherRelativePath() {
#if defined(_WIN32)
return QStringLiteral("launcher.exe");
#elif defined(__APPLE__)
return QStringLiteral("KytyPS5.app/Contents/MacOS/KytyPS5");
#else
return QStringLiteral("launcher");
#endif
}
QString InstallDirectory() {
QDir dir(QCoreApplication::applicationDirPath());
#if defined(__APPLE__)
// Official macOS releases are a bundle; never update a flat development build.
if (!dir.cdUp() || !dir.cdUp() || dir.dirName() != QStringLiteral("KytyPS5.app") ||
!dir.cdUp()) {
return {};
}
#endif
return dir.absolutePath();
}
QString HelperName() {
#if defined(_WIN32)
return QStringLiteral("kyty_updater.exe");
#else
return QStringLiteral("kyty_updater");
#endif
}
QNetworkRequest Request(const QUrl& url) {
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
request.setRawHeader("User-Agent", "KytyPS5-Updater");
request.setTransferTimeout(30000);
return request;
}
QString TarProgram() {
#if defined(_WIN32)
// Use the OS tool, not a tar.exe from the download or current directory.
return QDir(qEnvironmentVariable("SystemRoot", QStringLiteral("C:/Windows")))
.filePath(QStringLiteral("System32/tar.exe"));
#else
return QStringLiteral("/usr/bin/tar");
#endif
}
} // namespace
struct UpdateChecker::UpdateInfo {
QString tag;
QUrl page_url;
QString error;
};
UpdateChecker::UpdateChecker(QWidget* parent): QObject(parent), m_parent(parent), m_network(this) {}
UpdateChecker::~UpdateChecker() {
if (m_reply) {
m_reply->disconnect(this);
m_reply->abort();
}
// Extraction must stop before QTemporaryDir removes its output.
m_prepare.disconnect(this);
if (m_prepare.state() != QProcess::NotRunning) {
m_prepare.kill();
m_prepare.waitForFinished();
}
}
bool UpdateChecker::IsSupported() {
#if defined(KYTY_OFFICIAL_BUILD) && defined(NDEBUG)
return !QString::fromLatin1(KYTY_GIT_HASH).endsWith(QStringLiteral("-dirty"));
return !QString::fromLatin1(KYTY_GIT_HASH).endsWith(QStringLiteral("-dirty")) &&
!UpdateRelease::PlatformSuffix().isEmpty();
#else
return false;
#endif
}
UpdateChecker::UpdateInfo UpdateChecker::ParseUpdateInfo(const QByteArray& data) {
const auto document = QJsonDocument::fromJson(data);
if (!document.isObject()) {
return {{}, {}, tr("Invalid update feed")};
}
const auto root = document.object();
UpdateInfo info {root.value(QStringLiteral("tag")).toString(),
QUrl(root.value(QStringLiteral("html_url")).toString()), {}};
if (info.tag.isEmpty()) {
info.tag = root.value(QStringLiteral("tag_name")).toString();
}
if (info.tag.isEmpty() || !info.page_url.isValid() ||
info.page_url.scheme() != QStringLiteral("https")) {
info.error = tr("Incomplete update feed");
}
return info;
}
void UpdateChecker::Check(bool manual) {
if (!IsSupported() || m_checking_updates) {
void UpdateChecker::ShowPreviousResult(QWidget* parent) {
const auto directory = InstallDirectory();
QFile result(QDir(directory).filePath(QStringLiteral(".kyty-update-result")));
if (directory.isEmpty() || !result.open(QIODevice::ReadOnly)) {
return;
}
m_checking_updates = true;
emit CheckingChanged(true);
FetchUpdateInfo(DEFAULT_FEED_URL, false, manual);
const auto status = result.readLine().trimmed();
auto message = QString::fromUtf8(result.read(64 * 1024)).trimmed();
result.close();
result.remove();
if (status != "success" && status != "failure") {
return;
}
const bool success = status == "success";
if (message.isEmpty()) {
message = success ? tr("The update was installed successfully.")
: tr("The update could not be installed.");
}
QMessageBox(success ? QMessageBox::Information : QMessageBox::Warning, tr("KytyPS5 Update"),
message, QMessageBox::Ok, parent)
.exec();
}
void UpdateChecker::FetchUpdateInfo(const char* url, bool fallback, bool manual) {
QNetworkRequest request(QUrl(QString::fromLatin1(url)));
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
request.setRawHeader("User-Agent", "Kyty-Launcher");
void UpdateChecker::FetchRelease(const QUrl& url, std::function<void(UpdateRelease::Info)> done) {
auto request = Request(url);
request.setRawHeader("Accept", "application/vnd.github+json");
request.setRawHeader("X-GitHub-Api-Version", "2022-11-28");
request.setTransferTimeout(15000);
auto* reply = m_network.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply, fallback, manual]() {
UpdateInfo info;
if (reply->error() == QNetworkReply::NoError) {
info = ParseUpdateInfo(reply->readAll());
} else {
m_reply = reply;
reply->setReadBufferSize(MAX_METADATA_SIZE + 1);
connect(reply, &QNetworkReply::readyRead, this, [reply]() {
if (reply->bytesAvailable() > MAX_METADATA_SIZE) {
reply->abort();
}
});
connect(reply, &QNetworkReply::finished, this, [this, reply, done = std::move(done)]() {
UpdateRelease::Info info;
if (reply->bytesAvailable() > MAX_METADATA_SIZE) {
info.error = tr("The update response is too large.");
} else if (reply->error() != QNetworkReply::NoError) {
info.error = reply->errorString();
} else {
info = UpdateRelease::Parse(reply->readAll(), UpdateRelease::PlatformSuffix());
}
m_reply = nullptr;
reply->deleteLater();
if (!fallback) {
const bool up_to_date =
info.error.isEmpty() && info.tag == QString::fromLatin1(KYTY_RELEASE_TAG);
if (!up_to_date) {
if (info.error.isEmpty()) {
// The primary feed is reachable but disagrees with the build tag
// (e.g. a stale mirror); remember its answer in case the fallback
// request also fails.
m_primary_tag = info.tag;
m_primary_page_url = info.page_url;
}
FetchUpdateInfo(FALLBACK_FEED_URL, true, manual);
return;
}
}
if (fallback && !info.error.isEmpty() && !m_primary_tag.isEmpty()) {
// The fallback feed failed, but the primary feed already returned a
// usable answer; prefer it over reporting a network error.
info = {m_primary_tag, m_primary_page_url, {}};
}
m_primary_tag.clear();
m_primary_page_url.clear();
m_checking_updates = false;
emit CheckingChanged(false);
ShowUpdateResult(info, manual);
done(info);
});
}
void UpdateChecker::ShowUpdateResult(const UpdateInfo& info, bool manual) {
if (!info.error.isEmpty()) {
qWarning() << "Could not check for updates:" << info.error;
if (manual) {
QMessageBox::warning(m_parent, tr("Update Check"),
tr("Could not check for updates:\n%1").arg(info.error));
}
void UpdateChecker::Check(bool manual) {
if (!IsSupported() || m_busy) {
return;
}
m_busy = true;
emit CheckingChanged(true);
FetchRelease(QUrl(UpdateRelease::LATEST_URL), [this, manual](UpdateRelease::Info latest) {
m_release = std::move(latest);
if (!m_release.published_at.isValid()) {
Finish(m_release.error, manual);
} else if (m_release.tag == QString::fromLatin1(KYTY_RELEASE_TAG)) {
CheckCurrent(m_release, manual);
} else {
// Date/hash tags cannot order releases published on the same day.
const auto tag = QUrl::toPercentEncoding(QString::fromLatin1(KYTY_RELEASE_TAG));
FetchRelease(QUrl(UpdateRelease::TAG_URL.arg(QString::fromLatin1(tag))),
[this, manual](const UpdateRelease::Info& current) {
CheckCurrent(current, manual);
});
}
});
}
const bool current = info.tag == QString::fromLatin1(KYTY_RELEASE_TAG);
if (current) {
if (manual) {
QMessageBox::information(m_parent, tr("Update Check"),
tr("You are using the latest version (%1).").arg(info.tag));
void UpdateChecker::CheckCurrent(const UpdateRelease::Info& current, bool manual) {
if (current.tag != QString::fromLatin1(KYTY_RELEASE_TAG) || !current.published_at.isValid()) {
Finish(tr("Could not verify the currently installed release. Please try again later."),
manual);
return;
}
const auto relation = UpdateRelease::Compare(m_release, current);
if (relation == UpdateRelease::VersionRelation::Newer) {
if (m_release.error.isEmpty()) {
OfferUpdate();
} else {
Finish(m_release.error, manual);
}
return;
}
const auto message =
tr("An update is available.\n\nCurrent: %1\nLatest: %2\n\n"
"Open the release page?")
.arg(QString::fromLatin1(KYTY_RELEASE_TAG), info.tag);
if (QMessageBox::question(m_parent, tr("KytyPS5 Update"), message,
QMessageBox::Open | QMessageBox::Cancel,
QMessageBox::Open) == QMessageBox::Open) {
QDesktopServices::openUrl(info.page_url);
if (relation == UpdateRelease::VersionRelation::Unknown) {
Finish(tr("Could not determine whether the available release is newer."), manual);
return;
}
Finish();
if (manual) {
QMessageBox::information(m_parent, tr("Update Check"),
tr("No newer release is available."));
}
}
void UpdateChecker::OfferUpdate() {
QMessageBox question(QMessageBox::Information, tr("KytyPS5 Update"),
tr("Version %1 is available.\n\nCurrent: %2\n\n"
"Download and install it now? The launcher will restart. Your settings "
"and games are kept.")
.arg(m_release.tag, QString::fromLatin1(KYTY_RELEASE_TAG)),
QMessageBox::Yes | QMessageBox::Open | QMessageBox::Cancel, m_parent);
question.button(QMessageBox::Yes)->setText(tr("Install Update"));
question.button(QMessageBox::Open)->setText(tr("Release Notes"));
question.setDefaultButton(QMessageBox::Yes);
const auto answer = question.exec();
if (answer == QMessageBox::Yes) {
Download();
return;
}
if (answer == QMessageBox::Open) {
QDesktopServices::openUrl(m_release.page_url);
}
Finish();
}
void UpdateChecker::Download() {
if (m_game_running) {
Finish(tr("Close the running game before installing an update."));
return;
}
const auto directory = InstallDirectory();
const auto helper = QDir(QCoreApplication::applicationDirPath()).filePath(HelperName());
if (directory.isEmpty() || !QFileInfo::exists(helper) || !QFileInfo::exists(TarProgram())) {
Finish(tr("This installation cannot update itself. Download the release from GitHub and "
"extract it manually."));
return;
}
m_workspace = std::make_unique<QTemporaryDir>(
QDir(directory).filePath(QStringLiteral(".kyty-update-XXXXXX")));
if (!m_workspace->isValid()) {
Finish(tr("The installation folder is not writable. Move KytyPS5 to a writable folder and "
"try again."));
return;
}
if (!QFile::copy(helper, m_workspace->filePath(HelperName())) ||
!QDir(m_workspace->path()).mkdir(QStringLiteral("payload"))) {
Finish(tr("Could not prepare the update folder."));
return;
}
m_download.setFileName(
m_workspace->filePath(QStringLiteral("package") + UpdateRelease::PlatformSuffix()));
if (!m_download.open(QIODevice::WriteOnly)) {
Finish(m_download.errorString());
return;
}
m_hash.reset();
m_download_error.clear();
m_canceled = false;
m_installing = true;
emit InstallingChanged(true);
m_progress = new QProgressDialog(tr("Downloading %1…").arg(m_release.tag), tr("Cancel"), 0, 100,
m_parent);
m_progress->setWindowTitle(tr("KytyPS5 Update"));
m_progress->setWindowModality(Qt::ApplicationModal);
m_progress->setAutoClose(false);
m_progress->setAutoReset(false);
m_progress->setMinimumDuration(0);
connect(m_progress, &QProgressDialog::canceled, this, [this]() {
m_canceled = true;
if (m_reply) {
m_reply->abort();
} else if (m_prepare.state() != QProcess::NotRunning) {
m_prepare.kill();
}
});
auto* reply = m_network.get(Request(m_release.download_url));
m_reply = reply;
reply->setReadBufferSize(1024 * 1024);
connect(reply, &QNetworkReply::readyRead, this, [this, reply]() { ReadDownload(reply); });
connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64) {
if (m_progress) {
m_progress->setValue(
static_cast<int>(qMin<qint64>(99, received * 100 / m_release.size)));
}
});
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
ReadDownload(reply);
const auto error = reply->error();
const auto error_text = reply->errorString();
m_reply = nullptr;
reply->deleteLater();
const auto size = m_download.size();
const bool flushed = m_download.flush();
m_download.close();
if (m_canceled) {
Finish();
} else if (!m_download_error.isEmpty()) {
Finish(m_download_error);
} else if (error != QNetworkReply::NoError) {
Finish(error_text);
} else if (!flushed || size != m_release.size || m_hash.result() != m_release.sha256) {
Finish(tr("The update failed its size or SHA-256 verification. Please try again."));
} else {
PreparePackage();
}
});
}
void UpdateChecker::ReadDownload(QNetworkReply* reply) {
const auto data = reply->readAll();
if (data.isEmpty() || m_canceled || !m_download_error.isEmpty()) {
return;
}
if (m_download.size() + data.size() > m_release.size) {
m_download_error = tr("The downloaded file is larger than the release asset.");
} else if (m_download.write(data) != data.size()) {
m_download_error = m_download.errorString();
} else {
m_hash.addData(data);
}
if (!m_download_error.isEmpty() && !reply->isFinished()) {
reply->abort();
}
}
void UpdateChecker::PreparePackage() {
m_progress->setLabelText(tr("Preparing the update…"));
m_progress->setRange(0, 0);
RunPreparation(TarProgram(),
{QStringLiteral("-xf"), m_download.fileName(), QStringLiteral("-C"),
m_workspace->filePath(QStringLiteral("payload")),
QStringLiteral("--no-same-owner"), QStringLiteral("--no-same-permissions")},
false);
}
void UpdateChecker::RunPreparation(const QString& program, const QStringList& arguments,
bool validate) {
m_prepare.disconnect(this);
m_prepare.setProgram(program);
m_prepare.setArguments(arguments);
m_prepare.setWorkingDirectory(m_workspace->path());
auto environment = QProcessEnvironment::systemEnvironment();
environment.remove(QStringLiteral("TAR_OPTIONS"));
m_prepare.setProcessEnvironment(environment);
connect(&m_prepare, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
if (error == QProcess::FailedToStart) {
Finish(m_prepare.errorString());
}
});
connect(&m_prepare, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
[this, validate](int exit_code, QProcess::ExitStatus status) {
if (m_canceled) {
Finish();
} else if (exit_code != 0 || status != QProcess::NormalExit) {
Finish(
tr("Could not prepare the update:\n%1")
.arg(QString::fromUtf8(m_prepare.readAllStandardError()).left(4096)));
} else if (!validate) {
RunPreparation(m_workspace->filePath(HelperName()),
{QStringLiteral("--validate"),
m_workspace->filePath(QStringLiteral("payload")),
InstallDirectory(), LauncherRelativePath()},
true);
} else {
Restart();
}
});
m_prepare.start();
}
void UpdateChecker::Restart() {
if (m_game_running || m_canceled) {
Finish(tr("The update was canceled. Close any running game before trying again."));
return;
}
m_progress->setLabelText(tr("Restarting to install the update…"));
m_progress->setCancelButton(nullptr);
QProcess helper;
helper.setProgram(m_workspace->filePath(HelperName()));
helper.setArguments({QStringLiteral("--install"),
m_workspace->filePath(QStringLiteral("payload")), InstallDirectory(),
LauncherRelativePath(),
QString::number(QCoreApplication::applicationPid()), QDir::currentPath()});
helper.setWorkingDirectory(m_workspace->path());
helper.setStandardOutputFile(m_workspace->filePath(QStringLiteral("helper.log")));
helper.setStandardErrorFile(m_workspace->filePath(QStringLiteral("helper.log")),
QIODevice::Append);
if (!helper.startDetached()) {
Finish(tr("Could not start the update installer."));
return;
}
m_workspace->setAutoRemove(false);
QApplication::quit();
}
void UpdateChecker::Finish(QString error, bool report) {
const auto release_page = m_release.page_url;
m_release = {};
m_download.close();
m_workspace.reset();
if (m_progress) {
m_progress->disconnect(this);
m_progress->hide();
m_progress->deleteLater();
m_progress = nullptr;
}
m_busy = false;
m_installing = false;
emit CheckingChanged(false);
emit InstallingChanged(false);
if (error.isEmpty()) {
return;
}
qWarning() << "KytyPS5 updater:" << error;
if (!report) {
return;
}
QMessageBox message(QMessageBox::Warning, tr("KytyPS5 Update"), error, QMessageBox::Close,
m_parent);
if (!release_page.isEmpty()) {
message.addButton(QMessageBox::Open);
message.button(QMessageBox::Open)->setText(tr("Open Release Page"));
}
if (message.exec() == QMessageBox::Open) {
QDesktopServices::openUrl(release_page);
}
}

View File

@ -0,0 +1,89 @@
#include "updateRelease.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QRegularExpression>
namespace UpdateRelease {
QString PlatformSuffix() {
#if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__))
return QStringLiteral("-Windows-x64.zip");
#elif defined(__linux__) && defined(__x86_64__)
return QStringLiteral("-Linux-x86_64.tar.gz");
#elif defined(__APPLE__) && defined(__x86_64__)
return QStringLiteral("-macOS-x86_64.zip");
#else
return {};
#endif
}
Info Parse(const QByteArray& data, const QString& platform_suffix) {
Info info;
const auto document = QJsonDocument::fromJson(data);
if (!document.isObject()) {
info.error = QObject::tr("Invalid GitHub release response.");
return info;
}
const auto root = document.object();
info.tag = root.value(QStringLiteral("tag_name")).toString();
const auto published_at =
QDateTime::fromString(root.value(QStringLiteral("published_at")).toString(), Qt::ISODate);
if (info.tag.isEmpty() || !published_at.isValid() ||
root.value(QStringLiteral("draft")).toBool() ||
root.value(QStringLiteral("prerelease")).toBool()) {
info.error = QObject::tr("The release is not a published stable release.");
return info;
}
info.published_at = published_at;
const auto encoded_tag = QString::fromLatin1(QUrl::toPercentEncoding(info.tag));
info.page_url = QUrl(PAGE_URL.arg(encoded_tag));
if (platform_suffix.isEmpty()) {
info.error = QObject::tr("Built-in updates are unavailable for this platform.");
return info;
}
const auto asset_name = info.tag + platform_suffix;
for (const auto& value: root.value(QStringLiteral("assets")).toArray()) {
const auto asset = value.toObject();
if (asset.value(QStringLiteral("name")).toString() != asset_name) {
continue;
}
const auto digest = asset.value(QStringLiteral("digest")).toString();
const auto size = asset.value(QStringLiteral("size")).toInteger();
static const QRegularExpression digest_pattern(
QStringLiteral("\\Asha256:[A-Fa-f0-9]{64}\\z"));
if (!digest_pattern.match(digest).hasMatch() || size <= 0) {
info.error = QObject::tr("The update package size or SHA-256 checksum is invalid.");
return info;
}
const auto encoded_asset = QString::fromLatin1(QUrl::toPercentEncoding(asset_name));
info.download_url = QUrl(DOWNLOAD_URL.arg(encoded_tag, encoded_asset));
info.sha256 = QByteArray::fromHex(digest.mid(7).toLatin1());
info.size = size;
return info;
}
info.error = QObject::tr("This release has no update package for your platform.");
return info;
}
VersionRelation Compare(const Info& latest, const Info& current) {
if (latest.tag.isEmpty() || current.tag.isEmpty()) {
return VersionRelation::Unknown;
}
if (latest.tag == current.tag) {
return VersionRelation::Current;
}
if (!latest.published_at.isValid() || !current.published_at.isValid() ||
latest.published_at == current.published_at) {
return VersionRelation::Unknown;
}
return latest.published_at > current.published_at ? VersionRelation::Newer
: VersionRelation::Older;
}
} // namespace UpdateRelease

View File

@ -57,7 +57,6 @@ struct DriverDmem {
};
static DriverDmem g_driver_dmem;
static int InitializeDmem();
static void ReleaseDmem();
} // namespace Gen5Driver
@ -82,6 +81,8 @@ void Initialize() {
auto& video_out = VideoOut::VideoOutInit(width, height, presenter);
g_renderer = &presenter.Renderer();
g_renderer->InitializeGpu(&video_out);
// Native driver module startup reserves its DMEM before the guest can allocate it.
EXIT_IF(Gen5Driver::Initialize() != OK);
ShaderInit();
}
@ -258,29 +259,6 @@ struct CommandBuffer {
void* user_data;
uint32_t reserved_dw;
void DbgDump() const {
if (!Config::GraphicsDebugDumpEnabled() ||
Config::GetPrintfDirection() == Config::LogDirection::Silent) {
return;
}
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) >= 64) {
return;
}
LOGF("\t bottom = 0x%016" PRIx64 "\n"
"\t top = 0x%016" PRIx64 "\n"
"\t cursor_up = 0x%016" PRIx64 "\n"
"\t cursor_down = 0x%016" PRIx64 "\n"
"\t callback = 0x%016" PRIx64 "\n"
"\t user_data = 0x%016" PRIx64 "\n"
"\t reserved_dw = %" PRIu32 "\n",
reinterpret_cast<uint64_t>(bottom), reinterpret_cast<uint64_t>(top),
reinterpret_cast<uint64_t>(cursor_up), reinterpret_cast<uint64_t>(cursor_down),
reinterpret_cast<uint64_t>(callback), reinterpret_cast<uint64_t>(user_data),
reserved_dw);
}
[[nodiscard]] KYTY_SYSV_ABI uint64_t GetAvailableSizeDW() const {
// Interpret the signed cursor distance as an unsigned 64-bit DWORD count.
const auto distance = static_cast<int64_t>(reinterpret_cast<uintptr_t>(cursor_down) -
@ -301,7 +279,6 @@ struct CommandBuffer {
"\t command buffer exhausted and has no grow callback: requested = %" PRIu32
", remaining = %" PRIu64 ", reserved_dw = %" PRIu32 "\n",
num_dw, remaining, reserved_dw);
DbgDump();
return false;
}
@ -311,7 +288,6 @@ struct CommandBuffer {
"\t command buffer grow callback failed: requested = %" PRIu32
", remaining = %" PRIu64 ", reserved_dw = %" PRIu32 "\n",
num_dw, remaining, reserved_dw);
DbgDump();
return false;
}
if (GetAvailableSizeDW() < num_dw) {
@ -320,7 +296,6 @@ struct CommandBuffer {
"requested = %" PRIu32 ", remaining = %" PRIu64
", reserved_dw = %" PRIu32 "\n",
num_dw, GetAvailableSizeDW(), reserved_dw);
DbgDump();
return false;
}
}
@ -356,38 +331,6 @@ int KYTY_SYSV_ABI AgcInit(uint32_t* state, uint32_t ver) {
}
printf("version = %u\n", ver);
std::scoped_lock lock {Gen5Driver::g_driver_dmem.mutex};
if (Gen5Driver::g_driver_dmem.physical_offset >= 0) {
return OK;
}
if (const auto result = Gen5Driver::InitializeDmem(); result != OK) {
return result;
}
struct FunctionShaderBindings {
uint64_t code;
uint32_t user_data[2];
};
static_assert(sizeof(FunctionShaderBindings) == 16);
// Ordered-count queue validator, including its return to the caller.
static constexpr std::array<uint32_t, 16> validator {
0xbeeb03ff, 0x00000021, 0xb96a1818, 0xbf06106a,
0xbf800000, 0x8590807e, 0xb96a0a18, 0xbf066a80,
0xbf800000, 0x85ea807e, 0x88ea106a, 0xbf870003,
0xbefc03ff, 0x8a6ca000, 0xbf920009, 0xbefd210e,
};
const auto base = Gen5Driver::DriverDmemBase + Gen5Driver::AgcDmemOffset;
const auto function_table = base + 0x60;
const auto code_address = base + 0x100; // Shader code requires 256-byte alignment.
std::array<FunctionShaderBindings, 8> functions {};
functions[2].code = code_address;
const std::array<uint32_t, 4> descriptor {
static_cast<uint32_t>(function_table),
static_cast<uint32_t>(function_table >> 32u) | 0x00100000u,
static_cast<uint32_t>(functions.size()), 0x5204u,
};
std::memcpy(reinterpret_cast<void*>(function_table), functions.data(), sizeof(functions));
std::memcpy(reinterpret_cast<void*>(code_address), validator.data(), sizeof(validator));
std::memcpy(reinterpret_cast<void*>(base), descriptor.data(), sizeof(descriptor));
return OK;
}
@ -1629,8 +1572,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbContextStateOp(CommandBuffer* buf, uint32_t operat
return nullptr;
}
buf->DbgDump();
uint32_t* first = nullptr;
const auto append = [&](uint32_t segment_size_dw) {
auto* segment = buf->AllocateDW(segment_size_dw);
@ -1933,8 +1874,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbSetShRegisterRangeDirect(CommandBuffer* buf, uint32
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(num_values + 2);
if (cmd == nullptr) {
@ -1976,8 +1915,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbSetShRegistersDirect(CommandBuffer*
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED(regs == nullptr);
buf->DbgDump();
std::vector<ShaderRegister> local_regs(num_regs);
for (uint32_t i = 0; i < num_regs; i++) {
local_regs[i].offset = regs[i].offset;
@ -2043,8 +1980,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbSetUcRegistersDirect(CommandBuffer*
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED(regs == nullptr);
buf->DbgDump();
// Original implementation stages register values in a temporary DWORD array. It reads each new run only
// after the preceding run has been allocated, so preserve that ordering around grow callbacks.
std::vector<uint32_t> values(num_regs);
@ -2135,8 +2070,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbReleaseMem(CommandBuffer* buf, uint8_t action, uint
data_sel != 5);
EXIT_NOT_IMPLEMENTED(interrupt > 4);
buf->DbgDump();
auto* cmd = buf->AllocateDW(8);
if (cmd == nullptr) {
@ -2189,8 +2122,6 @@ uint32_t* KYTY_SYSV_ABI AgcAcbResetQueue(CommandBuffer* buf, uint32_t op) {
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED((op & ~0x1c2u) != 0);
buf->DbgDump();
auto* cmd = buf->AllocateDW(2);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2211,8 +2142,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbResetQueue(CommandBuffer* buf, uint32_t op, uint32
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED((op & ~0xfffu) != 0);
buf->DbgDump();
auto* cmd = buf->AllocateDW(2);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2234,8 +2163,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbWaitUntilSafeForRendering(CommandBuffer* buf,
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(7);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2288,8 +2215,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t st
}
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(WORKLOAD_ACTIVE_PACKET_SIZE_DW);
if (cmd == nullptr) {
return nullptr;
@ -2325,8 +2250,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t s
}
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(WORKLOAD_COMPLETE_PACKET_SIZE_DW);
if (cmd == nullptr) {
return nullptr;
@ -2350,8 +2273,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegisterDirect(CommandBuffer* buf, ShaderRegi
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
if (cmd == nullptr) {
@ -2371,8 +2292,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegisterDirect(CommandBuffer* buf, ShaderRegi
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
if (cmd == nullptr) {
@ -2398,8 +2317,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegisterDirect(CommandBuffer* buf, ShaderRegi
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
if (cmd == nullptr) {
@ -2425,8 +2342,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirect(CommandBuffer*
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2454,8 +2369,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer*
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2484,8 +2397,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegistersIndirect(CommandBuffer*
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2506,8 +2417,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetIndexSize(CommandBuffer* buf, uint8_t index_siz
index_size, cache_policy);
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2527,8 +2436,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetIndexBuffer(CommandBuffer* buf, uint64_t index_
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED((index_addr & 1u) != 0);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2547,8 +2454,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetIndexCount(CommandBuffer* buf, uint32_t index_c
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(2);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2568,8 +2473,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetNumInstances(CommandBuffer* buf, uint32_t num_i
return nullptr;
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(2);
if (cmd == nullptr) {
@ -2666,8 +2569,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndex(CommandBuffer* buf, uint32_t index_count
EXIT_NOT_IMPLEMENTED(index_addr == nullptr);
EXIT_NOT_IMPLEMENTED((reinterpret_cast<uint64_t>(index_addr) & 1u) != 0);
buf->DbgDump();
auto* cmd = buf->AllocateDW(6);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2709,8 +2610,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexMultiInstanced(CommandBuffer* buf, uint32
EXIT_NOT_IMPLEMENTED(object_ids == nullptr);
EXIT_NOT_IMPLEMENTED((reinterpret_cast<uint64_t>(index_addr) & 1u) != 0);
buf->DbgDump();
auto* cmd = buf->AllocateDW(9);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2778,8 +2677,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexAuto(CommandBuffer* buf, uint32_t index_c
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2808,8 +2705,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexOffset(CommandBuffer* buf, uint32_t index
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(5);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2839,8 +2734,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetBaseIndirectArgs(CommandBuffer* buf, uint32_t s
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(4);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2865,8 +2758,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexIndirect(CommandBuffer* buf, uint32_t dat
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(5);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2894,8 +2785,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndirect(CommandBuffer* buf, uint32_t data_off
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(5);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -2932,8 +2821,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndirectMulti(CommandBuffer* buf,
return nullptr;
}
buf->DbgDump();
constexpr uint32_t draw_packet_size_dw = 10u;
constexpr uint32_t total_size_dw =
AGC_INTERNAL_DATA_PACKET_SIZE_DW + draw_packet_size_dw +
@ -2994,8 +2881,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexIndirectMulti(CommandBuffer* buf,
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED((count_indirect & ~1u) != 0);
buf->DbgDump();
auto* cmd = buf->AllocateDW(10);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -3044,8 +2929,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDispatchIndirect(CommandBuffer* buf, uint32_t data
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -3077,8 +2960,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbEventWrite(CommandBuffer* buf, uint8_t event_type,
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED(event_type > 0x3fu);
buf->DbgDump();
const bool addressed_event = ((event_type & 0xfeu) == 0x38u);
auto* cmd = buf->AllocateDW(addressed_event ? 4u : 2u);
@ -3174,8 +3055,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbAcquireMem(CommandBuffer* buf, uint8_t engine, uin
}
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(8);
if (cmd == nullptr) {
@ -3234,8 +3113,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbCondExec(CommandBuffer* buf, const volatile uint32
return nullptr;
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(5);
if (cmd == nullptr) {
@ -3407,8 +3284,6 @@ uint32_t* KYTY_SYSV_ABI AgcAcbDispatchIndirect(CommandBuffer* buf,
return nullptr;
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(4);
if (cmd == nullptr) {
@ -3457,8 +3332,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbStallCommandBufferParser(CommandBuffer* buf) {
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(2);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -3745,6 +3618,20 @@ int KYTY_SYSV_ABI AgcSetPacketPredication(uint32_t* packet, uint32_t predication
return OK;
}
int KYTY_SYSV_ABI AgcSetNop(uint32_t* packet) {
PRINT_NAME();
const auto size = AgcGetPacketSize(packet);
packet[0] = (packet[0] & 0xffff0003u) | (Pm4::IT_NOP << 8u);
// Prevent the disabled payload from being treated as an executable marker.
if (size > 1 && (packet[1] & 0xffff0000u) == 0x68750000u) {
packet[1] &= 0xffffu;
}
return OK;
}
int KYTY_SYSV_ABI AgcSetRangePredication(uint32_t* start, const volatile uint32_t* end,
uint32_t predication) {
PRINT_NAME();
@ -3864,8 +3751,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbWriteData(CommandBuffer* buf, uint8_t dst, uint8_t
return nullptr;
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(4 + num_dwords);
if (cmd == nullptr) {
@ -3918,8 +3803,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbGetLodStats(CommandBuffer* buf, uint8_t cache_poli
return nullptr;
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(5);
if (cmd == nullptr) {
@ -4122,8 +4005,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbWaitRegMem(CommandBuffer* buf, uint8_t size, uint8
cache_policy);
}
buf->DbgDump();
auto address_value = reinterpret_cast<uint64_t>(address);
bool wait32 = (size == 0);
auto poll = wait_reg_mem_poll_cycles_to_packet(poll_cycles);
@ -4253,8 +4134,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetFlip(CommandBuffer* buf, uint32_t video_out_han
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(6);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@ -4275,7 +4154,11 @@ namespace Gen5Driver {
LIB_NAME("Graphics5Driver", "Graphics5Driver");
static int InitializeDmem() {
int Initialize() {
std::scoped_lock lock {g_driver_dmem.mutex};
if (g_driver_dmem.physical_offset >= 0) {
return OK;
}
int64_t physical_offset = -1;
auto result = LibKernel::Memory::KernelAllocateDirectMemory(
0, LibKernel::Memory::KernelGetDirectMemorySize(), DriverDmemSize, DriverDmemSize,
@ -4292,6 +4175,31 @@ static int InitializeDmem() {
}
EXIT_IF(reinterpret_cast<uint64_t>(address) != DriverDmemBase);
std::memset(address, 0, DriverDmemSize);
struct FunctionShaderBindings {
uint64_t code;
uint32_t user_data[2];
};
static_assert(sizeof(FunctionShaderBindings) == 16);
// Ordered-count queue validator, including its return to the caller.
static constexpr std::array<uint32_t, 16> validator {
0xbeeb03ff, 0x00000021, 0xb96a1818, 0xbf06106a,
0xbf800000, 0x8590807e, 0xb96a0a18, 0xbf066a80,
0xbf800000, 0x85ea807e, 0x88ea106a, 0xbf870003,
0xbefc03ff, 0x8a6ca000, 0xbf920009, 0xbefd210e,
};
const auto base = DriverDmemBase + AgcDmemOffset;
const auto function_table = base + 0x60;
const auto code_address = base + 0x100; // Shader code requires 256-byte alignment.
std::array<FunctionShaderBindings, 8> functions {};
functions[2].code = code_address;
const std::array<uint32_t, 4> descriptor {
static_cast<uint32_t>(function_table),
static_cast<uint32_t>(function_table >> 32u) | 0x00100000u,
static_cast<uint32_t>(functions.size()), 0x5204u,
};
std::memcpy(reinterpret_cast<void*>(function_table), functions.data(), sizeof(functions));
std::memcpy(reinterpret_cast<void*>(code_address), validator.data(), sizeof(validator));
std::memcpy(reinterpret_cast<void*>(base), descriptor.data(), sizeof(descriptor));
g_driver_dmem.physical_offset = physical_offset;
return OK;
}

View File

@ -270,6 +270,7 @@ uint32_t KYTY_SYSV_ABI AgcGetPacketSize(uint32_t* packet);
int KYTY_SYSV_ABI AgcSetPacketPredication(uint32_t* packet, uint32_t predication);
int KYTY_SYSV_ABI AgcSetRangePredication(uint32_t* start, const volatile uint32_t* end,
uint32_t predication);
int KYTY_SYSV_ABI AgcSetNop(uint32_t* packet);
int KYTY_SYSV_ABI AgcRewindPatchSetRewindState(uint32_t* cmd, uint8_t state);
int KYTY_SYSV_ABI AgcCondExecPatchSetEnd(uint32_t* cmd, const volatile uint32_t* buffer);
int KYTY_SYSV_ABI AgcCondExecPatchSetCommandAddress(uint32_t* cmd,
@ -307,6 +308,8 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetFlip(CommandBuffer* buf, uint32_t video_out_han
namespace Gen5Driver {
int Initialize();
struct Packet {
uint32_t* addr;
uint32_t dw_num;

View File

@ -978,11 +978,7 @@ public:
return false;
}
if (current_video) {
retired_video.push_back(std::move(current_video->buffer));
if (retired_video.size() >= static_cast<size_t>(max_video_buffers - 1)) {
video_buffers.Push(std::move(retired_video.front()));
retired_video.pop_front();
}
video_buffers.Push(std::move(current_video->buffer));
}
current_video = std::move(*frame);
*out = current_video->info;
@ -1060,7 +1056,6 @@ private:
video_buffers.Clear();
audio_buffers.Clear();
if (!retain_output_buffers) {
retired_video.clear();
current_video.reset();
current_audio.reset();
}
@ -1154,7 +1149,7 @@ private:
bool AllocateBuffers() {
if (video_id) {
auto size = VideoBufferSize(fmt->streams[video_id.value()]);
auto retained = retired_video.size() + (current_video ? 1u : 0u);
auto retained = current_video ? 1u : 0u;
for (size_t i = retained; i < static_cast<size_t>(max_video_buffers); i++) {
auto buffer = std::make_unique<GuestBuffer>(mem, 0x100, size, true);
if (!buffer->Valid()) {
@ -1676,7 +1671,6 @@ private:
WorkQueue<ReadyFrame> audio_frames;
std::optional<ReadyFrame> current_video;
std::optional<ReadyFrame> current_audio;
std::deque<std::unique_ptr<GuestBuffer>> retired_video;
LibKernel::Pthread demux_thread = nullptr;
LibKernel::Pthread video_thread = nullptr;
LibKernel::Pthread audio_thread = nullptr;

View File

@ -143,6 +143,7 @@ LIB_DEFINE(InitAgcDriver_1) {
LIB_FUNC("Lkf86B98qPc", Gen5::AgcGetPacketSize);
LIB_FUNC("w6Dj1VJt5qY", Gen5::AgcSetPacketPredication);
LIB_FUNC("n8vgpaQg6dA", Gen5::AgcSetRangePredication);
LIB_FUNC("K2mciNVxUCE", Gen5::AgcSetNop);
LIB_FUNC("i1jyy49AjXU", Gen5::AgcDcbWriteData);
LIB_FUNC("p9tI+yTvx68", Gen5::AgcDcbWriteDataGetSize);
LIB_FUNC("vuSXe69VILM", Gen5::AgcDcbGetLodStats);

View File

@ -868,6 +868,10 @@ static uint64_t AprReadFileRecordSize(uint64_t file_offset) {
return (file_offset >> 32u) != 0 ? READ_FILE_RECORD_SIZE_EXT : READ_FILE_RECORD_SIZE;
}
static uint64_t WriteAddressRecordSize(uint64_t value) {
return value <= 3 ? 8 : value < (uint64_t {1} << 34) ? 12 : 16;
}
static uint64_t AprReadGatherRecordSize(uint64_t file_offset) {
return file_offset > 0x3ffffu ? READ_GATHER_RECORD_SIZE_EXT : READ_GATHER_RECORD_SIZE;
}
@ -1207,7 +1211,7 @@ static int ExecuteCommand(const CommandBufferState::Command& entry) {
const auto& command = *payload;
int result = OK;
if (command.kind == AmmCommandKind::Unmap) {
result = LibKernel::Memory::KernelMunmap(command.va, command.size);
result = LibKernel::Memory::UnmapAmmMemory(command.va, command.size);
} else {
result = ExecuteAmmMapCommand(command);
}
@ -1617,11 +1621,10 @@ static uint64_t KYTY_SYSV_ABI MeasureCommandSizeFixed32(uint64_t, uint64_t, uint
return 0x20;
}
static uint64_t KYTY_SYSV_ABI MeasureCommandSizeWriteAddressOnCompletion(volatile uint64_t*,
uint64_t) {
static uint64_t KYTY_SYSV_ABI MeasureCommandSizeWriteAddress(volatile uint64_t*, uint64_t value) {
PRINT_NAME();
return 0x20;
return WriteAddressRecordSize(value);
}
static uint64_t KYTY_SYSV_ABI
@ -1950,7 +1953,7 @@ static int KYTY_SYSV_ABI CommandBufferWaitOnCounter(void* command_buffer, uint8_
}
static int AppendWriteAddressCommand(void* command_buffer, volatile uint64_t* address,
uint64_t value) {
uint64_t value, uint64_t record_size) {
if (command_buffer == nullptr || address == nullptr ||
(reinterpret_cast<uint64_t>(address) & 7) != 0) {
return LibKernel::KERNEL_ERROR_EINVAL;
@ -1958,7 +1961,7 @@ static int AppendWriteAddressCommand(void* command_buffer, volatile uint64_t* ad
return AppendCommandRecord(
reinterpret_cast<uint64_t>(command_buffer),
CommandBufferState::WriteAddressCommand {reinterpret_cast<uint64_t>(address), value},
0x20)
record_size)
? OK
: LibKernel::KERNEL_ERROR_EBUSY;
}
@ -1967,7 +1970,7 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddress(void* command_buffer, volatil
uint64_t value, uint32_t) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, value);
return AppendWriteAddressCommand(command_buffer, address, value, WriteAddressRecordSize(value));
}
static int KYTY_SYSV_ABI CommandBufferWriteAddressOnCompletion(void* command_buffer,
@ -1975,7 +1978,7 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressOnCompletion(void*
uint64_t value) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, value);
return AppendWriteAddressCommand(command_buffer, address, value, WriteAddressRecordSize(value));
}
static int KYTY_SYSV_ABI CommandBufferWriteCounter(void* command_buffer, uint8_t index,
@ -2007,14 +2010,14 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressFromTimeCounter(void*
uint32_t) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
}
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromTimeCounterOnCompletion(
void* command_buffer, volatile uint64_t* address) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
}
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounter(void* command_buffer,
@ -2022,14 +2025,14 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounter(void*
uint32_t) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
}
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterOnCompletion(
void* command_buffer, volatile uint64_t* address, uint8_t) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
}
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterPair(void* command_buffer,
@ -2037,14 +2040,14 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterPair(void*
uint8_t, uint32_t) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
}
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterPairOnCompletion(
void* command_buffer, volatile uint64_t* address, uint8_t) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
}
static int KYTY_SYSV_ABI AprCommandBufferReadFileGather(void* command_buffer, uint64_t, uint64_t,
@ -2452,14 +2455,14 @@ LIB_DEFINE(InitAmpr_1) {
LIB_FUNC("vWU-odnS+fU", Ampr::MeasureCommandSizeReadFile);
LIB_FUNC("sSAUCCU1dv4", Ampr::MeasureCommandSizeWriteKernelEventQueue);
LIB_FUNC("Zi3dBUjgyXI", Ampr::MeasureCommandSizeWriteKernelEventQueue);
LIB_FUNC("C+IEj+BsAFM", Ampr::MeasureCommandSizeWriteAddressOnCompletion);
LIB_FUNC("C+IEj+BsAFM", Ampr::MeasureCommandSizeWriteAddress);
LIB_FUNC("x7SQEXfeovg", Ampr::MeasureCommandSizeWriteAddressFromTimeCounterOnCompletion);
LIB_FUNC("32AcaTaBPSY", Ampr::MeasureCommandSizeWriteAddressFromCounterOnCompletion);
LIB_FUNC("vxC58+DRk-U", Ampr::MeasureCommandSizeWriteAddressFromCounterPairOnCompletion);
LIB_FUNC("4muPEJ-x5N8", Ampr::MeasureCommandSizeWriteCounterOnCompletion);
LIB_FUNC("0BMj1hgG+kE", Ampr::MeasureCommandSizeFixed32);
LIB_FUNC("ClnsFLLLcss", Ampr::MeasureCommandSizeFixed32);
LIB_FUNC("4fgtGfXDrFc", Ampr::MeasureCommandSizeFixed32);
LIB_FUNC("4fgtGfXDrFc", Ampr::MeasureCommandSizeWriteAddress);
LIB_FUNC("gAtc79UTt5E", Ampr::MeasureCommandSizeFixed32);
LIB_FUNC("JYd9g9L+TmE", Ampr::MeasureCommandSizeFixed32);
LIB_FUNC("2Hw8gjMdwSY", Ampr::MeasureCommandSizeFixed32);

View File

@ -54,11 +54,7 @@ struct CContext {
std::list<atexit_func_t> atexit;
};
struct InitEnvParams {
int argc;
uint32_t pad;
const char* argv[3];
};
using InitEnvParams = Loader::EntryParams;
static int g_argc = 0;
static const char* const* g_argv = nullptr;
@ -271,9 +267,7 @@ static KYTY_SYSV_ABI void init_env(const InitEnvParams* params) {
return;
}
constexpr int argv_capacity = static_cast<int>(sizeof(params->argv) / sizeof(params->argv[0]));
EXIT_NOT_IMPLEMENTED(params->argc < 0 || params->argc >= argv_capacity);
EXIT_NOT_IMPLEMENTED(params->argc < 0);
g_argc = params->argc;
g_argv = params->argv;

View File

@ -3117,6 +3117,7 @@ LIB_DEFINE(InitLibKernel_1_FS) {
LIB_FUNC("mBd4AfLP+u8", FileSystem::KernelPwritev);
LIB_FUNC("eV9wAD2riIA", FileSystem::KernelStat);
LIB_FUNC("kBwCPsYX-m4", FileSystem::KernelFstat);
LIB_FUNC("VW3TVZiM4-E", FileSystem::KernelFtruncate);
LIB_FUNC("AUXVxWeJU-A", FileSystem::KernelUnlink);
LIB_FUNC("52NcYU9+lEo", FileSystem::KernelRename);
LIB_FUNC("taRWhTJFTgE", FileSystem::KernelGetdirentries);

View File

@ -4,6 +4,8 @@
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "emulator.h"
#include "graphics/presentation/systemOverlay.h"
#include "libs/dialog.h"
#include "libs/errno.h"
#include "libs/libs.h"
@ -94,10 +96,15 @@ struct SystemServiceHdrToneMapLuminance {
static int KYTY_SYSV_ABI SystemServiceHideSplashScreen() {
PRINT_NAME();
Graphics::HideSplashScreen();
return OK;
}
int KYTY_SYSV_ABI SystemServiceLoadExec(const char* path, const char* const argv[]) {
PRINT_NAME();
return Emulator::LoadExec(path, argv);
}
static int KYTY_SYSV_ABI SystemServiceParamGetInt(int param_id, int* value) {
PRINT_NAME();
@ -558,6 +565,7 @@ LIB_DEFINE(InitSystemGesture_1) {
} // namespace SystemGesture
LIB_DEFINE(InitSystemService_1) {
LIB_FUNC("JoBqSQt1yyA", SystemService::SystemServiceLoadExec);
LIB_FUNC("Vo5V8KAwCmk", SystemService::SystemServiceHideSplashScreen);
LIB_FUNC("fZo48un7LK4", SystemService::SystemServiceParamGetInt);
LIB_FUNC("SsC-m-S9JTA", SystemService::SystemServiceParamGetString);

459
src/libs/libVdecsw.cpp Normal file
View File

@ -0,0 +1,459 @@
#include "common/abi.h"
#include "libs/libs.h"
#include "libs/videoDec2Decoder.h"
#include "loader/symbolDatabase.h"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <deque>
#include <mutex>
#include <new>
#include <optional>
#include <shared_mutex>
#include <unordered_set>
namespace Libs::Vdecsw {
LIB_VERSION("Vdecsw", 1, "Vdecsw", 1, 1);
namespace Decoder = VideoDec2::Decoder;
enum class Error : uint32_t {
ApiFail = 0x0100,
StructSize,
ArgumentPointer,
DecoderInstance,
MemorySize,
MemoryPointer,
FrameBufferSize,
FrameBufferPointer,
FrameBufferAlignment,
AccessUnitSize = 0x010d,
AccessUnitPointer,
OutputInfo,
ComputeQueue,
InputQueueFull = 0x0113,
OutputBufferFull,
InputQueueEmpty = 0x0116,
DecodePending,
OutputBufferEmpty,
ConfigInfo = 0x0200,
ComputePipeId,
ComputeQueueId,
ResourceType,
CodecType,
InputQueueDepth = 0x0206,
DpbFrameCount = 0x0209,
FrameWidthHeight,
ExtraConfigInfo,
AccessUnit = 0x0301,
OversizeDecode,
};
constexpr int32_t Fail(Error error) {
return static_cast<int32_t>(0x81510000u | static_cast<uint32_t>(error));
}
struct ComputeMemoryInfo {
size_t this_size;
size_t cpu_gpu_memory_size;
void* cpu_gpu_memory;
};
struct ComputeConfigInfo {
size_t this_size;
uint16_t pipe_id;
uint16_t queue_id;
bool check_memory_type;
uint8_t reserved0;
uint16_t reserved1;
};
struct ComputeQueue {
uint16_t pipe_id;
uint16_t queue_id;
uint32_t decoders = 0;
};
struct ConfigInfo {
size_t this_size;
uint32_t resource_type;
uint32_t codec_type;
uint32_t profile;
uint32_t max_level;
int32_t max_width;
int32_t max_height;
int32_t max_dpb_frames;
uint32_t input_queue_depth;
ComputeQueue* compute_queue;
uint64_t cpu_affinity_mask;
int32_t cpu_thread_priority;
bool optimize_progressive;
bool check_memory_type;
uint8_t reserved0;
int8_t extra_dpb_frames;
void* extra_config;
bool disable_sync_input;
uint32_t max_pending_sync;
};
struct MemoryInfo {
size_t this_size;
size_t cpu_memory_size;
void* cpu_memory;
size_t gpu_memory_size;
void* gpu_memory;
size_t cpu_gpu_memory_size;
void* cpu_gpu_memory;
size_t max_frame_buffer_size;
uint32_t frame_buffer_alignment;
uint32_t reserved0;
};
struct InputData {
size_t this_size;
void* au_data;
size_t au_size;
uint64_t pts;
uint64_t dts;
uint64_t attached_data;
};
struct InputResult {
size_t this_size;
void* decoded_au;
uint32_t output_frame_count;
uint32_t reserved0;
};
struct FrameBuffer {
size_t this_size;
void* data;
size_t size;
};
struct OutputInfo {
size_t this_size;
bool valid;
bool last_frame;
bool error_frame;
uint8_t picture_count;
uint32_t codec_type;
uint32_t width;
uint32_t pitch;
uint32_t height;
bool discarded_frame;
void* frame_buffer;
size_t frame_buffer_size;
uint32_t format;
uint32_t pitch_in_bytes;
};
static_assert(sizeof(ComputeMemoryInfo) == 24);
static_assert(sizeof(ComputeConfigInfo) == 16);
static_assert(sizeof(ConfigInfo) == 80);
static_assert(offsetof(ConfigInfo, extra_dpb_frames) == 63);
static_assert(offsetof(ConfigInfo, disable_sync_input) == 72);
static_assert(sizeof(MemoryInfo) == 72);
static_assert(sizeof(InputData) == 48);
static_assert(sizeof(InputResult) == 24);
static_assert(sizeof(FrameBuffer) == 24);
static_assert(sizeof(OutputInfo) == 56);
static_assert(offsetof(OutputInfo, discarded_frame) == 28);
static_assert(offsetof(OutputInfo, frame_buffer) == 32);
struct State;
static std::shared_mutex g_registry_mutex;
static std::unordered_set<ComputeQueue*> g_queues;
static std::unordered_set<State*> g_decoders;
struct State {
ConfigInfo config;
Decoder::Instance* decoder;
std::mutex mutex;
std::deque<InputResult> completed_inputs;
std::optional<FrameBuffer> output_buffer;
~State() {
Decoder::Destroy(decoder);
--config.compute_queue->decoders;
}
};
// The shared registry lock keeps guest-backed state alive until each API call returns.
// Deletion takes the exclusive lock before releasing that memory to the guest.
struct StateAccess {
std::shared_lock<std::shared_mutex> lock {g_registry_mutex};
State* value;
explicit StateAccess(State* handle): value(g_decoders.contains(handle) ? handle : nullptr) {}
explicit operator bool() const { return value != nullptr; }
State* operator->() const { return value; }
};
static int32_t Result(Decoder::Result result) {
switch (result) {
case Decoder::Result::Ok: return 0;
case Decoder::Result::ApiFail: return Fail(Error::ApiFail);
case Decoder::Result::AccessUnit: return Fail(Error::AccessUnit);
case Decoder::Result::FrameBufferSize: return Fail(Error::FrameBufferSize);
case Decoder::Result::OversizeDecode: return Fail(Error::OversizeDecode);
case Decoder::Result::InputFull: return Fail(Error::OutputBufferFull);
case Decoder::Result::NoFrame: return Fail(Error::DecodePending);
}
return Fail(Error::ApiFail);
}
static int32_t ValidateConfig(const ConfigInfo* config) {
if (config == nullptr) return Fail(Error::ArgumentPointer);
if (config->this_size != sizeof(ConfigInfo)) return Fail(Error::StructSize);
if (config->resource_type != 1) return Fail(Error::ResourceType);
if (config->codec_type != 1) return Fail(Error::CodecType);
if (config->reserved0 != 0) return Fail(Error::ConfigInfo);
if (config->extra_config != nullptr) return Fail(Error::ExtraConfigInfo);
if (config->input_queue_depth < 1 || config->input_queue_depth > 8)
return Fail(Error::InputQueueDepth);
if (config->max_dpb_frames < 1 || config->extra_dpb_frames < 0)
return Fail(Error::DpbFrameCount);
if (config->max_width <= 0 || config->max_height <= 0) return Fail(Error::FrameWidthHeight);
return 0;
}
static size_t FrameSize(const ConfigInfo& config) {
// AVC output: 256-byte pitch, 16-line maximum height, and 4096 bytes of metadata.
const auto pitch = (static_cast<size_t>(config.max_width) + 255u) & ~size_t {255};
const auto height = (static_cast<size_t>(config.max_height) + 15u) & ~size_t {15};
return pitch * height * 3 / 2 + 4096;
}
static int32_t KYTY_SYSV_ABI QueryComputeMemoryInfo(ComputeMemoryInfo* memory) {
PRINT_NAME();
if (memory == nullptr) return Fail(Error::ArgumentPointer);
if (memory->this_size != sizeof(ComputeMemoryInfo)) return Fail(Error::StructSize);
memory->cpu_gpu_memory_size = sizeof(ComputeQueue);
return 0;
}
static int32_t KYTY_SYSV_ABI AllocateComputeQueue(const ComputeConfigInfo* config,
const ComputeMemoryInfo* memory,
ComputeQueue** output) {
PRINT_NAME();
if (config == nullptr || memory == nullptr || output == nullptr)
return Fail(Error::ArgumentPointer);
if (config->this_size != sizeof(ComputeConfigInfo) ||
memory->this_size != sizeof(ComputeMemoryInfo))
return Fail(Error::StructSize);
if (config->reserved0 != 0 || config->reserved1 != 0) return Fail(Error::ConfigInfo);
if (config->pipe_id > 4) return Fail(Error::ComputePipeId);
if (config->queue_id > 7) return Fail(Error::ComputeQueueId);
if (memory->cpu_gpu_memory_size < sizeof(ComputeQueue)) return Fail(Error::MemorySize);
if (memory->cpu_gpu_memory == nullptr ||
reinterpret_cast<uintptr_t>(memory->cpu_gpu_memory) % alignof(ComputeQueue) != 0)
return Fail(Error::MemoryPointer);
std::scoped_lock lock(g_registry_mutex);
for (const auto* queue: g_queues)
if ((queue->pipe_id == config->pipe_id && queue->queue_id == config->queue_id) ||
queue == memory->cpu_gpu_memory)
return Fail(Error::ComputeQueue);
auto* queue = new (memory->cpu_gpu_memory) ComputeQueue {config->pipe_id, config->queue_id};
g_queues.insert(queue);
*output = queue;
return 0;
}
static int32_t KYTY_SYSV_ABI ReleaseComputeQueue(ComputeQueue* queue) {
PRINT_NAME();
std::scoped_lock lock(g_registry_mutex);
if (!g_queues.contains(queue) || queue->decoders != 0) return Fail(Error::ComputeQueue);
g_queues.erase(queue);
queue->~ComputeQueue();
return 0;
}
static int32_t KYTY_SYSV_ABI QueryDecoderMemoryInfo(const ConfigInfo* config, MemoryInfo* memory) {
PRINT_NAME();
if (const auto error = ValidateConfig(config); error != 0) return error;
if (memory == nullptr) return Fail(Error::ArgumentPointer);
if (memory->this_size != sizeof(MemoryInfo)) return Fail(Error::StructSize);
memory->cpu_memory_size = sizeof(State);
// The FFmpeg backend needs no guest GPU workspace.
memory->gpu_memory_size = 0;
memory->cpu_gpu_memory_size = 0;
memory->max_frame_buffer_size = FrameSize(*config);
memory->frame_buffer_alignment = 256;
memory->reserved0 = 0;
return 0;
}
static int32_t KYTY_SYSV_ABI CreateDecoder(const ConfigInfo* config, const MemoryInfo* memory,
State** output) {
PRINT_NAME();
if (const auto error = ValidateConfig(config); error != 0) return error;
if (memory == nullptr || output == nullptr) return Fail(Error::ArgumentPointer);
if (memory->this_size != sizeof(MemoryInfo)) return Fail(Error::StructSize);
if (memory->cpu_memory_size < sizeof(State)) return Fail(Error::MemorySize);
if (memory->cpu_memory == nullptr ||
reinterpret_cast<uintptr_t>(memory->cpu_memory) % alignof(State) != 0)
return Fail(Error::MemoryPointer);
std::scoped_lock lock(g_registry_mutex);
if (!g_queues.contains(config->compute_queue)) return Fail(Error::ComputeQueue);
if (g_decoders.contains(static_cast<State*>(memory->cpu_memory)))
return Fail(Error::MemoryPointer);
auto* decoder = Decoder::Create(
{config->codec_type, config->max_width, config->max_height,
std::max(2u, static_cast<uint32_t>(config->max_dpb_frames) + config->extra_dpb_frames)});
if (decoder == nullptr) return Fail(Error::ApiFail);
auto* state = new (memory->cpu_memory) State {*config, decoder};
++config->compute_queue->decoders;
g_decoders.insert(state);
*output = state;
return 0;
}
static int32_t KYTY_SYSV_ABI DeleteDecoder(State* handle) {
PRINT_NAME();
std::scoped_lock lock(g_registry_mutex);
if (g_decoders.erase(handle) == 0) return Fail(Error::DecoderInstance);
handle->~State();
return 0;
}
static int32_t KYTY_SYSV_ABI SetDecodeInput(State* handle, const InputData* input) {
PRINT_NAME();
const StateAccess state(handle);
if (!state) return Fail(Error::DecoderInstance);
if (input == nullptr) return Fail(Error::ArgumentPointer);
if (input->this_size != sizeof(InputData)) return Fail(Error::StructSize);
if (input->au_data == nullptr) return Fail(Error::AccessUnitPointer);
if (input->au_size == 0) return Fail(Error::AccessUnitSize);
std::scoped_lock lock(state->mutex);
const auto& config = state->config;
if (!config.disable_sync_input &&
state->completed_inputs.size() >=
uint64_t {config.input_queue_depth} + config.max_pending_sync)
return Fail(Error::InputQueueFull);
uint32_t produced = 0;
const auto result = Decoder::Submit(
state->decoder,
{input->au_data, input->au_size, input->pts, input->dts, input->attached_data}, &produced);
if (result != Decoder::Result::Ok) return Result(result);
if (!config.disable_sync_input)
state->completed_inputs.push_back({sizeof(InputResult), input->au_data, produced, 0});
return 0;
}
static int32_t KYTY_SYSV_ABI TrySyncDecodeInput(State* handle, InputResult* output) {
PRINT_NAME();
const StateAccess state(handle);
if (!state) return Fail(Error::DecoderInstance);
if (output == nullptr) return Fail(Error::ArgumentPointer);
if (output->this_size != sizeof(InputResult)) return Fail(Error::StructSize);
std::scoped_lock lock(state->mutex);
if (state->config.disable_sync_input) return Fail(Error::ApiFail);
if (state->completed_inputs.empty()) return Fail(Error::InputQueueEmpty);
*output = state->completed_inputs.front();
state->completed_inputs.pop_front();
return 0;
}
static int32_t KYTY_SYSV_ABI SetDecodeOutput(State* handle, FrameBuffer* buffer) {
PRINT_NAME();
const StateAccess state(handle);
if (!state) return Fail(Error::DecoderInstance);
if (buffer == nullptr) return Fail(Error::ArgumentPointer);
if (buffer->this_size != sizeof(FrameBuffer)) return Fail(Error::StructSize);
if (buffer->data == nullptr) return Fail(Error::FrameBufferPointer);
if (reinterpret_cast<uintptr_t>(buffer->data) % 256 != 0)
return Fail(Error::FrameBufferAlignment);
if (buffer->size < FrameSize(state->config)) return Fail(Error::FrameBufferSize);
std::scoped_lock lock(state->mutex);
if (state->output_buffer) return Fail(Error::OutputBufferFull);
state->output_buffer = *buffer;
return 0;
}
static int32_t KYTY_SYSV_ABI TrySyncDecodeOutput(State* handle, OutputInfo* output) {
PRINT_NAME();
const StateAccess state(handle);
if (!state) return Fail(Error::DecoderInstance);
if (output == nullptr) return Fail(Error::ArgumentPointer);
if (output->this_size != sizeof(OutputInfo)) return Fail(Error::StructSize);
std::scoped_lock lock(state->mutex);
if (!state->output_buffer) return Fail(Error::OutputBufferEmpty);
const auto& buffer = *state->output_buffer;
Decoder::Output decoded {};
const auto result = Decoder::Receive(state->decoder, {buffer.data, buffer.size}, &decoded);
if (result != Decoder::Result::Ok) return Result(result);
*output = {sizeof(OutputInfo),
true,
decoded.last_frame,
decoded.error_frame,
1,
decoded.codec_type,
decoded.width,
decoded.pitch,
decoded.height,
false,
decoded.buffer,
decoded.buffer_size,
0,
decoded.pitch};
state->output_buffer.reset();
return 0;
}
static int32_t KYTY_SYSV_ABI FinalizeDecodeSequence(State* handle) {
PRINT_NAME();
const StateAccess state(handle);
if (!state) return Fail(Error::DecoderInstance);
std::scoped_lock lock(state->mutex);
return Result(Decoder::Finalize(state->decoder));
}
static int32_t KYTY_SYSV_ABI ResetDecoder(State* handle) {
PRINT_NAME();
const StateAccess state(handle);
if (!state) return Fail(Error::DecoderInstance);
std::scoped_lock lock(state->mutex);
Decoder::Reset(state->decoder);
state->completed_inputs.clear();
state->output_buffer.reset();
return 0;
}
static int32_t KYTY_SYSV_ABI GetAvcPictureInfo(const OutputInfo* output,
Decoder::AvcPictureInfo* first,
Decoder::AvcPictureInfo* second) {
PRINT_NAME();
if (output == nullptr || first == nullptr) return Fail(Error::ArgumentPointer);
if (output->this_size != sizeof(OutputInfo) || first->this_size != sizeof(*first) ||
(second != nullptr && second->this_size != sizeof(*second)))
return Fail(Error::StructSize);
if (!output->valid || output->codec_type != 1 || output->picture_count == 0)
return Fail(Error::OutputInfo);
Decoder::PictureInfo decoded {};
if (!Decoder::GetPictureInfo(output->frame_buffer, &decoded) || decoded.codec_type != 1)
return Fail(Error::OutputInfo);
*first = Decoder::MakeAvcPictureInfo(decoded, sizeof(*first));
if (second != nullptr) *second = {.this_size = sizeof(*second)};
return 0;
}
LIB_DEFINE(InitVdecsw_1) {
PRINT_NAME_ENABLE(true);
LIB_FUNC("0moTubWCsTM", QueryComputeMemoryInfo);
LIB_FUNC("hIgrg5h4V6s", AllocateComputeQueue);
LIB_FUNC("A+2M7EivuOU", QueryDecoderMemoryInfo);
LIB_FUNC("+L5ArV1tPGA", CreateDecoder);
LIB_FUNC("ecUtPX+dBYk", DeleteDecoder);
LIB_FUNC("fX-zOOefbbs", ReleaseComputeQueue);
LIB_FUNC("aqMiF0AgUYI", SetDecodeInput);
LIB_FUNC("veb-YBrOqo0", ResetDecoder);
LIB_FUNC("kMBw37oH8nI", TrySyncDecodeOutput);
LIB_FUNC("rgtMCOpyBSc", SetDecodeOutput);
LIB_FUNC("ihNT-uuEAr4", GetAvcPictureInfo);
LIB_FUNC("5Y6nZqIZvBg", FinalizeDecodeSequence);
LIB_FUNC("l4sQYy5wPkc", TrySyncDecodeInput);
}
} // namespace Libs::Vdecsw

View File

@ -111,69 +111,7 @@ struct Videodec2FrameBuffer {
bool is_accepted;
};
struct Videodec2AvcPictureInfo {
size_t this_size;
bool is_valid;
uint64_t pts_data;
uint64_t dts_data;
uint64_t attached_data;
uint8_t idr_picture_flag;
uint8_t profile_idc;
uint8_t level_idc;
uint32_t pic_width_in_mbs_minus1;
uint32_t pic_height_in_map_units_minus1;
uint8_t frame_mbs_only_flag;
uint8_t frame_cropping_flag;
uint32_t frame_crop_left_offset;
uint32_t frame_crop_right_offset;
uint32_t frame_crop_top_offset;
uint32_t frame_crop_bottom_offset;
uint8_t aspect_ratio_info_present_flag;
uint8_t aspect_ratio_idc;
uint16_t sar_width;
uint16_t sar_height;
uint8_t video_signal_type_present_flag;
uint8_t video_format;
uint8_t video_full_range_flag;
uint8_t colour_description_present_flag;
uint8_t colour_primaries;
uint8_t transfer_characteristics;
uint8_t matrix_coefficients;
uint8_t timing_info_present_flag;
uint32_t num_units_in_tick;
uint32_t time_scale;
uint8_t fixed_frame_rate_flag;
uint8_t bitstream_restriction_flag;
uint8_t max_dec_frame_buffering;
uint8_t pic_struct_present_flag;
uint8_t pic_struct;
uint8_t field_pic_flag;
uint8_t bottom_field_flag;
uint8_t sequence_parameter_set_present_flag;
uint8_t picture_parameter_set_present_flag;
uint8_t au_delimiter_present_flag;
uint8_t end_of_sequence_present_flag;
uint8_t end_of_stream_present_flag;
uint8_t filler_data_present_flag;
uint8_t picture_timing_sei_present_flag;
uint8_t buffering_period_sei_present_flag;
uint8_t constraint_set0_flag;
uint8_t constraint_set1_flag;
uint8_t constraint_set2_flag;
uint8_t constraint_set3_flag;
uint8_t constraint_set4_flag;
uint8_t constraint_set5_flag;
};
using Videodec2AvcPictureInfo = Decoder::AvcPictureInfo;
struct Videodec2ComputeMemoryInfo {
size_t this_size;
@ -235,6 +173,8 @@ static void FillNoPictureOutput(const Videodec2FrameBuffer* frame_buffer,
static int32_t MapDecoderResult(Decoder::Result result) {
switch (result) {
case Decoder::Result::Ok: return OK;
case Decoder::Result::InputFull:
case Decoder::Result::NoFrame:
case Decoder::Result::ApiFail: return VIDEODEC2_ERROR_API_FAIL;
case Decoder::Result::AccessUnit: return VIDEODEC2_ERROR_ACCESS_UNIT;
case Decoder::Result::FrameBufferSize: return VIDEODEC2_ERROR_FRAME_BUFFER_SIZE;
@ -599,40 +539,7 @@ static int32_t KYTY_SYSV_ABI GetPictureInfo(const Videodec2OutputInfo* output_in
return VIDEODEC2_ERROR_STRUCT_SIZE;
}
Videodec2AvcPictureInfo picture {};
picture.this_size = requested_size;
picture.is_valid = true;
picture.pts_data = decoded.pts;
picture.dts_data = decoded.dts;
picture.attached_data = decoded.attached_data;
picture.idr_picture_flag = decoded.key_frame ? 1 : 0;
picture.profile_idc = static_cast<uint8_t>(decoded.profile);
picture.level_idc = static_cast<uint8_t>(decoded.level);
picture.pic_width_in_mbs_minus1 = (decoded.width + 15u) / 16u - 1u;
picture.pic_height_in_map_units_minus1 = (decoded.height + 15u) / 16u - 1u;
picture.frame_mbs_only_flag = 1;
picture.frame_cropping_flag = decoded.crop_left != 0 || decoded.crop_right != 0 ||
decoded.crop_top != 0 || decoded.crop_bottom != 0
? 1
: 0;
picture.frame_crop_left_offset = decoded.crop_left;
picture.frame_crop_right_offset = decoded.crop_right;
picture.frame_crop_top_offset = decoded.crop_top;
picture.frame_crop_bottom_offset = decoded.crop_bottom;
picture.aspect_ratio_info_present_flag =
decoded.sar_width != 0 && decoded.sar_height != 0 ? 1 : 0;
picture.aspect_ratio_idc = picture.aspect_ratio_info_present_flag ? 255 : 0;
picture.sar_width = decoded.sar_width;
picture.sar_height = decoded.sar_height;
picture.video_signal_type_present_flag = 1;
picture.video_format = 5;
picture.video_full_range_flag = decoded.color_range == 2 ? 1 : 0;
picture.colour_description_present_flag =
decoded.color_primaries != 0 || decoded.color_trc != 0 || decoded.color_space != 0 ? 1
: 0;
picture.colour_primaries = decoded.color_primaries;
picture.transfer_characteristics = decoded.color_trc;
picture.matrix_coefficients = decoded.color_space;
const auto picture = Decoder::MakeAvcPictureInfo(decoded, requested_size);
std::memcpy(first_picture_info, &picture, requested_size);
} else {
const auto result = fill_common(first_picture_info, true);

View File

@ -22,6 +22,10 @@ namespace VideoDec2 {
LIB_DEFINE(InitVideoDec2_1);
} // namespace VideoDec2
namespace Vdecsw {
LIB_DEFINE(InitVdecsw_1);
} // namespace Vdecsw
namespace LibMouse {
LIB_DEFINE(InitMouse_1);
} // namespace LibMouse
@ -148,6 +152,7 @@ void InitAll(Loader::SymbolDatabase* s) {
LibUlt::InitUlt_1(s);
InitUserService_1(s);
VideoDec2::InitVideoDec2_1(s);
Vdecsw::InitVdecsw_1(s);
LibGen5::InitVideoOut_1(s);
LibGen5::VrrStatus::InitVideoOutVrrStatus_1(s);
InitWebBrowserDialog_1(s);

View File

@ -15,6 +15,9 @@
#else
#include <arpa/inet.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <net/route.h>
#include <netdb.h>
#include <netinet/tcp.h>
#include <sys/select.h>
@ -41,10 +44,12 @@
#include <cstring>
#include <fmt/format.h>
#include <fmt/ranges.h>
#include <fstream>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
@ -3386,7 +3391,6 @@ constexpr int NET_CTL_STATE_DISCONNECTED = 0;
constexpr int NET_CTL_STATE_IPOBTAINED = 3;
constexpr int NET_CTL_DEVICE_WIRED = 0;
constexpr int NET_CTL_DEVICE_WIRELESS = 1;
constexpr int NET_CTL_LINK_DISCONNECTED = 0;
constexpr int NET_CTL_LINK_CONNECTED = 1;
constexpr int NET_CTL_EVENT_DISCONNECTED = 1;
constexpr int NET_CTL_EVENT_IPOBTAINED = 3;
@ -3448,8 +3452,6 @@ struct NetCtlCallbackSlot {
static Common::Mutex g_net_ctl_callbacks_mutex;
static std::array<NetCtlCallbackSlot, 8> g_net_ctl_callbacks;
static std::atomic_bool g_net_ctl_connected = false;
static std::atomic_bool g_net_ctl_status_initialized = false;
struct HostNetworkInfo {
bool connected = false;
@ -3463,22 +3465,16 @@ struct HostNetworkInfo {
};
static void CopyIpv4String(char* dst, size_t dst_size, const sockaddr* addr) {
#if defined(_WIN32)
if (dst == nullptr || dst_size == 0 || addr == nullptr || addr->sa_family != AF_INET) {
return;
}
const auto* in = reinterpret_cast<const sockaddr_in*>(addr);
inet_ntop(AF_INET, &in->sin_addr, dst, static_cast<socklen_t>(dst_size));
#else
(void)dst;
(void)dst_size;
(void)addr;
#endif
}
static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
#if defined(_WIN32)
static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
if (dst == nullptr || dst_size == 0 || prefix_len > 32) {
return;
}
@ -3487,26 +3483,22 @@ static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
in_addr in {};
in.S_un.S_addr = htonl(mask);
inet_ntop(AF_INET, &in, dst, static_cast<socklen_t>(dst_size));
#else
(void)dst;
(void)dst_size;
(void)prefix_len;
#endif
}
#endif
static HostNetworkInfo QueryHostNetworkInfo() {
HostNetworkInfo info;
#if defined(_WIN32)
ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST;
ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS;
ULONG size = 0;
auto ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, nullptr, &size);
auto ret = GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &size);
if (ret != ERROR_BUFFER_OVERFLOW || size == 0) {
return info;
}
std::vector<uint8_t> buffer(size);
auto* adapters = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buffer.data());
ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, adapters, &size);
ret = GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &size);
if (ret != NO_ERROR) {
return info;
}
@ -3557,40 +3549,76 @@ static HostNetworkInfo QueryHostNetworkInfo() {
}
return info;
}
} else if (addr->sa_family == AF_INET6) {
const auto* in6 = reinterpret_cast<const sockaddr_in6*>(addr);
if (!IN6_IS_ADDR_LOOPBACK(&in6->sin6_addr) &&
!IN6_IS_ADDR_UNSPECIFIED(&in6->sin6_addr)) {
info.connected = true;
}
}
}
}
return info;
#else
info.connected = true;
ifaddrs* addresses = nullptr;
if (getifaddrs(&addresses) != 0) {
return info;
}
const auto usable = [](const ifaddrs* address) {
return address->ifa_addr != nullptr && address->ifa_addr->sa_family == AF_INET &&
(address->ifa_flags & (IFF_UP | IFF_RUNNING | IFF_LOOPBACK)) ==
(IFF_UP | IFF_RUNNING) &&
reinterpret_cast<const sockaddr_in*>(address->ifa_addr)->sin_addr.s_addr != 0;
};
const ifaddrs* selected = nullptr;
for (auto* address = addresses; address != nullptr; address = address->ifa_next) {
if (usable(address)) {
selected = address;
break;
}
}
std::ifstream routes("/proc/net/route");
std::string line;
uint32_t best_metric = std::numeric_limits<uint32_t>::max();
uint32_t gateway = 0;
bool has_default_route = false;
while (std::getline(routes, line)) {
std::istringstream fields(line);
std::string name;
uint32_t destination = 0, candidate_gateway = 0, flags = 0, refs = 0, uses = 0;
uint32_t metric = 0, mask = 0;
if (!(fields >> name >> std::hex >> destination >> candidate_gateway >> flags >>
std::dec >> refs >> uses >> metric >> std::hex >> mask) ||
destination != 0 || mask != 0 || (flags & (RTF_UP | RTF_REJECT)) != RTF_UP ||
(has_default_route && metric >= best_metric)) {
continue;
}
for (auto* address = addresses; address != nullptr; address = address->ifa_next) {
if (usable(address) && name == address->ifa_name) {
selected = address;
gateway = candidate_gateway;
best_metric = metric;
has_default_route = true;
break;
}
}
}
if (selected != nullptr) {
CopyIpv4String(info.ip_address, sizeof(info.ip_address), selected->ifa_addr);
CopyIpv4String(info.netmask, sizeof(info.netmask), selected->ifa_netmask);
info.connected = info.ip_address[0] != '\0';
if (has_default_route) {
in_addr address {gateway};
inet_ntop(AF_INET, &address, info.default_route, sizeof(info.default_route));
}
}
freeifaddrs(addresses);
return info;
#endif
}
static bool NetCtlConnected() {
if (!g_net_ctl_status_initialized.load()) {
g_net_ctl_connected = false;
g_net_ctl_status_initialized = true;
LOGF("\t host network connected = false (forced offline)\n");
}
return g_net_ctl_connected.load();
return Config::IsNetworkConnected() && QueryHostNetworkInfo().connected;
}
int KYTY_SYSV_ABI NetCtlInit() {
PRINT_NAME();
g_net_ctl_connected = false;
g_net_ctl_status_initialized = true;
LOGF("\t host network connected = false (forced offline)\n");
return OK;
}
@ -3651,7 +3679,7 @@ int KYTY_SYSV_ABI NetCtlGetStateV6(int* state) {
return NET_CTL_ERROR_INVALID_ADDR;
}
*state = (NetCtlConnected() ? NET_CTL_STATE_IPOBTAINED : NET_CTL_STATE_DISCONNECTED);
*state = NET_CTL_STATE_DISCONNECTED;
return OK;
}
@ -3720,24 +3748,28 @@ int KYTY_SYSV_ABI NetCtlGetResult(int event_type, int* error_code) {
int KYTY_SYSV_ABI NetCtlGetInfo(int code, NetCtlInfo* info) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(info == nullptr);
if (info == nullptr) {
return NET_CTL_ERROR_INVALID_ADDR;
}
if (code < 1 || code > 21) {
return NET_CTL_ERROR_INVALID_CODE;
}
if (!Config::IsNetworkConnected()) {
return NET_CTL_ERROR_NOT_CONNECTED;
}
const auto host = QueryHostNetworkInfo();
if (!host.connected || (code == 16 && host.default_route[0] == '\0')) {
return NET_CTL_ERROR_NOT_CONNECTED;
}
memset(info, 0, sizeof(NetCtlInfo));
// Online/host-backed info responses are left below for testing, but disabled
// to preserve the disconnected-console behavior.
return NET_CTL_ERROR_NOT_CONNECTED;
switch (code) {
case 1:
info->device =
(QueryHostNetworkInfo().wireless ? NET_CTL_DEVICE_WIRELESS : NET_CTL_DEVICE_WIRED);
info->device = (host.wireless ? NET_CTL_DEVICE_WIRELESS : NET_CTL_DEVICE_WIRED);
break;
case 2: info->ether_addr = QueryHostNetworkInfo().ether_addr; break;
case 2: info->ether_addr = host.ether_addr; break;
case 3: info->mtu = 1500; break;
case 4:
info->link = (NetCtlConnected() ? NET_CTL_LINK_CONNECTED : NET_CTL_LINK_DISCONNECTED);
break;
case 4: info->link = NET_CTL_LINK_CONNECTED; break;
case 5: break;
case 6: break;
case 7: info->wifi_security = 0; break;
@ -3748,28 +3780,25 @@ int KYTY_SYSV_ABI NetCtlGetInfo(int code, NetCtlInfo* info) {
case 12: break;
case 13: break;
case 14:
std::strncpy(info->ip_address, QueryHostNetworkInfo().ip_address,
std::strncpy(info->ip_address, host.ip_address,
sizeof(info->ip_address) - 1);
break;
case 15:
std::strncpy(info->netmask, QueryHostNetworkInfo().netmask, sizeof(info->netmask) - 1);
std::strncpy(info->netmask, host.netmask, sizeof(info->netmask) - 1);
break;
case 16:
std::strncpy(info->default_route, QueryHostNetworkInfo().default_route,
std::strncpy(info->default_route, host.default_route,
sizeof(info->default_route) - 1);
break;
case 17:
std::strncpy(info->primary_dns, QueryHostNetworkInfo().primary_dns,
sizeof(info->primary_dns) - 1);
std::strncpy(info->primary_dns, host.primary_dns, sizeof(info->primary_dns) - 1);
break;
case 18:
std::strncpy(info->secondary_dns, QueryHostNetworkInfo().secondary_dns,
sizeof(info->secondary_dns) - 1);
std::strncpy(info->secondary_dns, host.secondary_dns, sizeof(info->secondary_dns) - 1);
break;
case 19: info->http_proxy_config = NET_CTL_HTTP_PROXY_OFF; break;
case 20: break;
case 21: info->http_proxy_port = 0; break;
default: LOGF("\t unknown NetCtl info code: %d\n", code); return NET_CTL_ERROR_INVALID_CODE;
}
return OK;

View File

@ -30,6 +30,10 @@ LIB_NAME("Ngs2", "Ngs2");
constexpr int32_t NGS2_ERROR_INVALID_OUT_ADDRESS =
static_cast<int32_t>(0x804a8010u);
constexpr int32_t NGS2_ERROR_INVALID_NUM_WAVEFORM_BLOCKS =
static_cast<int32_t>(0x804a8053u);
constexpr int32_t NGS2_ERROR_FULL_WAVEFORM_BLOCKS =
static_cast<int32_t>(0x804a8054u);
constexpr int32_t NGS2_ERROR_INVALID_WAVEFORM_DATA =
static_cast<int32_t>(0x804a8430u);
constexpr int32_t NGS2_ERROR_INVALID_WAVEFORM_FORMAT =
@ -549,6 +553,7 @@ struct Ngs2VoiceInternal {
uint64_t sample_step = 0;
uint64_t decoded_samples = 0;
uint64_t decoded_bytes = 0;
uint64_t source_generation = 0;
uint32_t duration_remaining = 0;
uint32_t skip_remaining = 0;
const uint8_t* waveform_end = nullptr;
@ -574,6 +579,19 @@ struct Ngs2VoiceInternal {
filters.clear();
}
void ClearWaveformBlocks() {
++source_generation;
blocks.clear();
sample_phase = 0;
duration_remaining = 0;
skip_remaining = 0;
compressed_input.clear();
frame_cursor = 0;
frame_samples = 0;
next_frame_samples = 0;
waveform_end = nullptr;
}
void SetupSampler(const Ngs2WaveformFormat& format) {
const bool deferred = format.waveform_type == 0 && format.num_channels == 0 &&
format.sample_rate == 0 && format.config_data == 0 &&
@ -582,23 +600,15 @@ struct Ngs2VoiceInternal {
(format.frame_margin != 0 || format.frame_offset != 0 ||
format.sample_rate == 0 || format.num_channels == 0));
ResetSetupState();
channels = format.num_channels;
sample_rate = format.sample_rate;
sample_phase = 0;
sample_step = uint64_t(sample_rate) << 32u;
decoded_samples = 0;
decoded_bytes = 0;
duration_remaining = 0;
skip_remaining = 0;
waveform_end = nullptr;
blocks.clear();
ClearWaveformBlocks();
channels = format.num_channels;
sample_rate = format.sample_rate;
sample_step = uint64_t(sample_rate) << 32u;
decoded_samples = 0;
decoded_bytes = 0;
decoder.reset();
decoded_frame.clear();
next_decoded_frame.clear();
compressed_input.clear();
frame_cursor = 0;
frame_samples = 0;
next_frame_samples = 0;
accepts_blocks = true;
if (deferred) {
return;
@ -625,6 +635,63 @@ struct Ngs2VoiceInternal {
}
}
int AddWaveformBlocks(const uint8_t* data, uint32_t flags, uint32_t count,
const Ngs2WaveformBlock* waveform_blocks) {
const auto capacity = rack->type == Ngs2RackType::Sampler
? rack->option.sampler.max_waveform_blocks
: rack->option.custom_sampler.max_waveform_blocks;
if (count > capacity) {
return NGS2_ERROR_INVALID_NUM_WAVEFORM_BLOCKS;
}
EXIT_NOT_IMPLEMENTED(!accepts_blocks || (flags & ~0x17u) != 0);
const bool clear = (flags & 4u) != 0;
const bool only_data = (flags & 2u) != 0;
const bool join = only_data && (flags & 0x10u) == 0;
EXIT_NOT_IMPLEMENTED(only_data && decoder == nullptr);
const auto retained = clear ? 0 : blocks.size();
Block tail = retained == 0 ? Block {} : blocks.back();
std::vector<Block> pending;
pending.reserve(count);
for (uint32_t i = 0; i < count; ++i) {
auto info = waveform_blocks[i];
if (!only_data && info.num_samples == 0 && info.data_size == 0) {
continue;
}
EXIT_NOT_IMPLEMENTED(!only_data && info.num_samples == 0);
if (only_data) {
info.num_repeats = 0;
info.num_skip_samples = 0;
info.num_samples = 0;
}
EXIT_NOT_IMPLEMENTED(info.num_repeats != 0 && rack->type != Ngs2RackType::Sampler);
EXIT_NOT_IMPLEMENTED(decoder == nullptr &&
(uint64_t(info.num_skip_samples) + info.num_samples) *
channels * sizeof(int16_t) >
info.data_size);
Block block {data + info.data_offset, info};
auto* previous = pending.empty() ? (retained == 0 ? nullptr : &tail)
: &pending.back();
if (join && previous != nullptr &&
previous->data + previous->info.data_size == block.data) {
previous->info.data_size += block.info.data_size;
} else {
pending.push_back(block);
}
}
// Validate the complete addition before clearing, extending, or closing the stream.
if (retained + pending.size() > capacity) {
return NGS2_ERROR_FULL_WAVEFORM_BLOCKS;
}
if (clear) {
ClearWaveformBlocks();
} else if (retained != 0) {
blocks.back().info.data_size = tail.info.data_size;
}
blocks.insert(blocks.end(), pending.begin(), pending.end());
accepts_blocks = (flags & 1u) != 0;
return OK;
}
void SetMatrix(uint32_t index, const float* levels, uint32_t count) {
EXIT_NOT_IMPLEMENTED(index >= matrices.size());
matrices[index].assign(levels, levels + count);
@ -1425,9 +1492,10 @@ static_assert(offsetof(Ngs2VoiceCallbackInfo, size) == 40 &&
offsetof(Ngs2VoiceCallbackInfo, repeats) == 44 &&
offsetof(Ngs2VoiceCallbackInfo, attributes) == 48);
static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
auto& block = voice.blocks.front();
const bool repeat = block.info.num_repeats != 0;
static bool Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
const auto generation = voice.source_generation;
auto& block = voice.blocks.front();
const bool repeat = block.info.num_repeats != 0;
voice.waveform_end = voice.WaveformData();
if (repeat) {
if (block.info.num_repeats != UINT32_MAX) {
@ -1458,32 +1526,41 @@ static void Ngs2FinishBlock(Ngs2VoiceInternal& voice) {
reinterpret_cast<void KYTY_SYSV_ABI (*)(const Ngs2VoiceCallbackInfo*)>(voice.callback)(
&info);
}
return voice.source_generation == generation;
}
static void Ngs2AdvancePcm(Ngs2VoiceInternal& voice) {
const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u;
// Completion callbacks may interrupt advancement across multiple blocks.
static bool Ngs2DecodePcmFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
uint32_t& frame_samples) {
// PCM has no encoded frame size. Use one system grain as a bounded staging
// quantum, keeping source decoding separate from resampled output consumption.
const auto quantum = std::max(voice.rack->ngs->option.num_grain_samples, 1u);
frame.resize(size_t(quantum) * voice.channels);
frame_samples = 0;
while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty() &&
voice.sample_phase >= output_rate) {
auto& block = voice.blocks.front();
const auto available = block.info.num_samples - block.cursor;
const auto advance = std::min(voice.sample_phase / output_rate,
uint64_t(available));
block.cursor += static_cast<uint32_t>(advance);
voice.decoded_samples += advance;
voice.decoded_bytes += advance * voice.channels * sizeof(int16_t);
voice.sample_phase -= advance * output_rate;
if (block.cursor == block.info.num_samples) {
Ngs2FinishBlock(voice);
frame_samples < quantum) {
auto& block = voice.blocks.front();
const auto count = std::min(quantum - frame_samples, block.info.num_samples - block.cursor);
const auto* pcm = reinterpret_cast<const int16_t*>(voice.WaveformData());
for (size_t i = 0; i < size_t(count) * voice.channels; ++i) {
frame[size_t(frame_samples) * voice.channels + i] = pcm[i] / 32768.0f;
}
block.cursor += count;
frame_samples += count;
voice.decoded_samples += count;
voice.decoded_bytes += size_t(count) * voice.channels * sizeof(int16_t);
if (block.cursor == block.info.num_samples && !Ngs2FinishBlock(voice)) {
return false;
}
}
if (voice.blocks.empty() && !voice.accepts_blocks) {
voice.sample_phase = 0;
}
return frame_samples != 0;
}
static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
uint32_t& frame_samples) {
if (voice.decoder == nullptr) {
return Ngs2DecodePcmFrame(voice, frame, frame_samples);
}
frame_samples = 0;
while (voice.state == Ngs2VoicePlayState::Playing && !voice.blocks.empty()) {
auto& block = voice.blocks.front();
if (block.info.num_samples != 0 && block.data_cursor == 0) {
@ -1491,7 +1568,9 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
voice.skip_remaining = block.info.num_skip_samples;
}
if (voice.duration_remaining == 0) {
Ngs2FinishBlock(voice);
if (!Ngs2FinishBlock(voice)) {
return false;
}
continue;
}
const auto skip =
@ -1512,7 +1591,9 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
block.data_cursor += bytes;
voice.decoded_bytes += bytes;
if (voice.compressed_input.size() < info.next_frame_size) {
Ngs2FinishBlock(voice);
if (!Ngs2FinishBlock(voice)) {
return false;
}
continue;
}
}
@ -1529,7 +1610,9 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
block.data + block.info.data_size);
voice.decoded_bytes += block.info.data_size - block.data_cursor;
block.data_cursor = block.info.data_size;
Ngs2FinishBlock(voice);
if (!Ngs2FinishBlock(voice)) {
return false;
}
continue;
}
EXIT_NOT_IMPLEMENTED(result.result != OK || result.input_consumed == 0);
@ -1547,13 +1630,16 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
EXIT_NOT_IMPLEMENTED(decoded_samples > voice.duration_remaining);
frame_samples = decoded_samples;
voice.duration_remaining -= decoded_samples;
voice.decoded_samples += decoded_samples;
}
if (voice.duration_remaining == 0 ||
(block.data_cursor == block.info.data_size && voice.compressed_input.empty())) {
if (voice.duration_remaining == 0) {
voice.compressed_input.clear();
}
Ngs2FinishBlock(voice);
if (!Ngs2FinishBlock(voice)) {
return false;
}
}
if (decoded_samples != 0) {
return true;
@ -1562,8 +1648,10 @@ static bool Ngs2DecodeFrame(Ngs2VoiceInternal& voice, std::vector<float>& frame,
return false;
}
static bool Ngs2CurrentAtracFrame(Ngs2VoiceInternal& voice) {
static bool Ngs2CurrentFrame(Ngs2VoiceInternal& voice) {
if (voice.frame_cursor == voice.frame_samples) {
voice.frame_cursor = 0;
voice.frame_samples = 0;
if (voice.next_frame_samples != 0) {
std::swap(voice.decoded_frame, voice.next_decoded_frame);
voice.frame_samples = voice.next_frame_samples;
@ -1571,21 +1659,21 @@ static bool Ngs2CurrentAtracFrame(Ngs2VoiceInternal& voice) {
} else if (!Ngs2DecodeFrame(voice, voice.decoded_frame, voice.frame_samples)) {
return false;
}
voice.frame_cursor = 0;
}
return voice.state == Ngs2VoicePlayState::Playing &&
voice.frame_cursor < voice.frame_samples;
}
static void Ngs2AdvanceAtrac9(Ngs2VoiceInternal& voice) {
static void Ngs2AdvanceSamples(Ngs2VoiceInternal& voice) {
const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u;
while (voice.state == Ngs2VoicePlayState::Playing && voice.sample_phase >= output_rate) {
if (!Ngs2CurrentAtracFrame(voice)) {
if (!Ngs2CurrentFrame(voice)) {
break;
}
++voice.frame_cursor;
++voice.decoded_samples;
voice.sample_phase -= output_rate;
const auto advance = std::min(voice.sample_phase / output_rate,
uint64_t(voice.frame_samples - voice.frame_cursor));
voice.frame_cursor += static_cast<uint32_t>(advance);
voice.sample_phase -= advance * output_rate;
}
if (voice.blocks.empty() && voice.frame_cursor == voice.frame_samples &&
voice.next_frame_samples == 0 && !voice.accepts_blocks) {
@ -1594,42 +1682,14 @@ static void Ngs2AdvanceAtrac9(Ngs2VoiceInternal& voice) {
}
static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
uint32_t output = 0;
const auto generation = voice.source_generation;
uint32_t output = 0;
const auto output_rate = uint64_t(voice.rack->ngs->option.sample_rate) << 32u;
while (voice.state == Ngs2VoicePlayState::Playing && output < grain &&
(!voice.blocks.empty() || voice.frame_cursor < voice.frame_samples ||
voice.next_frame_samples != 0)) {
if (voice.decoder == nullptr) {
Ngs2AdvancePcm(voice);
if (voice.state != Ngs2VoicePlayState::Playing || voice.blocks.empty()) {
break;
}
const auto& block = voice.blocks.front();
const auto* pcm = reinterpret_cast<const int16_t*>(voice.WaveformData());
const auto* next = pcm;
if (block.cursor + 1 < block.info.num_samples) {
next += voice.channels;
} else if (block.info.num_repeats != 0) {
next = reinterpret_cast<const int16_t*>(block.data) +
block.info.num_skip_samples * voice.channels;
} else if (voice.blocks.size() > 1) {
const auto& next_block = voice.blocks[1];
next = reinterpret_cast<const int16_t*>(next_block.data) +
next_block.info.num_skip_samples * voice.channels;
}
const float fraction = static_cast<float>(voice.sample_phase) /
static_cast<float>(output_rate);
for (uint32_t c = 0; c < voice.channels; ++c) {
voice.samples[c * grain + output] =
std::lerp(static_cast<float>(pcm[c]), static_cast<float>(next[c]), fraction) /
32768.0f;
}
voice.sample_phase += voice.sample_step;
++output;
continue;
}
Ngs2AdvanceAtrac9(voice);
if (!Ngs2CurrentAtracFrame(voice)) {
Ngs2AdvanceSamples(voice);
if (voice.source_generation != generation || !Ngs2CurrentFrame(voice)) {
break;
}
if (voice.frame_cursor + 1 == voice.frame_samples && voice.sample_phase != 0) {
@ -1639,7 +1699,8 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
}
Ngs2DecodeFrame(voice, voice.next_decoded_frame, voice.next_frame_samples);
}
if (voice.state != Ngs2VoicePlayState::Playing ||
if (voice.source_generation != generation ||
voice.state != Ngs2VoicePlayState::Playing ||
voice.frame_cursor >= voice.frame_samples) {
break;
}
@ -1657,10 +1718,13 @@ static void Ngs2ConsumeSamples(Ngs2VoiceInternal& voice, uint32_t grain) {
voice.sample_phase += voice.sample_step;
++output;
}
if (voice.decoder == nullptr) {
Ngs2AdvancePcm(voice);
} else {
Ngs2AdvanceAtrac9(voice);
if (voice.source_generation == generation) {
Ngs2AdvanceSamples(voice);
}
if (voice.source_generation != generation) {
// A callback replaced the source, possibly changing its channel layout.
voice.samples.assign(size_t(grain) * voice.channels, 0.0f);
output = 0;
}
voice.has_samples = output != 0;
}
@ -2370,41 +2434,10 @@ int KYTY_SYSV_ABI Ngs2VoiceControl(uintptr_t voice_handle, const Ngs2VoiceParamH
const Ngs2WaveformBlock* blocks;
};
const auto& blocks = *reinterpret_cast<const BlocksParam*>(param);
EXIT_NOT_IMPLEMENTED(!voice->accepts_blocks ||
(blocks.flags & ~0x17u) != 0);
if ((blocks.flags & 0x4u) != 0) {
voice->blocks.clear();
voice->sample_phase = 0;
voice->duration_remaining = 0;
voice->skip_remaining = 0;
voice->compressed_input.clear();
voice->frame_cursor = 0;
voice->frame_samples = 0;
voice->next_frame_samples = 0;
voice->waveform_end = nullptr;
}
voice->accepts_blocks = (blocks.flags & 1u) != 0;
const bool only_data = (blocks.flags & 2u) != 0;
EXIT_NOT_IMPLEMENTED(only_data && voice->decoder == nullptr);
for (uint32_t i = 0; i < blocks.count; ++i) {
auto block = blocks.blocks[i];
if (!only_data && block.num_samples == 0 && block.data_size == 0) {
continue;
}
EXIT_NOT_IMPLEMENTED(!only_data && block.num_samples == 0);
if (only_data) {
block.num_repeats = 0;
block.num_skip_samples = 0;
block.num_samples = 0;
}
EXIT_NOT_IMPLEMENTED(block.num_repeats != 0 &&
voice->rack->type != Ngs2RackType::Sampler);
EXIT_NOT_IMPLEMENTED(
voice->decoder == nullptr &&
(uint64_t(block.num_skip_samples) + block.num_samples) *
voice->channels * 2 >
block.data_size);
voice->blocks.push_back({blocks.data + block.data_offset, block});
const auto result = voice->AddWaveformBlocks(
blocks.data, blocks.flags, blocks.count, blocks.blocks);
if (result != OK) {
return result;
}
break;
}
@ -2511,6 +2544,15 @@ int KYTY_SYSV_ABI Ngs2VoiceRunCommands(uintptr_t voice_handle, const void* comma
return OK;
}
template <typename State>
static void Ngs2FillSamplerState(const Ngs2VoiceInternal& voice, State& state) {
state.voice_state.state_flags = Ngs2GetStateFlags(&voice);
state.num_decoded_samples = voice.decoded_samples;
state.decoded_data_size = voice.decoded_bytes;
state.waveform_data = voice.WaveformData();
state.user_data = voice.blocks.empty() ? 0 : voice.blocks.front().info.user_data;
}
int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* state,
size_t state_size) {
PRINT_NAME();
@ -2547,7 +2589,7 @@ int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* stat
EXIT_NOT_IMPLEMENTED(state_size != configured_size);
std::memset(state, 0, state_size);
auto* sampler = reinterpret_cast<Ngs2CustomSamplerVoiceState*>(state);
sampler->voice_state.state_flags = Ngs2GetStateFlags(voice);
Ngs2FillSamplerState(*voice, *sampler);
break;
}
case Ngs2RackType::Sampler: {
@ -2568,12 +2610,7 @@ int KYTY_SYSV_ABI Ngs2VoiceGetState(uintptr_t voice_handle, Ngs2VoiceState* stat
sampler->envelope_height = 1.0f;
sampler->peak_height = 0.0f;
sampler->reserved = 0;
sampler->num_decoded_samples = voice->decoded_samples;
sampler->decoded_data_size = voice->decoded_bytes;
sampler->waveform_data = voice->WaveformData();
if (!voice->blocks.empty()) {
sampler->user_data = voice->blocks.front().info.user_data;
}
Ngs2FillSamplerState(*voice, *sampler);
break;
}
default: EXIT("unknown type: %s\n", magic_enum::enum_name(voice->rack->type));

View File

@ -6,6 +6,7 @@
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <deque>
#include <limits>
#include <mutex>
#include <unordered_map>
@ -77,6 +78,7 @@ public:
explicit Instance(const Config& config): m_config(config) {}
~Instance() {
ClearFrames();
ClearPictureMetadata();
if (m_sws != nullptr) {
sws_freeContext(m_sws);
@ -119,116 +121,138 @@ public:
[[nodiscard]] uint32_t CodecType() const { return m_config.codec_type; }
[[nodiscard]] Result Submit(const Input& input, uint32_t* produced) {
std::scoped_lock lock(m_mutex);
return SubmitInput(input, produced);
}
[[nodiscard]] Result Receive(const FrameBuffer& frame_buffer, Output* output) {
std::scoped_lock lock(m_mutex);
return ReceiveOutput(frame_buffer, output);
}
[[nodiscard]] Result Finalize() {
std::scoped_lock lock(m_mutex);
if (m_finalized) return Result::Ok;
const auto drained = DrainFrames();
if (drained != Result::Ok) return drained;
const int result = avcodec_send_packet(m_codec, nullptr);
if (result == AVERROR(EAGAIN)) return Result::InputFull;
if (result < 0 && result != AVERROR_EOF) return Result::ApiFail;
m_finalized = true;
return DrainFrames();
}
[[nodiscard]] Result DecodeInput(const Input& input, const FrameBuffer& frame_buffer,
Output* output) {
std::scoped_lock lock(m_mutex);
*output = {};
AVPacket* packet = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
if (packet == nullptr || frame == nullptr ||
input.size > static_cast<size_t>(std::numeric_limits<int>::max())) {
av_packet_free(&packet);
av_frame_free(&frame);
return Result::ApiFail;
uint32_t produced = 0;
auto result = SubmitInput(input, &produced);
if (result == Result::InputFull) {
// The combined API supplies an output buffer together with each AU.
result = ReceiveOutput(frame_buffer, output);
if (result != Result::Ok) return result;
return SubmitInput(input, &produced);
}
int result = av_new_packet(packet, static_cast<int>(input.size));
if (result < 0) {
LOGF("Videodec2: av_new_packet failed: %s (%d)\n", AvErrorString(result), result);
av_packet_free(&packet);
av_frame_free(&frame);
return Result::ApiFail;
}
std::memcpy(packet->data, input.data, input.size);
packet->pts = ToAvTimestamp(input.pts);
packet->dts = ToAvTimestamp(input.dts);
packet->opaque_ref = av_buffer_alloc(sizeof(PacketMetadata));
if (packet->opaque_ref == nullptr) {
av_packet_free(&packet);
av_frame_free(&frame);
return Result::ApiFail;
}
const PacketMetadata metadata {input.pts, input.dts, input.attached_data};
std::memcpy(packet->opaque_ref->data, &metadata, sizeof(metadata));
bool have_pending_frame = false;
result = avcodec_send_packet(m_codec, packet);
if (result == AVERROR(EAGAIN)) {
result = avcodec_receive_frame(m_codec, frame);
if (result < 0) {
LOGF("Videodec2: decoder rejected an AU while no output was available: %s (%d)\n",
AvErrorString(result), result);
av_packet_free(&packet);
av_frame_free(&frame);
return Result::AccessUnit;
}
have_pending_frame = true;
result = avcodec_send_packet(m_codec, packet);
}
if (result < 0) {
LOGF("Videodec2: avcodec_send_packet failed: %s (%d)\n", AvErrorString(result), result);
av_packet_free(&packet);
av_frame_free(&frame);
return Result::AccessUnit;
}
Result decode_result = Result::Ok;
if (!have_pending_frame) {
result = avcodec_receive_frame(m_codec, frame);
if (result != AVERROR(EAGAIN) && result != AVERROR_EOF) {
if (result < 0) {
LOGF("Videodec2: avcodec_receive_frame failed: %s (%d)\n",
AvErrorString(result), result);
decode_result = Result::AccessUnit;
} else {
decode_result = CopyFrame(frame, frame_buffer, output);
}
}
} else {
decode_result = CopyFrame(frame, frame_buffer, output);
}
av_packet_free(&packet);
av_frame_free(&frame);
return decode_result;
if (result != Result::Ok) return result;
result = ReceiveOutput(frame_buffer, output);
return result == Result::NoFrame ? Result::Ok : result;
}
[[nodiscard]] Result FlushOutput(const FrameBuffer& frame_buffer, Output* output) {
std::scoped_lock lock(m_mutex);
*output = {};
AVFrame* frame = av_frame_alloc();
if (frame == nullptr) {
return Result::ApiFail;
}
// Guest Flush collects available pictures between AUs while retaining reference frames.
const int receive_result = avcodec_receive_frame(m_codec, frame);
if (receive_result == AVERROR(EAGAIN) || receive_result == AVERROR_EOF) {
av_frame_free(&frame);
return Result::Ok;
}
if (receive_result < 0) {
LOGF("Videodec2: receiving a flushed frame failed: %s (%d)\n",
AvErrorString(receive_result), receive_result);
av_frame_free(&frame);
return Result::ApiFail;
}
const auto result = CopyFrame(frame, frame_buffer, output);
av_frame_free(&frame);
return result;
const auto result = ReceiveOutput(frame_buffer, output);
return result == Result::NoFrame ? Result::Ok : result;
}
void ResetDecoder() {
std::scoped_lock lock(m_mutex);
avcodec_flush_buffers(m_codec);
ClearFrames();
ClearPictureMetadata();
m_finalized = m_eof = false;
}
private:
struct PendingFrame {
AVFrame* frame;
PictureInfo picture;
};
void ClearFrames() {
for (auto& pending: m_frames) av_frame_free(&pending.frame);
m_frames.clear();
}
[[nodiscard]] Result DrainFrames(uint32_t* produced = nullptr) {
while (!m_eof && m_frames.size() < m_config.max_pending_frames) {
AVFrame* frame = av_frame_alloc();
if (frame == nullptr) return Result::ApiFail;
const int result = avcodec_receive_frame(m_codec, frame);
if (result < 0) {
av_frame_free(&frame);
if (result == AVERROR_EOF) m_eof = true;
if (result == AVERROR(EAGAIN) || m_eof) return Result::Ok;
LOGF("Video decoder: avcodec_receive_frame failed: %s (%d)\n",
AvErrorString(result), result);
return Result::AccessUnit;
}
m_frames.push_back({frame, MakePictureInfo(frame)});
if (produced != nullptr) ++*produced;
}
return Result::Ok;
}
[[nodiscard]] Result SubmitInput(const Input& input, uint32_t* produced) {
*produced = 0;
if (m_finalized) return Result::ApiFail;
const auto drained = DrainFrames();
if (drained != Result::Ok) return drained;
if (m_frames.size() >= m_config.max_pending_frames) return Result::InputFull;
if (input.size > static_cast<size_t>(std::numeric_limits<int>::max())) return Result::ApiFail;
AVPacket* packet = av_packet_alloc();
if (packet == nullptr) return Result::ApiFail;
int result = av_new_packet(packet, static_cast<int>(input.size));
if (result < 0) {
av_packet_free(&packet);
return Result::ApiFail;
}
std::memcpy(packet->data, input.data, input.size);
packet->pts = ToAvTimestamp(input.pts);
packet->dts = ToAvTimestamp(input.dts);
packet->opaque_ref = av_buffer_alloc(sizeof(PacketMetadata));
if (packet->opaque_ref == nullptr) {
av_packet_free(&packet);
return Result::ApiFail;
}
const PacketMetadata metadata {input.pts, input.dts, input.attached_data};
std::memcpy(packet->opaque_ref->data, &metadata, sizeof(metadata));
result = avcodec_send_packet(m_codec, packet);
av_packet_free(&packet);
if (result == AVERROR(EAGAIN)) return Result::InputFull; // The AU was not accepted.
if (result < 0) {
LOGF("Video decoder: submitting an AU failed: %s (%d)\n", AvErrorString(result), result);
return Result::AccessUnit;
}
// Once accepted, a full output queue cannot turn this AU into a retry.
return DrainFrames(produced);
}
[[nodiscard]] Result ReceiveOutput(const FrameBuffer& frame_buffer, Output* output) {
*output = {};
const auto drained = DrainFrames();
if (drained != Result::Ok) return drained;
if (m_frames.empty()) return Result::NoFrame;
auto& pending = m_frames.front();
const auto result = CopyFrame(pending.frame, pending.picture, frame_buffer, output);
if (result != Result::Ok) return result;
output->last_frame = m_finalized && m_eof && m_frames.size() == 1u;
av_frame_free(&pending.frame);
m_frames.pop_front();
return Result::Ok;
}
[[nodiscard]] PictureInfo MakePictureInfo(const AVFrame* frame) const {
PictureInfo result {};
if (frame->opaque_ref != nullptr && frame->opaque_ref->size >= sizeof(PacketMetadata)) {
@ -268,8 +292,8 @@ private:
return result;
}
[[nodiscard]] Result CopyFrame(const AVFrame* frame, const FrameBuffer& frame_buffer,
Output* output) {
[[nodiscard]] Result CopyFrame(const AVFrame* frame, const PictureInfo& picture,
const FrameBuffer& frame_buffer, Output* output) {
if (frame->width <= 0 || frame->height <= 0) {
return Result::ApiFail;
}
@ -329,7 +353,7 @@ private:
{
std::scoped_lock lock(g_picture_mutex);
g_picture_infos[frame_buffer.data] = {this, MakePictureInfo(frame)};
g_picture_infos[frame_buffer.data] = {this, picture};
m_picture_buffers.insert(frame_buffer.data);
}
return Result::Ok;
@ -346,6 +370,9 @@ private:
m_picture_buffers.clear();
}
std::deque<PendingFrame> m_frames;
bool m_finalized = false;
bool m_eof = false;
Config m_config;
AVCodecContext* m_codec = nullptr;
SwsContext* m_sws = nullptr;
@ -358,7 +385,7 @@ bool IsCodecSupported(uint32_t codec_type) {
}
Instance* Create(const Config& config) {
if (!IsCodecSupported(config.codec_type)) {
if (!IsCodecSupported(config.codec_type) || config.max_pending_frames < 2u) {
return nullptr;
}
auto* instance = new Instance(config);
@ -377,6 +404,18 @@ uint32_t GetCodecType(const Instance* instance) {
return instance->CodecType();
}
Result Submit(Instance* instance, const Input& input, uint32_t* produced) {
return instance->Submit(input, produced);
}
Result Receive(Instance* instance, const FrameBuffer& frame_buffer, Output* output) {
return instance->Receive(frame_buffer, output);
}
Result Finalize(Instance* instance) {
return instance->Finalize();
}
Result Decode(Instance* instance, const Input& input, const FrameBuffer& frame_buffer,
Output* output) {
return instance->DecodeInput(input, frame_buffer, output);
@ -390,6 +429,44 @@ void Reset(Instance* instance) {
instance->ResetDecoder();
}
AvcPictureInfo MakeAvcPictureInfo(const PictureInfo& decoded, size_t size) {
AvcPictureInfo picture {};
picture.this_size = size;
picture.is_valid = true;
picture.pts_data = decoded.pts;
picture.dts_data = decoded.dts;
picture.attached_data = decoded.attached_data;
picture.idr_picture_flag = decoded.key_frame ? 1 : 0;
picture.profile_idc = static_cast<uint8_t>(decoded.profile);
picture.level_idc = static_cast<uint8_t>(decoded.level);
picture.pic_width_in_mbs_minus1 = (decoded.width + 15u) / 16u - 1u;
picture.pic_height_in_map_units_minus1 = (decoded.height + 15u) / 16u - 1u;
picture.frame_mbs_only_flag = 1;
picture.frame_cropping_flag = decoded.crop_left != 0 || decoded.crop_right != 0 ||
decoded.crop_top != 0 || decoded.crop_bottom != 0
? 1
: 0;
picture.frame_crop_left_offset = decoded.crop_left;
picture.frame_crop_right_offset = decoded.crop_right;
picture.frame_crop_top_offset = decoded.crop_top;
picture.frame_crop_bottom_offset = decoded.crop_bottom;
picture.aspect_ratio_info_present_flag =
decoded.sar_width != 0 && decoded.sar_height != 0 ? 1 : 0;
picture.aspect_ratio_idc = picture.aspect_ratio_info_present_flag ? 255 : 0;
picture.sar_width = decoded.sar_width;
picture.sar_height = decoded.sar_height;
picture.video_signal_type_present_flag = 1;
picture.video_format = 5;
picture.video_full_range_flag = decoded.color_range == 2 ? 1 : 0;
picture.colour_description_present_flag =
decoded.color_primaries != 0 || decoded.color_trc != 0 || decoded.color_space != 0 ? 1
: 0;
picture.colour_primaries = decoded.color_primaries;
picture.transfer_characteristics = decoded.color_trc;
picture.matrix_coefficients = decoded.color_space;
return picture;
}
bool GetPictureInfo(void* frame_buffer, PictureInfo* picture_info) {
std::scoped_lock lock(g_picture_mutex);
const auto it = g_picture_infos.find(frame_buffer);

View File

@ -14,12 +14,15 @@ enum class Result {
AccessUnit,
FrameBufferSize,
OversizeDecode,
InputFull,
NoFrame,
};
struct Config {
uint32_t codec_type = 0;
int32_t max_width = -1;
int32_t max_height = -1;
uint32_t codec_type = 0;
int32_t max_width = -1;
int32_t max_height = -1;
uint32_t max_pending_frames = 2;
};
struct Input {
@ -37,6 +40,7 @@ struct FrameBuffer {
struct Output {
bool valid = false;
bool last_frame = false;
bool error_frame = false;
bool buffer_accepted = false;
uint32_t codec_type = 0;
@ -69,12 +73,83 @@ struct PictureInfo {
bool key_frame = false;
};
struct AvcPictureInfo {
size_t this_size;
bool is_valid;
uint64_t pts_data;
uint64_t dts_data;
uint64_t attached_data;
uint8_t idr_picture_flag;
uint8_t profile_idc;
uint8_t level_idc;
uint32_t pic_width_in_mbs_minus1;
uint32_t pic_height_in_map_units_minus1;
uint8_t frame_mbs_only_flag;
uint8_t frame_cropping_flag;
uint32_t frame_crop_left_offset;
uint32_t frame_crop_right_offset;
uint32_t frame_crop_top_offset;
uint32_t frame_crop_bottom_offset;
uint8_t aspect_ratio_info_present_flag;
uint8_t aspect_ratio_idc;
uint16_t sar_width;
uint16_t sar_height;
uint8_t video_signal_type_present_flag;
uint8_t video_format;
uint8_t video_full_range_flag;
uint8_t colour_description_present_flag;
uint8_t colour_primaries;
uint8_t transfer_characteristics;
uint8_t matrix_coefficients;
uint8_t timing_info_present_flag;
uint32_t num_units_in_tick;
uint32_t time_scale;
uint8_t fixed_frame_rate_flag;
uint8_t bitstream_restriction_flag;
uint8_t max_dec_frame_buffering;
uint8_t pic_struct_present_flag;
uint8_t pic_struct;
uint8_t field_pic_flag;
uint8_t bottom_field_flag;
uint8_t sequence_parameter_set_present_flag;
uint8_t picture_parameter_set_present_flag;
uint8_t au_delimiter_present_flag;
uint8_t end_of_sequence_present_flag;
uint8_t end_of_stream_present_flag;
uint8_t filler_data_present_flag;
uint8_t picture_timing_sei_present_flag;
uint8_t buffering_period_sei_present_flag;
uint8_t constraint_set0_flag;
uint8_t constraint_set1_flag;
uint8_t constraint_set2_flag;
uint8_t constraint_set3_flag;
uint8_t constraint_set4_flag;
uint8_t constraint_set5_flag;
};
static_assert(sizeof(AvcPictureInfo) == 120);
[[nodiscard]] AvcPictureInfo MakeAvcPictureInfo(const PictureInfo& decoded, size_t size);
class Instance;
[[nodiscard]] bool IsCodecSupported(uint32_t codec_type);
[[nodiscard]] Instance* Create(const Config& config);
void Destroy(Instance* instance);
[[nodiscard]] uint32_t GetCodecType(const Instance* instance);
[[nodiscard]] Result Submit(Instance* instance, const Input& input, uint32_t* produced);
[[nodiscard]] Result Receive(Instance* instance, const FrameBuffer& frame_buffer, Output* output);
[[nodiscard]] Result Finalize(Instance* instance);
[[nodiscard]] Result Decode(Instance* instance, const Input& input, const FrameBuffer& frame_buffer,
Output* output);
[[nodiscard]] Result Flush(Instance* instance, const FrameBuffer& frame_buffer, Output* output);

130
src/loader/loadExec.cpp Normal file
View File

@ -0,0 +1,130 @@
#include "common/file.h"
#include "common/logging/log.h"
#include "emulator.h"
#include "kernel/fileSystem.h"
#include "libs/errno.h"
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <string_view>
#include <vector>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#elif defined(__linux__)
#include <linux/close_range.h>
#include <unistd.h>
#endif
namespace Emulator {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
static std::wstring QuoteArgument(std::wstring_view argument) {
std::wstring quoted = L"\"";
size_t slashes = 0;
for (wchar_t c: argument) {
if (c == L'\\') {
++slashes;
continue;
}
quoted.append(c == L'"' ? slashes * 2 + 1 : slashes, L'\\');
quoted += c;
slashes = 0;
}
quoted.append(slashes * 2, L'\\');
return quoted + L'"';
}
#endif
int LoadExec(const char* path, const char* const argv[]) {
using namespace Libs::SystemService;
if (path == nullptr || !std::string_view(path).starts_with("/app0/") ||
!Common::File::IsFileExisting(Libs::LibKernel::FileSystem::GetRealFilename(path))) {
return SYSTEM_SERVICE_ERROR_PARAMETER;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS || defined(__linux__)
std::vector<std::string> arguments {"--guest-exec", path};
size_t bytes = 0;
for (size_t i = 0; argv != nullptr && argv[i] != nullptr; ++i) {
const auto size = strnlen(argv[i], 4096 - bytes) + 1;
if (size > 4096 - bytes) {
return SYSTEM_SERVICE_ERROR_PARAMETER;
}
bytes += size;
arguments.emplace_back(argv[i], size - 1);
}
LOGF("LoadExec: %s\n", path);
Log::Flush();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
wchar_t executable[32768] {};
const auto length = GetModuleFileNameW(nullptr, executable, 32768);
if (length == 0 || length >= 32768) {
return SYSTEM_SERVICE_ERROR_INTERNAL;
}
if (__wargv == nullptr) {
return SYSTEM_SERVICE_ERROR_INTERNAL;
}
std::wstring command = QuoteArgument(executable);
for (int i = 1; i < __argc && __wargv[i] != nullptr; ++i) {
if (std::wstring_view(__wargv[i]) == L"--guest-exec") {
break;
}
command += L" " + QuoteArgument(__wargv[i]);
}
for (const auto& argument: arguments) {
const int size =
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, argument.c_str(), -1, nullptr, 0);
if (size == 0) {
return SYSTEM_SERVICE_ERROR_PARAMETER;
}
std::wstring wide(size, L'\0');
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, argument.c_str(), -1, wide.data(),
size) == 0) {
return SYSTEM_SERVICE_ERROR_PARAMETER;
}
wide.resize(size - 1);
command += L" " + QuoteArgument(wide);
}
STARTUPINFOW startup {};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process {};
if (!CreateProcessW(executable, command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
nullptr, nullptr, &startup, &process)) {
LOGF("LoadExec: process creation failed: %lu\n", GetLastError());
return SYSTEM_SERVICE_ERROR_INTERNAL;
}
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
std::fflush(nullptr);
std::quick_exit(0);
#else
std::ifstream command("/proc/self/cmdline", std::ios::binary);
std::vector<std::string> host_arguments;
for (std::string argument;
std::getline(command, argument, '\0') && argument != "--guest-exec";) {
host_arguments.push_back(std::move(argument));
}
if (host_arguments.empty()) {
return SYSTEM_SERVICE_ERROR_INTERNAL;
}
host_arguments.insert(host_arguments.end(), arguments.begin(), arguments.end());
std::vector<char*> host_argv;
for (auto& argument: host_arguments) {
host_argv.push_back(argument.data());
}
host_argv.push_back(nullptr);
if (close_range(3, ~0u, CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC) == 0) {
execvp(host_argv[0], host_argv.data());
}
LOGF("LoadExec failed: %s\n", std::strerror(errno));
#endif
return SYSTEM_SERVICE_ERROR_INTERNAL;
#else
return SYSTEM_SERVICE_ERROR_UNAVAILABLE;
#endif
}
} // namespace Emulator

View File

@ -25,6 +25,7 @@
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@ -88,16 +89,6 @@ ThreadLocalStorage::~ThreadLocalStorage() {
}
}
#pragma pack(1)
struct EntryParams {
int argc;
uint32_t pad;
const char* argv[3];
};
#pragma pack()
using atexit_func_t = KYTY_SYSV_ABI void (*)();
using entry_func_t = KYTY_SYSV_ABI void (*)(EntryParams* params, atexit_func_t atexit_func);
using module_ini_fini_func_t = KYTY_SYSV_ABI int (*)(size_t args, const void* argp,
@ -1283,11 +1274,16 @@ void RuntimeLinker::Execute(const std::filesystem::path& game_patch) {
LOGF_COLOR(Log::Color::BrightYellow, "---\n--- Execute: %s\n---\n", "Main");
if (auto entry = GetEntry(); entry != 0) {
const auto& arguments = Config::GetGuestArguments();
const auto params_size = offsetof(EntryParams, argv) +
(arguments.size() + 2) * sizeof(const char*);
auto* params = reinterpret_cast<EntryParams*>(
(reinterpret_cast<uintptr_t>(main_stack_top) - 0x100u) & ~static_cast<uintptr_t>(0x0f));
std::memset(params, 0, sizeof(EntryParams));
params->argc = 1;
params->argv[0] = "KytyEmu";
(reinterpret_cast<uintptr_t>(main_stack_top) - params_size - 0x100u) &
~static_cast<uintptr_t>(0x0f));
std::memset(params, 0, params_size);
for (const auto& argument: arguments) {
params->argv[params->argc++] = argument.c_str();
}
LOGF("stack_addr = %" PRIx64 "\n", reinterpret_cast<uint64_t>(params));

View File

@ -14,6 +14,12 @@
namespace Loader {
struct EntryParams {
int argc;
uint32_t pad;
const char* argv[1];
};
class Elf64;
struct Elf64_Sym;
struct Elf64_Rela;

View File

@ -164,6 +164,8 @@ void Psf::OpenJson(const std::filesystem::path& file_name) {
add_string_param("contentId", "CONTENT_ID");
add_string_param("contentVersion", "APP_VER");
add_string_param("appVersion", "APP_VER");
AddParamInt("ATTRIBUTE", 0);
add_int_param("attribute", "ATTRIBUTE");
add_int_param("attribute3", "ATTRIBUTE3");
add_int_param("userDefinedParam1", "USER_DEFINED_PARAM_1");
add_int_param("userDefinedParam2", "USER_DEFINED_PARAM_2");
@ -360,10 +362,12 @@ void PlayGo::Open(const std::filesystem::path& file_name) {
}
uint32_t magic1 = 0;
constexpr uint32_t PLGO_MAGIC = 0x6f676c70;
constexpr uint32_t PLGX_MAGIC = 0x78676c70;
m_f.Read(&magic1, 4);
if (magic1 != 0x6f676c70) {
if (magic1 != PLGO_MAGIC && magic1 != PLGX_MAGIC) {
LOGF("invalid file: magic1 = %08" PRIx32 "\n", magic1);
return;
}

View File

@ -61,13 +61,16 @@ static void PrintUsage() {
::printf(
" --gpu <index> Vulkan physical device index. Default: auto.\n");
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
::printf(" --hdr Enable HDR10 output when supported.\n");
::printf(
" --hide-cursor Hide the cursor after 2 s idle. Default: off.\n");
::printf(" --no-splash Disable the startup splash screen.\n");
::printf(" --vr Enable the virtual VR headset.\n");
::printf(" --amd-cpu Apply AMD CPU instruction patches.\n");
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n");
::printf(" --vulkan-validation <true|false> Enable Vulkan validation.\n");
::printf(" --network-connected <true|false> Use the host local network (default: false).\n");
::printf(" --gpu-assisted-validation <t|f> Bounds-check shader accesses on the GPU.\n"
" Implies --vulkan-validation; very slow.\n");
::printf(" --shader-validation <true|false> Enable shader validation.\n");
@ -214,11 +217,21 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
continue;
}
if (arg == "--hdr") {
options.config.hdr_enabled = true;
continue;
}
if (arg == "--hide-cursor") {
options.config.hide_cursor_enabled = true;
continue;
}
if (arg == "--no-splash") {
options.config.splash_enabled = false;
continue;
}
if (arg == "--vr") {
options.config.vr_enabled = true;
continue;
@ -294,6 +307,10 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
value.c_str());
return false;
}
} else if (arg == "--guest-exec") {
options.elf = Common::PathFromUtf8(Common::FixFilenameSlash(value));
options.config.guest_arguments.assign(argv + i + 1, argv + argc);
break;
} else if (arg == "--game-patch") {
if (!options.game_patch.empty()) {
::printf("--game-patch can only be specified once\n");
@ -372,6 +389,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid console language: %s\n", value.c_str());
return false;
}
} else if (arg == "--network-connected") {
if (!ParseBool(value, options.config.network_connected)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--vulkan-validation") {
if (!ParseBool(value, options.config.vulkan_validation_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());

View File

@ -0,0 +1,15 @@
# The installer is copied outside the application before it runs. Keep it free of
# Qt and application DLL dependencies so it can replace those files on Windows.
if(MSVC)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
add_executable(kyty_updater main.cpp UpdateInstaller.cpp)
if(MSVC)
target_compile_options(kyty_updater PRIVATE /EHsc)
else()
target_compile_options(kyty_updater PRIVATE -fexceptions)
endif()
if(MINGW)
target_link_options(kyty_updater PRIVATE -static -municode)
endif()

View File

@ -0,0 +1,249 @@
#include "UpdateInstaller.h"
#include <algorithm>
#include <stdexcept>
#include <vector>
namespace Kyty::Updater {
namespace {
namespace fs = std::filesystem;
struct Entry {
fs::path relative;
fs::file_type type;
};
struct Plan {
fs::path source;
fs::path target;
std::vector<Entry> entries;
};
bool IsWithin(const fs::path& path, const fs::path& root) {
auto part = path.begin();
for (const auto& component: root) {
if (part == path.end() || *part++ != component) {
return false;
}
}
return true;
}
bool IsRelative(const fs::path& path) {
if (path.empty() || path.is_absolute() || path.has_root_name()) {
return false;
}
for (const auto& component: path) {
if (component == ".." || component == "." || component.empty()) {
return false;
}
}
return true;
}
fs::file_type Type(const fs::path& path) {
std::error_code error;
const auto status = fs::symlink_status(path, error);
if (error && error != std::errc::no_such_file_or_directory) {
throw fs::filesystem_error("Cannot inspect update path", path, error);
}
return status.type();
}
void CheckLink(const fs::path& path, const fs::path& root) {
const auto link = fs::read_symlink(path);
if (link.is_absolute() || !IsWithin(fs::canonical(path), root)) {
throw std::runtime_error("Update contains a link outside its installation: " +
path.string());
}
}
void CheckDestination(const fs::path& target, const Entry& entry) {
const auto destination = target / entry.relative;
const auto type = Type(destination);
if (type == fs::file_type::not_found) {
return;
}
if ((entry.type == fs::file_type::directory) != (type == fs::file_type::directory) ||
(type != fs::file_type::regular && type != fs::file_type::directory &&
type != fs::file_type::symlink)) {
throw std::runtime_error("Update would replace a different file type: " +
destination.string());
}
if (type == fs::file_type::symlink) {
CheckLink(destination, target);
}
}
Plan MakePlan(const fs::path& source, const fs::path& target, const fs::path& launcher) {
if (!IsRelative(launcher) || Type(source) != fs::file_type::directory ||
Type(target) != fs::file_type::directory) {
throw std::runtime_error(
"Update source, installation directory, or launcher path is invalid.");
}
Plan plan {fs::canonical(source), fs::canonical(target), {}};
if (IsWithin(plan.target, plan.source)) {
throw std::runtime_error("Update source must not contain the installation directory.");
}
// macOS releases also have flat runtime files; install only the signed bundle.
fs::path scope;
if (launcher.begin()->extension() == ".app") {
scope = *launcher.begin();
}
const auto source_root = scope.empty() ? plan.source : plan.source / scope;
if (Type(source_root) != fs::file_type::directory) {
throw std::runtime_error("The release does not contain the application bundle.");
}
const auto binary_directory = launcher.parent_path();
#ifdef _WIN32
const fs::path suffix = ".exe";
#else
const fs::path suffix;
#endif
for (const auto& required: {launcher, binary_directory / ("kyty_emulator" + suffix.string()),
binary_directory / ("kyty_updater" + suffix.string())}) {
if (Type(plan.source / required) != fs::file_type::regular) {
throw std::runtime_error("Release is missing an executable: " + required.string());
}
#ifndef _WIN32
const auto executable =
fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec;
if ((fs::status(plan.source / required).permissions() & executable) == fs::perms::none) {
throw std::runtime_error("Release executable lacks execute permissions: " +
required.string());
}
#endif
}
if (!scope.empty()) {
plan.entries.push_back({scope, fs::file_type::directory});
}
for (const auto& item: fs::recursive_directory_iterator(source_root)) {
const auto relative = item.path().lexically_relative(plan.source);
const auto type = item.symlink_status().type();
if (relative.begin()->string().starts_with(".kyty-update")) {
throw std::runtime_error("Release uses a reserved updater filename.");
}
if (type == fs::file_type::symlink) {
CheckLink(item.path(), source_root);
} else if (type != fs::file_type::directory && type != fs::file_type::regular) {
throw std::runtime_error("Release contains an unsupported file: " + relative.string());
}
plan.entries.push_back({relative, type});
}
// Checking directories before their children prevents destination symlink traversal.
std::sort(plan.entries.begin(), plan.entries.end(),
[](const Entry& left, const Entry& right) { return left.relative < right.relative; });
for (const auto& entry: plan.entries) {
CheckDestination(plan.target, entry);
}
return plan;
}
struct InstallLock {
fs::path path;
void Acquire(const fs::path& target) {
auto candidate = target / ".kyty-update-lock";
if (!fs::create_directory(candidate)) {
throw std::runtime_error(
"Another update is running, or a previous update was interrupted. "
"Close all updater processes, inspect retained backups, then remove " +
candidate.string());
}
path = std::move(candidate);
}
~InstallLock() {
if (!path.empty()) {
std::error_code ignored;
// An incomplete rollback leaves backups here and keeps the lock for recovery.
fs::remove(path, ignored);
}
}
};
struct Change {
fs::path relative;
bool old_moved = false;
bool installed = false;
};
} // namespace
Result Validate(const fs::path& source, const fs::path& target, const fs::path& launcher_relative) {
try {
MakePlan(source, target, launcher_relative);
// Check actual write access, including ACLs, and interrupted/concurrent updates.
InstallLock lock;
lock.Acquire(target);
return {true, {}};
} catch (const std::exception& error) {
return {false, error.what()};
}
}
Result Install(const fs::path& source, const fs::path& target, const fs::path& launcher_relative) {
InstallLock lock;
fs::path backup;
std::vector<Change> changes;
std::vector<fs::path> created_directories;
try {
const auto plan = MakePlan(source, target, launcher_relative);
lock.Acquire(plan.target);
backup = lock.path / "backup";
changes.reserve(plan.entries.size());
created_directories.reserve(plan.entries.size());
// The launcher extracts beside the installation, so files can move directly
// from that staging directory without another full copy of the release.
for (const auto& entry: plan.entries) {
CheckDestination(plan.target, entry);
const auto destination = plan.target / entry.relative;
if (entry.type == fs::file_type::directory) {
if (fs::create_directory(destination)) {
created_directories.push_back(destination);
}
continue;
}
changes.push_back({entry.relative});
auto& change = changes.back();
if (Type(destination) != fs::file_type::not_found) {
fs::create_directories((backup / entry.relative).parent_path());
fs::rename(destination, backup / entry.relative);
change.old_moved = true;
}
fs::rename(plan.source / entry.relative, destination);
change.installed = true;
}
std::error_code cleanup_error;
fs::remove_all(backup, cleanup_error);
return {true, cleanup_error ? "Update installed; backup cleanup failed: " + backup.string()
: std::string {}};
} catch (const std::exception& error) {
std::string message = error.what();
bool restored = true;
for (auto change = changes.rbegin(); change != changes.rend(); ++change) {
try {
const auto destination = target / change->relative;
if (change->installed) {
fs::rename(destination, source / change->relative);
}
if (change->old_moved) {
fs::rename(backup / change->relative, destination);
}
} catch (const std::exception& rollback_error) {
restored = false;
message += "\nRollback failed: " + std::string(rollback_error.what());
}
}
for (auto directory = created_directories.rbegin(); directory != created_directories.rend();
++directory) {
std::error_code ignored;
fs::remove(*directory, ignored);
}
if (!backup.empty()) {
if (restored) {
std::error_code ignored;
fs::remove_all(backup, ignored);
} else {
message += "\nBackup retained at: " + backup.string();
}
}
return {false, message};
}
}
} // namespace Kyty::Updater

View File

@ -0,0 +1,18 @@
#pragma once
#include <filesystem>
#include <string>
namespace Kyty::Updater {
struct Result {
bool success;
std::string message;
};
// Only release files are replaced. Files absent from the release are preserved.
// Successful installation consumes staged files; stage on the target filesystem.
Result Validate(const std::filesystem::path& source, const std::filesystem::path& target,
const std::filesystem::path& launcher_relative);
Result Install(const std::filesystem::path& source, const std::filesystem::path& target,
const std::filesystem::path& launcher_relative);
} // namespace Kyty::Updater

Some files were not shown because too many files have changed in this diff Show More