Compare commits

...
Author SHA1 Message Date
nmzik c8baa7b909 shader: derive immutable resource policy once per plan 2026-09-28 18:01:49 +04:00
nmzik 34a3eb6b58 memory: recycle automatic backing through physical ownership 2026-09-28 18:01:49 +04:00
nmzik a20b64d3e5 audio: rewind compressed sampler blocks at loop boundaries 2026-09-28 18:01:49 +04:00
nmzik 2016f8e2fe gpu: address extended guest memory without widening dense cache tables 2026-09-28 18:01:49 +04:00
nmzik c91fe80d64 memory: separate extended allocations from ordinary guest mappings 2026-09-28 18:01:49 +04:00
nmzik ab2c1c4c03 Update README screenshot to UFC 5 2026-09-28 18:01:49 +04:00
gohansharma79-svgandnmzik 0a5774be7b shader: support FP64 conversions used by FighterZ (#884)
shader: support float and unsigned widening to FP64

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-28 17:39:31 +04:00
nmzik 0791f921f6 UFC 5 in-game (#883)
* shader: release descriptor dependencies

* shader: simplify shared-exit control flow

* shader: fix scalar memory loads

* shader: fix LDS counter bounds

* renderer: skip inactive pixel shaders

* shader: respect FP32 mode in comparisons

* renderer: fix sampler border colors

* shader: resolve descriptors from control flow

* shader: resolve descriptors before resource planning

* shader: handle scalar guards for image bounds

* shader: handle reduced guards for image loops

* shader: fix wave guards and material indices

* shader: preserve scalar buffer offset alignment and carry

* shader: prove masked loop bounds across iteration transitions

* shader: preserve source and IR before resource tracking failures

* shader: track material keys selected from active lanes

* shader: remove obsolete post-dominator analysis

* shader: follow bounds guards through control-flow joins
2026-09-28 16:03:05 +04:00
nmzik 421684e75f docs: add weekly updates and Discord links to README (#866)
* docs: link weekly updates from README

* docs: add Discord development channel

* docs: use Discord server invite link

* docs: refine weekly updates wording
2026-09-27 18:06:26 +04:00
Elijaandnmzik 025b39c018 Fix empty query handling in HttpUriParse (#764)
* Fix empty query handling in HttpUriParse

* Handle empty URI query consistently

* Cover empty URI query handling

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-27 16:07:03 +04:00
João Loboandnmzik 7c8583d204 audio: implement the AJM Opus decoder (#858)
* audio: implement the AJM Opus decoder

* Audio: simplify Opus decoding and preserve packet boundaries

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-27 15:53:20 +04:00
9229c04ebf samplerCache: handle reserved sampler anisotropy ratios
Co-authored-by: EmK530 <admin@emk530.net>
Co-authored-by: Hamza Shakir <60213466+hamzashakir99@users.noreply.github.com>
2026-09-27 15:46:25 +04:00
Matthew Hurleyandnmzik 1b6af301b5 tests: wire dormant VOP3 lane-read check and guard new dormant tests (#840)
Tests: enable lane-read coverage and clarify layout failures

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-27 15:37:32 +04:00
nmzik e51f252b75 AvPlayer: preserve clip timestamps across playback loops 2026-09-27 15:23:36 +04:00
nmzik 0f1ff37f23 Graphics: materialize CMASK clears before publishing expanded metadata 2026-09-27 15:05:46 +04:00
nmzik da33a1c4a3 Graphics: respect physical tile layout when resolving depth aliases 2026-09-27 15:05:46 +04:00
nmzik f53f0ca483 Graphics: preserve rendered contents across 1D and 2D texture aliases 2026-09-27 15:05:46 +04:00
nmzik 7b6bfdec64 Graphics: share canonical padded layouts for single-sample color targets 2026-09-27 15:05:46 +04:00
nmzik a88181b897 Graphics: share texture layout for linear render-target mip chains 2026-09-27 11:10:02 +04:00
nmzik 041874f663 Graphics: resolve equivalent terminal mip views without expanding backing 2026-09-27 11:04:19 +04:00
nmzik 496432bbb4 Shader: skip identity image remapping on materialization hits 2026-09-27 10:47:18 +04:00
Pcniadoandnmzik bec67b7863 Fix HITMAN 3 boot, layered presentation, and socket imports (#851)
* videoout: synchronize layered flips with premultiplied composition

* shader: support unsigned FFBH subword sources

* shader: apply cube-face result scaling

* shader: prune buffer reads proven out of bounds

* net: implement send and receive exports with shared socket handling

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-27 02:13:30 +04:00
Devran Cosmo Uenalandnmzik a22a9d9348 audio: play DualSense haptics from vibration audio ports (#843)
* audio: play DualSense haptics from vibration audio ports

Games drive the DualSense's actuators through a vibration audio port, one channel per actuator.
These ports had no device, so their samples were dropped. Over USB the DualSense is a 4-channel
audio device whose back channels feed the actuators; play the port's samples there through SDL.

The device is looked up while the port plays, so the controller can be connected at any time. The
queue keeps about the latency of the regular audio path, with the playback rate nudged by up to 3%
when the port is fed off real time.

While haptic audio plays, hold a DualSense's rumble at zero: SDL disables audio haptics whenever
rumble is non-zero.

Add pad_haptics tests for the sample conversion, device-name matching and rate correction.

* Simplify DualSense audio haptics and restore idle rumble

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-27 00:37:27 +04:00
nmzik 81f312638f graphics: encode packed UNorm buffer stores before writing skinning normals 2026-09-26 23:42:19 +04:00
nmzik f1f930e06c fix(launcher): use native macOS game folder picker 2026-09-26 22:14:09 +04:00
Devran Cosmo Uenalandnmzik e0b682e4b2 AvPlayer: fix Crash Bandicoot 4 intro playback and green video padding (#848)
* AvPlayer: advance the shared audio-master clock after final audio frame

* AvPlayer: initialize padded NV12 planes with neutral black

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-26 20:39:46 +04:00
nmzik 16b83a034c Shader: execute RDNA2 FP64 shadow arithmetic and log denorm limits
Shader: disable FP64 denorm preservation controls for now
2026-09-26 15:15:35 +04:00
cf268af839 Shader: implement RDNA2 image atomic float maximum
Shader: implement RDNA2 image atomic float minimum

Co-authored-by: foufouadi <attar.foued@gmail.com>
Co-authored-by: frans <francois.att@gmail.com>
Co-authored-by: fxpw <fxpw@outlook.com>
Co-authored-by: Matthew Hurley <171001439+theantipopau@users.noreply.github.com>
2026-09-26 15:15:35 +04:00
Mehmet Cambaz fd2e15ee73 ci: run the recompiler unit tests (#447)
Build and run the CPU-only resource_tracking, scalar_provenance, resource_materialization, and shader_vertex_metadata tests on Windows, macOS, and Linux. Each test executable is excluded from the default build, so add it to the explicit CMake targets and the matching CTest selection. Leave the Vulkan-dependent shader_recompiler_compute suite for a separate CI setup.
2026-09-26 03:00:01 +04:00
Saadiandnmzik 4e8d8c849d tests: add LeastRecentlyUsedCache regression tests (#671)
* launcher: keep primary update feed answer when the fallback request fails

When the primary feed is reachable but its tag differs from the build tag,
the result was discarded and the GitHub API fallback was consulted. If the
fallback request failed (api.github.com rate-limits unauthenticated
requests), the launcher reported an error even though the primary feed had
already returned a usable update answer with a working release URL. Keep
the primary feed answer and show it if the fallback fails.

* emulator: validate numeric CLI arguments

--screen-width, --screen-height, --gpu, and --vblank-frequency were parsed
with Common::ToInt32/ToInt64, which ignores std::from_chars errors and
returns 0, so values like "--screen-width abc" or "--gpu xyz" were
silently accepted: zero screen size, or GPU 0 selected instead of auto
(-1). Reject invalid values with an error message like the other options,
using from_chars-based helpers (ParseUint32/ParseInt32); reject zero
screen dimensions.

* tests: add LeastRecentlyUsedCache regression tests

Cover the LRU cache walk order, tick filtering in ForEachItemBelow,
early-exit boolean callbacks, Touch reordering and same-tick skips,
Free, and freed-id reuse by later inserts. Modeled on BitArrayTests.

* tests: use a tick cutoff that detects a stale newest-item tick

Collect(cache, 35) also visits c when Touch(c, 35) failed to update
its tick (30 <= 35). Use a cutoff of 30 and expect only b.

* Run LRU cache tests in CI

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-26 02:42:31 +04:00
Matthew Hurleyandnmzik 1e438136fa Build save_data_memory_tests from the kyty_tests aggregate (#798)
Build save_data_memory_tests with kyty_tests

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-26 02:39:20 +04:00
Emilandnmzik 313b60e922 Capture PCM at set time when multiple AudioOut2 ports share a buffer (#826)
* capture PCM at set time when ports share a buffer

* Snapshot size limit and open device check

* Fall back to PCM pointer on stale snapshot

* audio: own AudioOut2 PCM at attribute set time

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-26 02:19:49 +04:00
Hamza Shakir cda236dbb3 Shader recompiler: accept V_NOT_B32 SDWA partial dst with any source; allow 64 buffers
SDWA source extraction and destination insertion are independent. Cover the reported word1-to-word0 preserve form and byte-source partial destinations in compute tests. Verify 64 distinct compute buffers survive resource tracking and binding layout, and remove the obsolete CFG rejection.

(cherry picked from commit 9e74df7dc0)
2026-09-26 01:58:55 +04:00
1216fa36cf docs: fix stale README notes, document building without Qt (#812)
* docs: fix stale README notes, document building without Qt

- Use the current issue template name, Game Emulation Status Report
  (renamed in 027433f).
- Raise the documented Windows CMake minimum from 3.12 to 3.22.1, which
  the bundled SPIRV-Tools requires.
- Note that -DKYTY_BUILD_LAUNCHER=OFF builds kyty_emulator and the
  regression tests without Qt (#193).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* docs: add Qt-free Linux build commands

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-26 01:50:06 +04:00
nmzik 497166009c loader: unlink exact module imports before releasing their mapping 2026-09-25 21:15:41 +04:00
nmzik 0fb3ac5605 kernel: retain modules until their final explicit unload 2026-09-25 21:15:41 +04:00
nmzik e056d55dd5 loader: consolidate eager symbol resolution and remove dead PLT machinery 2026-09-25 21:15:41 +04:00
nmzik 1371219f5c loader: preserve resolved relocations across dynamic loads 2026-09-25 21:15:41 +04:00
nmzik cf423df624 shader: remove per-draw vertex diagnostic atomics 2026-09-25 21:15:41 +04:00
nmzik 5ce4f08364 audio: improve NGS2 stream and ATRAC9 playback 2026-09-25 18:26:08 +04:00
nmzik 7e1c2d1532 avplayer: auto-start playback without an event callback 2026-09-25 16:39:54 +04:00
nmzik 0ec43a8d3d graphics: name Vulkan images and views only on creation 2026-09-25 16:15:05 +04:00
nmzikandMac bfdbba3aa4 graphics: share EOP clock and immediate write handling
Co-authored-by: Mac <me@itsmemac.com>
2026-09-25 16:14:40 +04:00
Devran Cosmo Uenalandnmzik f7ae6e0a83 audio: support NGS2 PCM16 samplers and rear low-pass filters (#825)
* audio: support standard NGS2 PCM16 sampler playback

Extend the existing fixed-point PCM path to standard samplers, loops, and progress reporting. Apply voice events immediately and honor callback interruption during source advancement.

Reuse upstream rate and pitch handling. Add regression coverage for streaming, routing, callbacks, loops, and custom sampler controls; ngs2_sampler passes.

* audio: apply NGS2 rear low-pass filters

Handle rear low-pass FCQ controls at the output sample rate with per-channel history and channel masks. Preserve filter tails through streaming gaps and use the standard pi constant for coefficients.

Add regression coverage for frequency response, control layout, channel isolation, and starvation/refill continuity. Emulator build and both audio test suites pass.

* Preserve resampling phase for accepting voices

Only reset voice.sample_phase when there are no blocks and the voice does not accept blocks (change in src/libs/ngs2.cpp). Add TestResamplingPhaseAfterStarvation to tests to ensure sampler and custom sampler retain resampling phase and pending source advancement after starvation, and that SetupSampler still resets the phase. This prevents resampling phase loss during starvation/refill across sample rates.

* audio: correct sampler controls, lifecycle, and filter tails

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-25 15:44:58 +04:00
113 changed files with 9023 additions and 4030 deletions

No files matched your search

+13 -6
View File
@@ -112,12 +112,15 @@ jobs:
- name: Build
shell: cmd
run: |
cmake --build _Build/windows --target launcher audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests --parallel
cmake --build _Build/windows --target launcher ^
audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
resource_tracking_tests scalar_provenance_tests ^
resource_materialization_tests shader_vertex_metadata_tests --parallel
- name: Test
shell: cmd
run: |
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|ime_dialog|virtual_memory_allocation)$"
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$"
- name: Install
shell: cmd
@@ -198,14 +201,16 @@ jobs:
shell: bash
run: |
cmake --build _Build/macos \
--target launcher audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests \
--target launcher audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
resource_tracking_tests scalar_provenance_tests \
resource_materialization_tests shader_vertex_metadata_tests \
--parallel
- name: Test
shell: bash
run: |
ctest --test-dir _Build/macos --output-on-failure \
-R '^(audio_out2_port|ime_dialog|virtual_memory_allocation)$'
-R '^(audio_out2_port|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$'
- name: Install
shell: bash
@@ -379,14 +384,16 @@ jobs:
run: |
cmake --build _Build/linux \
--target launcher page_manager_tests memory_tracker_tests \
audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests \
audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
resource_tracking_tests scalar_provenance_tests \
resource_materialization_tests shader_vertex_metadata_tests \
--parallel
- name: Test
shell: bash
run: |
ctest --test-dir _Build/linux --output-on-failure \
-R '^(audio_out2_port|ime_dialog|page_manager|memory_tracker|virtual_memory_allocation)$'
-R '^(audio_out2_port|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$'
- name: Install
shell: bash
+31
View File
@@ -301,6 +301,7 @@ 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)
@@ -411,6 +412,11 @@ add_executable(bit_array_tests EXCLUDE_FROM_ALL
)
target_include_directories(bit_array_tests PRIVATE ${inc_headers})
add_executable(lru_cache_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/LruCacheTests.cpp"
)
target_include_directories(lru_cache_tests PRIVATE ${inc_headers})
add_executable(ime_dialog_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/ImeDialogTests.cpp"
"${KYTY_SOURCE_DIR}/libs/imeCommon.cpp"
@@ -465,12 +471,28 @@ add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
target_link_libraries(audio_out2_port_tests common fmt::fmt)
target_include_directories(audio_out2_port_tests PRIVATE ${inc_headers})
add_executable(ngs2_sampler_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/Ngs2SamplerTests.cpp"
"${KYTY_SOURCE_DIR}/loader/timer.cpp"
)
target_link_libraries(ngs2_sampler_tests common fmt::fmt LibAtrac9)
target_include_directories(ngs2_sampler_tests PRIVATE ${inc_headers})
add_executable(pad_haptics_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/PadHapticsTests.cpp"
)
target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static)
target_include_directories(pad_haptics_tests PRIVATE ${inc_headers})
add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/SaveDataMemoryTests.cpp"
)
target_link_libraries(save_data_memory_tests common fmt::fmt)
target_include_directories(save_data_memory_tests PRIVATE ${inc_headers})
add_kyty_full_emulator_test(http_uri_parse_tests
"${KYTY_TESTS_DIR}/HttpUriParseTests.cpp")
add_executable(event_queue_lifetime_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/EventQueueLifetimeTests.cpp"
"${KYTY_SOURCE_DIR}/kernel/eventQueue.cpp"
@@ -548,6 +570,7 @@ if(BUILD_TESTING)
add_test(NAME memory_tracker COMMAND $<TARGET_FILE:memory_tracker_tests>)
add_test(NAME page_manager COMMAND $<TARGET_FILE:page_manager_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>)
add_test(NAME resource_materialization COMMAND $<TARGET_FILE:resource_materialization_tests>)
add_test(NAME resource_tracking COMMAND $<TARGET_FILE:resource_tracking_tests>)
@@ -555,7 +578,10 @@ if(BUILD_TESTING)
add_test(NAME kernel_file_system COMMAND $<TARGET_FILE:kernel_file_system_tests>)
add_test(NAME sync_on_address COMMAND $<TARGET_FILE:sync_on_address_tests>)
add_test(NAME audio_out2_port COMMAND $<TARGET_FILE:audio_out2_port_tests>)
add_test(NAME ngs2_sampler COMMAND $<TARGET_FILE:ngs2_sampler_tests>)
add_test(NAME pad_haptics COMMAND $<TARGET_FILE:pad_haptics_tests>)
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
add_test(NAME http_uri_parse COMMAND $<TARGET_FILE:http_uri_parse_tests>)
add_test(NAME shader_recompiler_compute COMMAND $<TARGET_FILE:shader_recompiler_compute_tests>)
add_test(NAME shader_recompiler_alignbyte
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --alignbyte-only)
@@ -611,6 +637,7 @@ if(BUILD_TESTING)
memory_tracker_tests
page_manager_tests
bit_array_tests
lru_cache_tests
shader_vertex_metadata_tests
resource_materialization_tests
resource_tracking_tests
@@ -620,6 +647,10 @@ if(BUILD_TESTING)
shader_recompiler_compute_tests
virtual_memory_allocation_tests
audio_out2_port_tests
ngs2_sampler_tests
pad_haptics_tests
save_data_memory_tests
http_uri_parse_tests
)
endif()
+21 -5
View File
@@ -7,6 +7,10 @@
[![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.
**[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
[Kyty](https://github.com/InoriRus/Kyty). The project is in active development, and behavior
@@ -68,8 +72,8 @@ or graphical glitches, so please include the version you tested when reporting a
<img src="docs/screenshots/ps5-02.png" width="300" alt="Demon's Souls running in KytyPS5">
</td>
<td align="center">
<strong>Hellboy</strong><br>
<img src="docs/screenshots/ps5-06.png" width="300" alt="Hellboy running in KytyPS5">
<strong>UFC 5</strong><br>
<img src="docs/screenshots/ps5-06.png" width="300" alt="UFC 5 running in KytyPS5">
</td>
</tr>
</table>
@@ -79,7 +83,7 @@ or graphical glitches, so please include the version you tested when reporting a
## Contributing
Testing games and submitting detailed bug reports are useful ways to contribute. Search existing
issues first, then use the **Game Emulation Bug Report** template and attach the complete log file.
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
@@ -132,7 +136,7 @@ the Vulkan/SPIR-V validation rules.
### Build requirements (Windows)
- Git
- CMake 3.12 or newer
- CMake 3.22.1 or newer
- Ninja
- Visual Studio 2022 or Build Tools 2022 with the **Desktop development with C++** workload and
**C++ Clang tools for Windows** component
@@ -178,7 +182,7 @@ 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 also required — either the distribution packages
Qt 6 (Concurrent, Network, Widgets) is required for the launcher — either the distribution packages
(`qt6-base-dev`) or an official Qt installation.
```bash
@@ -197,6 +201,18 @@ The install step copies the Qt libraries and plugins next to the binaries, so
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.
To build `kyty_emulator` and the `kyty_tests` target without Qt, use a separate build directory:
```bash
git submodule update --init --recursive
cmake -S . -B _Build/linux-no-qt -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ \
-DKYTY_BUILD_LAUNCHER=OFF
cmake --build _Build/linux-no-qt --target kyty_emulator kyty_tests --parallel
```
As on Windows, the MSVC compiler is not used; Clang is required. `cl.exe` is rejected at configure
time.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

+8 -42
View File
@@ -1205,8 +1205,9 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
}
break;
case 0x02:
case 0x04:
if constexpr (sizeof(T) == sizeof(uint32_t)) {
if (eop_event_type == 0x2f && event_index == 0x06) {
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;
@@ -1214,6 +1215,9 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
}
}
} else {
if (event_write_source == 0x04) {
value = Sync::ReadReferenceClock();
}
auto write64 = [&](bool with_writeback) {
auto* dst = static_cast<uint64_t*>(dst_gpu_addr);
std::memcpy(dst, &value, sizeof(value));
@@ -1270,7 +1274,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
case 0x14:
case 0x28:
if (((eop_event_type == 0x04 || eop_event_type == 0x28) &&
event_index == 0x05 && !with_interrupt) ||
event_index == 0x05) ||
(event_index == 0x00)) {
write64(true);
return;
@@ -1302,45 +1306,6 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
}
}
break;
case 0x04:
if constexpr (sizeof(T) == sizeof(uint64_t)) {
const auto clock = Sync::ReadReferenceClock();
auto* dst = static_cast<uint64_t*>(dst_gpu_addr);
std::memcpy(dst, &clock, sizeof(clock));
switch (cache_action) {
case 0x00:
if ((eop_event_type == 0x04 && event_index == 0x05) ||
(eop_event_type == 0x28 && event_index == 0x00)) {
if (with_interrupt) {
Sync::WriteAtEndOfPipeWithInterrupt64(
m_submit_id, command, dst, clock, m_interrupt_event_id,
interrupt_context_id);
} else {
Sync::WriteAtEndOfPipeClockCounter(m_submit_id, command,
dst, clock);
}
return;
}
break;
case 0x38:
if ((eop_event_type == 0x04 &&
(event_index == 0x00 || event_index == 0x05)) ||
(eop_event_type == 0x28 && event_index == 0x00)) {
if (with_interrupt) {
Sync::WriteAtEndOfPipeWithInterruptWriteBack64(
m_submit_id, command, dst, clock, m_interrupt_event_id,
interrupt_context_id);
} else {
Sync::WriteAtEndOfPipeClockCounterWithWriteBack(
m_submit_id, command, dst, clock);
}
return;
}
break;
default: break;
}
}
break;
default: break;
}
@@ -1537,8 +1502,9 @@ void CommandProcessor::PrepareCpuFlip(uint64_t request_id) {
ProcessorScope processor_scope(*this);
m_renderer.GetVideoOut().PrepareFlip(request_id, command);
GetScheduler().DeferPriorityOperation(
[this, request_id] { m_renderer.GetVideoOut().CompleteFlip(request_id); });
GetScheduler().Flush();
m_renderer.GetVideoOut().CompleteFlip(request_id);
}
void CommandProcessor::SynchronizeGpu() {
+22 -39
View File
@@ -1213,20 +1213,12 @@ bool TileGetRenderTargetSize(uint32_t width, uint32_t height, uint32_t pitch,
return true;
}
bool TileGetDccSize(uint32_t width, uint32_t height, uint32_t slices,
uint32_t bytes_per_element, uint32_t levels, Prospero::TileMode tile,
TileSizeAlign& total_size, uint32_t num_fragments_log2) {
total_size = {};
if (width == 0 || height == 0 || slices == 0 || levels != 1 || num_fragments_log2 != 0 ||
!std::has_single_bit(bytes_per_element) || bytes_per_element > 16 ||
(tile != Prospero::TileMode::kRenderTarget && tile != Prospero::TileMode::kDepth)) {
static bool TileGetColorMetadataSize(uint32_t width, uint32_t height, uint32_t slices,
uint32_t block_width, uint32_t block_height,
TileSizeAlign& total_size) {
if (width == 0 || height == 0 || slices == 0) {
return false;
}
// Gen5 color metadata uses 4 KiB blocks, with one byte per 256 bytes of color data.
// Metadata stays thin for volume surfaces, so every depth slice has its own block raster.
const uint32_t coverage_bits = 20u - std::countr_zero(bytes_per_element);
const uint32_t block_width = 1u << ((coverage_bits + 1u) / 2u);
const uint32_t block_height = 1u << (coverage_bits / 2u);
const uint64_t blocks_x = (static_cast<uint64_t>(width) + block_width - 1u) / block_width;
const uint64_t blocks_y = (static_cast<uint64_t>(height) + block_height - 1u) / block_height;
const uint64_t blocks = blocks_x * blocks_y;
@@ -1238,36 +1230,27 @@ bool TileGetDccSize(uint32_t width, uint32_t height, uint32_t slices,
return true;
}
bool TileGetRenderTargetMipLayout(uint32_t width, uint32_t height, uint32_t pitch,
uint32_t bytes_per_element, uint32_t levels,
TileSizeAlign& total_size, TileSizeOffset* level_sizes,
TilePaddedSize* padded_size) {
bool TileGetDccSize(uint32_t width, uint32_t height, uint32_t slices,
uint32_t bytes_per_element, uint32_t levels, Prospero::TileMode tile,
TileSizeAlign& total_size, uint32_t num_fragments_log2) {
total_size = {};
if (width == 0 || height == 0 || levels == 0 || levels > 16 ||
pitch != TileGetRenderTargetPitch(width, bytes_per_element)) {
if (levels != 1 || num_fragments_log2 != 0 ||
!std::has_single_bit(bytes_per_element) || bytes_per_element > 16 ||
(tile != Prospero::TileMode::kRenderTarget && tile != Prospero::TileMode::kDepth)) {
return false;
}
uint32_t max_levels = 1;
uint32_t max_dimension = std::max(width, height);
while (max_dimension > 1) {
max_dimension >>= 1u;
max_levels++;
}
if (levels > max_levels) {
return false;
}
auto format = Prospero::BufferFormat::kInvalid;
switch (bytes_per_element) {
case 1: format = Prospero::BufferFormat::k8UNorm; break;
case 2: format = Prospero::BufferFormat::k16UNorm; break;
case 4: format = Prospero::BufferFormat::k32Float; break;
case 8: format = Prospero::BufferFormat::k16_16_16_16Float; break;
case 16: format = Prospero::BufferFormat::k32_32_32_32Float; break;
default: return false;
}
TileGetTextureSize(format, width, height, levels, Prospero::TileMode::kRenderTarget,
&total_size, level_sizes, padded_size);
return total_size.size != 0 && total_size.align == 65536;
// Gen5 DCC uses 4 KiB blocks, with one byte per 256 bytes of color data.
// Metadata stays thin for volume surfaces, so every depth slice has its own block raster.
const uint32_t coverage_bits = 20u - std::countr_zero(bytes_per_element);
return TileGetColorMetadataSize(width, height, slices, 1u << ((coverage_bits + 1u) / 2u),
1u << (coverage_bits / 2u), total_size);
}
bool TileGetCmaskSize(uint32_t width, uint32_t height, uint32_t slices, uint32_t levels,
TileSizeAlign& total_size) {
total_size = {};
// Gen5 CMASK stores four bits per 8x8 tile in 4 KiB blocks covering 1024x512 pixels.
return levels == 1 && TileGetColorMetadataSize(width, height, slices, 1024, 512, total_size);
}
void TileGetTextureSize(Prospero::BufferFormat format, uint32_t width, uint32_t height,
+2 -4
View File
@@ -126,10 +126,8 @@ bool TileGetRenderTargetSize(uint32_t width, uint32_t height, uint32_t pitch
bool TileGetDccSize(uint32_t width, uint32_t height, uint32_t slices,
uint32_t bytes_per_element, uint32_t levels, Prospero::TileMode tile,
TileSizeAlign& total_size, uint32_t num_fragments_log2 = 0);
bool TileGetRenderTargetMipLayout(uint32_t width, uint32_t height, uint32_t pitch,
uint32_t bytes_per_element, uint32_t levels,
TileSizeAlign& total_size, TileSizeOffset* level_sizes,
TilePaddedSize* padded_size);
bool TileGetCmaskSize(uint32_t width, uint32_t height, uint32_t slices, uint32_t levels,
TileSizeAlign& total_size);
void TileGetTextureSize(Prospero::BufferFormat format, uint32_t width, uint32_t height,
uint32_t levels, Prospero::TileMode tile, TileSizeAlign* total_size,
TileSizeOffset* level_sizes, TilePaddedSize* padded_size);
+1
View File
@@ -29,6 +29,7 @@ struct GraphicContext {
bool memory_budget_ext_enabled = false;
bool compute_subgroup_size_control_enabled = false;
bool sample_rate_shading_enabled = false;
// bool fp64_denorm_preserve = false; // Temporarily disabled.
bool attachment_feedback_loop_enabled = false;
bool provoking_vertex_last_enabled = false;
bool supports_block_texel_view = false;
+8 -3
View File
@@ -3,6 +3,7 @@
#include "common/bitArray.h"
#include "common/common.h"
#include "kernel/memory.h"
#include <compare>
@@ -10,7 +11,8 @@ namespace Libs::Graphics {
constexpr uint64_t TRACKER_PAGE_SIZE = 4ull * 1024ull;
constexpr uint64_t TRACKER_REGION_SIZE = 4ull * 1024ull * 1024ull;
constexpr uint64_t TRACKER_ADDRESS_SIZE = 1ull << 40u;
constexpr uint64_t TRACKER_ADDRESS_SIZE = 1ull << 44u;
constexpr uint64_t LOWER_ADDRESS_SIZE = 1ull << 40u;
constexpr size_t TRACKER_REGION_PAGES = TRACKER_REGION_SIZE / TRACKER_PAGE_SIZE;
struct GuestRange {
@@ -19,8 +21,11 @@ struct GuestRange {
[[nodiscard]] constexpr bool Empty() const noexcept { return address == 0 && size == 0; }
[[nodiscard]] constexpr bool Valid() const noexcept {
return address != 0 && size != 0 && address < TRACKER_ADDRESS_SIZE &&
size <= TRACKER_ADDRESS_SIZE - address;
if (address == 0 || size == 0) return false;
if (address < LOWER_ADDRESS_SIZE) return size <= LOWER_ADDRESS_SIZE - address;
constexpr auto base = LibKernel::Memory::kExtendedMemoryBase;
constexpr auto length = LibKernel::Memory::kExtendedMemorySize;
return address >= base && address - base < length && size <= length - (address - base);
}
[[nodiscard]] constexpr bool ValidOrEmpty() const noexcept { return Empty() || Valid(); }
[[nodiscard]] constexpr uint64_t End() const noexcept { return address + size; }
+11 -5
View File
@@ -55,7 +55,8 @@ template <bool insert>
void BufferCache::ChangeRegister(BufferId id) {
auto& buffer = m_slot_buffers[id];
PageTable::PageRange pages {};
EXIT_IF(!PageTable::TryGetPageRange(buffer.CpuAddress(), buffer.Size(), pages));
EXIT_IF(!(GuestRange {buffer.CpuAddress(), buffer.Size()}.Valid()) ||
!PageTable::TryGetPageRange(buffer.CpuAddress(), buffer.Size(), pages));
for (size_t page = pages.first; page < pages.last_exclusive; ++page) {
if constexpr (insert) {
m_page_table[page] = id;
@@ -64,6 +65,7 @@ void BufferCache::ChangeRegister(BufferId id) {
}
}
const auto size_pages = pages.last_exclusive - pages.first;
const auto table_offset = PageIndex(buffer.CpuAddress()) * sizeof(vk::DeviceAddress);
if constexpr (insert) {
const auto [it, inserted] = m_buffers.emplace(buffer.CpuAddress(), id);
(void)it;
@@ -75,7 +77,7 @@ void BufferCache::ChangeRegister(BufferId id) {
for (uint64_t i = 0; i < size_pages; ++i) {
addresses.push_back(buffer.BufferDeviceAddress() + (i << CACHING_PAGEBITS));
}
WriteDataBuffer(m_bda_pagetable_buffer, pages.first * sizeof(vk::DeviceAddress),
WriteDataBuffer(m_bda_pagetable_buffer, table_offset,
addresses.data(), addresses.size() * sizeof(vk::DeviceAddress));
} else {
const auto found = m_buffers.find(buffer.CpuAddress());
@@ -84,7 +86,7 @@ void BufferCache::ChangeRegister(BufferId id) {
EXIT_IF(buffer.Size() > m_total_used_memory);
m_total_used_memory -= buffer.Size();
m_lru_cache.Free(buffer.lru_id);
m_bda_pagetable_buffer.Fill(pages.first * sizeof(vk::DeviceAddress),
m_bda_pagetable_buffer.Fill(table_offset,
size_pages * sizeof(vk::DeviceAddress), 0);
buffer.is_deleted = true;
}
@@ -312,10 +314,14 @@ BufferCache::OverlapResult BufferCache::ResolveOverlaps(uint64_t vaddr, uint64_t
// Reserve space in the incoming stream's direction of growth.
// The old buffer extending left of the request predicts growth to the right, and vice versa.
if (expands_left) {
end += std::min(StreamLeapSize, PageTable::kAddressSpaceSize - end);
end += std::min(StreamLeapSize, (vaddr < LOWER_ADDRESS_SIZE ? LOWER_ADDRESS_SIZE
: LibKernel::Memory::kExtendedMemoryBase +
LibKernel::Memory::kExtendedMemorySize) - end);
}
if (expands_right) {
const auto minimum = CACHING_PAGESIZE * 2;
const auto minimum = vaddr < LOWER_ADDRESS_SIZE
? CACHING_PAGESIZE * 2
: LibKernel::Memory::kExtendedMemoryBase;
if (begin > minimum) {
begin -= std::min(StreamLeapSize, begin - minimum);
}
+14 -2
View File
@@ -29,10 +29,22 @@ class BufferCache {
public:
static constexpr uint32_t CACHING_PAGEBITS = 14;
static constexpr uint64_t CACHING_PAGESIZE = uint64_t {1} << CACHING_PAGEBITS;
static constexpr uint64_t CACHING_NUMPAGES = uint64_t {1} << (40 - CACHING_PAGEBITS);
static constexpr uint64_t CACHING_NUMPAGES = (LOWER_ADDRESS_SIZE + LibKernel::Memory::kExtendedMemorySize) >> CACHING_PAGEBITS;
static constexpr uint64_t BDA_PAGETABLE_SIZE =
CACHING_NUMPAGES * sizeof(vk::DeviceAddress);
static constexpr uint64_t PageIndex(uint64_t address) {
return (address < LOWER_ADDRESS_SIZE
? address
: address - LibKernel::Memory::kExtendedMemoryBase + LOWER_ADDRESS_SIZE) >>
CACHING_PAGEBITS;
}
static constexpr uint64_t GuestAddress(uint64_t offset) {
return offset < LOWER_ADDRESS_SIZE
? offset
: offset - LOWER_ADDRESS_SIZE + LibKernel::Memory::kExtendedMemoryBase;
}
BufferCache(GraphicContext& graphics, CommandScheduler& scheduler, PageManager& page_manager,
TextureCache& texture_cache);
~BufferCache();
@@ -88,7 +100,7 @@ private:
bool has_stream_leap;
};
using PageTable = MultiLevelPageTable<BufferId, CACHING_PAGEBITS, 40, 16>;
using PageTable = MultiLevelPageTable<BufferId, CACHING_PAGEBITS, 44, 20>;
static_assert(CACHING_PAGESIZE == (uint64_t {1} << PageTable::kPageBits));
void WriteDataBuffer(Buffer& buffer, uint64_t address, const void* source, uint64_t size);
void TouchBuffer(const Buffer& buffer);
+3 -2
View File
@@ -134,8 +134,9 @@ void FaultManager::ProcessFaultBuffer() {
const auto* faults = std::bit_cast<const uint64_t*>(mapped);
const auto count = static_cast<uint32_t>(faults[0]);
for (uint32_t index = 1; index <= count; ++index) {
fault_ranges.Add(faults[index], BufferCache::CACHING_PAGESIZE);
LOGF("Accessed non-GPU cached memory at 0x%016" PRIx64 "\n", faults[index]);
const auto address = BufferCache::GuestAddress(faults[index]);
fault_ranges.Add(address, BufferCache::CACHING_PAGESIZE);
LOGF("Accessed non-GPU cached memory at 0x%016" PRIx64 "\n", address);
}
fault_ranges.ForEach([this](uint64_t start, uint64_t end) {
EXIT_IF(end - start > std::numeric_limits<uint32_t>::max());
+12 -14
View File
@@ -14,10 +14,10 @@ SamplerCache::~SamplerCache() {
}
}
vk::Sampler SamplerCache::GetSampler(const ShaderSamplerResource& r) {
vk::Sampler SamplerCache::GetSampler(const ShaderSamplerResource& r, bool integer_border) {
Common::LockGuard lock(m_mutex);
const SamplerKey key {r.fields[0], r.fields[1], r.fields[2], r.fields[3]};
const SamplerKey key {r.fields[0], r.fields[1], r.fields[2], r.fields[3], integer_border};
if (auto iter = m_samplers.find(key); iter != m_samplers.end()) {
return iter->second;
}
@@ -55,11 +55,9 @@ vk::Sampler SamplerCache::GetSampler(const ShaderSamplerResource& r) {
case Prospero::SamplerAnisoRatio::kTwo: aniso_ratio = 2.0f; break;
case Prospero::SamplerAnisoRatio::kFour: aniso_ratio = 4.0f; break;
case Prospero::SamplerAnisoRatio::kEight: aniso_ratio = 8.0f; break;
case Prospero::SamplerAnisoRatio::kSixteen: aniso_ratio = 16.0f; break;
default:
EXIT("unknown ratio: %d dwords=%08x,%08x,%08x,%08x\n",
static_cast<int>(r.MaxAnisoRatio()), r.fields[0], r.fields[1], r.fields[2],
r.fields[3]);
case Prospero::SamplerAnisoRatio::kSixteen:
// Reserved encodings 5-7 fall back to the maximum ratio.
default: aniso_ratio = 16.0f; break;
}
}
@@ -95,23 +93,23 @@ vk::Sampler SamplerCache::GetSampler(const ShaderSamplerResource& r) {
return vk::SamplerAddressMode::eClampToBorder;
};
vk::BorderColor border = vk::BorderColor::eIntTransparentBlack;
vk::BorderColor border = integer_border ? vk::BorderColor::eIntTransparentBlack
: vk::BorderColor::eFloatTransparentBlack;
switch (static_cast<Prospero::SamplerBorderColor>(r.BorderColorType())) {
case Prospero::SamplerBorderColor::kTransBlack:
border = vk::BorderColor::eIntTransparentBlack;
break;
case Prospero::SamplerBorderColor::kTransBlack: break;
case Prospero::SamplerBorderColor::kOpaqueBlack:
border = vk::BorderColor::eIntOpaqueBlack;
border = integer_border ? vk::BorderColor::eIntOpaqueBlack
: vk::BorderColor::eFloatOpaqueBlack;
break;
case Prospero::SamplerBorderColor::kOpaqueWhite:
border = vk::BorderColor::eIntOpaqueWhite;
border = integer_border ? vk::BorderColor::eIntOpaqueWhite
: vk::BorderColor::eFloatOpaqueWhite;
break;
case Prospero::SamplerBorderColor::kFromTable:
LOGF(
"temporary: approximating table border color as transparent black, index = %" PRIu16
"\n",
r.BorderColorPtr());
border = vk::BorderColor::eIntTransparentBlack;
break;
default: EXIT("unknown border color: %d", static_cast<int>(r.BorderColorType()));
}
+2 -2
View File
@@ -24,10 +24,10 @@ public:
~SamplerCache();
KYTY_CLASS_NO_COPY(SamplerCache);
vk::Sampler GetSampler(const ShaderSamplerResource& r);
vk::Sampler GetSampler(const ShaderSamplerResource& r, bool integer_border);
private:
using SamplerKey = std::array<uint32_t, 4>;
using SamplerKey = std::array<uint32_t, 5>;
struct SamplerKeyHash {
std::size_t operator()(const SamplerKey& key) const {
+47 -72
View File
@@ -33,24 +33,27 @@ namespace {
constexpr uint64_t NumFramesBeforeRemoval = 32;
[[nodiscard]] bool DecodeDccClear(const TextureCache::ImageDesc& desc, uint8_t code,
[[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 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.
return desc.type == TextureCache::BindingType::RenderTarget &&
metadata.clear_register_valid &&
DecodePackedColorClear(format, metadata.clear_word, clear);
}
if (cmask) {
return false;
}
switch (code) {
case 0x00:
case 0x20:
case 0x40:
case 0x80:
case 0xc0: break;
default: return false;
}
const auto& metadata = desc.info.metadata;
const auto format = desc.view_info.format;
if (code == 0x20) {
// Clear-to-register is a color-buffer operation; the texture pipe cannot decode it.
return desc.type == TextureCache::BindingType::RenderTarget &&
metadata.dcc_clear_register_valid &&
DecodePackedColorClear(format, metadata.dcc_clear_word, clear);
}
clear = {};
if (code == 0x00) {
return true;
@@ -107,36 +110,6 @@ constexpr uint64_t NumFramesBeforeRemoval = 32;
return true;
}
[[nodiscard]] const char* BindingTypeName(TextureCache::BindingType type) {
switch (type) {
case TextureCache::BindingType::Texture: return "Texture";
case TextureCache::BindingType::Storage: return "StorageTexture";
case TextureCache::BindingType::RenderTarget: return "ColorTarget";
case TextureCache::BindingType::DepthTarget: return "DepthTarget";
case TextureCache::BindingType::VideoOut: return "VideoOut";
}
return "Image";
}
void NameImageBinding(GraphicContext& graphics, Image& image, vk::ImageView view,
TextureCache::BindingType type, const ImageViewInfo& view_info) {
const auto* role = BindingTypeName(type);
SetVulkanObjectNameF(
graphics.device, image.backing.image,
"Kyty.{}.Image[guest=0x{:016x} size=0x{:x} extent={}x{}x{} format={} mips={} layers={} "
"samples={}]",
role, image.info.data.address, image.info.data.size, image.info.extent.width,
image.info.extent.height, image.info.extent.depth,
static_cast<uint32_t>(image.info.pixel_format), image.info.resources.levels,
image.info.resources.layers, image.info.samples);
SetVulkanObjectNameF(
graphics.device, view,
"Kyty.{}.View[guest=0x{:016x} format={} aspect=0x{:x} mip={}+{} layer={}+{}]", role,
image.info.data.address, static_cast<uint32_t>(view_info.format),
static_cast<vk::ImageAspectFlags::MaskType>(view_info.aspect), view_info.base_level,
view_info.level_count, view_info.base_layer, view_info.layer_count);
}
[[nodiscard]] std::vector<vk::BufferImageCopy> BuildDepthCopies(const ImageInfo& info,
uint64_t slice_stride,
vk::ImageAspectFlags aspect) {
@@ -223,7 +196,7 @@ bool TextureCache::SameBacking(const ImageInfo& cached, const ImageInfo& request
return false;
}
if (!ImageViewOps::FormatsCompatible(cached.pixel_format, requested.pixel_format) ||
(cached.type != requested.type && requested.extent != vk::Extent3D {1, 1, 1})) {
cached.type != requested.type) {
return false;
}
if (exact_format && cached.pixel_format != requested.pixel_format) {
@@ -669,9 +642,12 @@ void TextureCache::CopyImage(ImageId destination_id, ImageId source_id) {
const bool source_depth = source.info.IsDepth();
const bool dest_depth = destination.info.IsDepth();
const bool direct_copy =
source.backing.format == destination.backing.format ||
(!source_depth && !dest_depth &&
vk::blockSize(source.backing.format) == vk::blockSize(destination.backing.format));
(source.backing.image_type == destination.backing.image_type ||
(source.backing.image_type != vk::ImageType::e1D &&
destination.backing.image_type != vk::ImageType::e1D)) &&
(source.backing.format == destination.backing.format ||
(!source_depth && !dest_depth &&
vk::blockSize(source.backing.format) == vk::blockSize(destination.backing.format)));
if (direct_copy) {
destination.CopyImage(source);
} else if (!CopyD16(destination, source)) {
@@ -705,7 +681,8 @@ void TextureCache::CopyImageMip(ImageId destination_id, ImageId source_id, uint3
ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingType binding,
ImageId cached_id) {
auto& cached = m_slot_images[cached_id];
if (!cached.info.IsDepth() && !requested.IsDepth()) {
if ((!cached.info.IsDepth() && !requested.IsDepth()) ||
cached.info.tile_mode != requested.tile_mode) {
return {};
}
const bool stencil_match = requested.HasStencil() == cached.info.HasStencil();
@@ -719,8 +696,8 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
requested.samples == 1 && requested.data == cached.info.data &&
requested.extent == cached.info.extent && requested.resources == cached.info.resources &&
requested.type == cached.info.type && requested.pitch == cached.info.pitch &&
requested.tile_mode == cached.info.tile_mode && !requested.HasStencil() &&
!cached.info.HasStencil() && !requested.HasMetadata() && !cached.info.HasMetadata();
!requested.HasStencil() && !cached.info.HasStencil() && !requested.HasMetadata() &&
!cached.info.HasMetadata();
// PPSA04264
const bool retain_cached_layout =
requested.samples == 1 && cached.info.samples == 1 && cached.backing.samples == 1 &&
@@ -731,7 +708,7 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
requested.resources.layers != 0 && cached.info.resources.layers != 0 &&
requested.resources.layers < cached.info.resources.layers &&
requested.type == cached.info.type && requested.pitch == cached.info.pitch &&
requested.tile_mode == cached.info.tile_mode && requested.mip_layout[0].offset == 0 &&
requested.mip_layout[0].offset == 0 &&
cached.info.mip_layout[0].offset == 0 &&
requested.mip_layout[0].size == requested.data.size &&
cached.info.mip_layout[0].size == cached.info.data.size &&
@@ -840,12 +817,15 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
return {merged_id};
}
// PPSA08394
// A view cannot change the native image type or grow its extent.
if (requested.data.size == cached.info.data.size &&
requested.resources == cached.info.resources && requested.type == cached.info.type &&
requested.extent.width > cached.info.extent.width &&
requested.extent.height >= cached.info.extent.height &&
requested.extent.depth >= cached.info.extent.depth &&
ImageViewOps::FormatsCompatible(cached.info.pixel_format, requested.pixel_format)) {
requested.resources == cached.info.resources &&
ImageViewOps::FormatsCompatible(cached.info.pixel_format, requested.pixel_format) &&
(requested.type != cached.info.type
? requested.extent == cached.info.extent
: requested.extent.width > cached.info.extent.width &&
requested.extent.height >= cached.info.extent.height &&
requested.extent.depth >= cached.info.extent.depth)) {
return {ExpandImage(requested, cached_id)};
}
// PS5 mip tails can expose more levels without increasing the guest allocation.
@@ -1148,9 +1128,10 @@ void TextureCache::InitializeImage(ImageId id) {
}
}
void TextureCache::MaterializeDccClear(ImageId id, const ImageDesc& desc,
void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
uint32_t metadata_base_layer) {
if (desc.info.metadata.kind != ImageMetadataKind::Dcc) {
if (desc.info.metadata.kind != ImageMetadataKind::Dcc &&
desc.info.metadata.kind != ImageMetadataKind::Cmask) {
return;
}
const auto range = desc.info.metadata.range;
@@ -1158,18 +1139,18 @@ void TextureCache::MaterializeDccClear(ImageId id, const ImageDesc& desc,
std::scoped_lock lock {m_lock};
auto& image = m_slot_images[id];
image.info.metadata = desc.info.metadata;
// A native DCC allocation must not retain a reused HTile/CMask/FMask interpretation.
// Native color metadata must not retain a reused HTile/CMask/FMask clear flag.
m_surface_metas.erase(range.address);
if (range.size == 0 || desc.info.resources.levels != 1 || image.info.resources.levels != 1) {
return;
}
}
const auto layers = desc.info.TransferLayers();
// These one-mip surfaces use complete 4 KiB DCC metadata blocks.
// These one-mip surfaces use complete 4 KiB color metadata blocks.
constexpr uint64_t MetadataBlockSize = 0x1000;
if (!range.Valid() || range.address % MetadataBlockSize != 0 || layers == 0 ||
range.size % layers != 0 || (range.size / layers) % MetadataBlockSize != 0) {
EXIT("TextureCache: DCC slices must contain aligned 4 KiB blocks\n");
EXIT("TextureCache: color metadata slices must contain aligned 4 KiB blocks\n");
}
const auto& view = desc.view_info;
const bool volume_texture = desc.info.IsVolume() && view.type == vk::ImageViewType::e3D;
@@ -1177,7 +1158,7 @@ void TextureCache::MaterializeDccClear(ImageId id, const ImageDesc& desc,
const auto image_first = volume_texture ? 0u : view.base_layer;
const auto count = volume_texture ? desc.info.extent.depth : view.layer_count;
if (first >= layers || count > layers - first) {
EXIT("TextureCache: DCC view exceeds its native metadata slices\n");
EXIT("TextureCache: color view exceeds its native metadata slices\n");
}
// Finish native metadata writes before reading backing bytes. This can submit the scheduler,
// so discovery runs before final draw uploads and never holds the texture lock across it.
@@ -1189,15 +1170,15 @@ void TextureCache::MaterializeDccClear(ImageId id, const ImageDesc& desc,
const auto address = range.address + slice_size * (first + slice);
uint8_t code = 0;
if (!LibKernel::Memory::TryReadBacking(address, &code, sizeof(code))) {
EXIT("TextureCache: failed to read DCC metadata backing\n");
EXIT("TextureCache: failed to read color metadata backing\n");
}
vk::ClearValue clear {};
if (!DecodeDccClear(desc, code, clear.color)) {
if (!DecodeColorClear(desc, code, clear.color)) {
continue;
}
std::vector<uint8_t> bytes(slice_size);
if (!LibKernel::Memory::TryReadBacking(address, bytes.data(), bytes.size())) {
EXIT("TextureCache: failed to read DCC metadata slice\n");
EXIT("TextureCache: failed to read color metadata slice\n");
}
if (!std::all_of(bytes.begin(), bytes.end(), [code](uint8_t byte) { return byte == code; })) {
continue;
@@ -1342,7 +1323,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
image.tick_accessed_last = m_scheduler.CurrentTick();
TouchImage(image);
}
MaterializeDccClear(result, desc, metadata_base_layer);
MaterializeColorClear(result, desc, metadata_base_layer);
if (desc.type == BindingType::VideoOut &&
desc.info.metadata.compression != VideoOutCompression::Uncompressed) {
std::scoped_lock lock {m_lock};
@@ -1443,9 +1424,7 @@ vk::ImageView TextureCache::FindTexture(ImageId id, const ImageDesc& desc) {
break;
default: EXIT("TextureCache: invalid texture binding\n");
}
const auto view = image.FindView(desc.view_info);
NameImageBinding(m_graphics, image, view, desc.type, desc.view_info);
return view;
return image.FindView(desc.view_info);
}
vk::ImageView TextureCache::FindRenderTarget(ImageId id, const ImageDesc& desc) {
@@ -1463,9 +1442,7 @@ vk::ImageView TextureCache::FindRenderTarget(ImageId id, const ImageDesc& desc)
RefreshImage(id);
CommitGpuWrite(image);
TrackImageDownload(id, image);
const auto view = image.FindView(desc.view_info);
NameImageBinding(m_graphics, image, view, desc.type, desc.view_info);
return view;
return image.FindView(desc.view_info);
}
vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
@@ -1492,9 +1469,7 @@ vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
if (desc.info.HasStencil()) {
RefreshImage(AssociateStencil(id, desc.info.stencil));
}
const auto view = image.FindView(desc.view_info);
NameImageBinding(m_graphics, image, view, desc.type, desc.view_info);
return view;
return image.FindView(desc.view_info);
}
void TextureCache::MarkGpuWritten(ImageId id) {
+3 -3
View File
@@ -91,9 +91,9 @@ private:
};
using ImageIds = InlinePageOwnerList<ImageId, 16>;
using ImagePageTable = MultiLevelPageTable<ImageIds, 20, 40, 10>;
using ImagePageTable = MultiLevelPageTable<ImageIds, 20, 44, 14>;
// Callers have validated the nonempty 40-bit range with TryGetPageRange.
// Callers have validated the nonempty 44-bit range with TryGetPageRange.
template <typename Func>
static void ForEachPage(uint64_t address, size_t size, Func&& func) {
using FuncReturn = typename std::invoke_result<Func, uint64_t>::type;
@@ -139,7 +139,7 @@ private:
ImageId cached);
[[nodiscard]] ImageId ExpandImage(const ImageInfo& info, ImageId source);
void RefreshImage(ImageId id);
void MaterializeDccClear(ImageId id, const ImageDesc& desc,
void MaterializeColorClear(ImageId id, const ImageDesc& desc,
uint32_t metadata_base_layer);
void InitializeImage(ImageId id);
[[nodiscard]] TextureTransfer
@@ -17,6 +17,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
namespace Libs::Graphics {
@@ -138,8 +139,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
// Color-control state selects the color-buffer operation and logical blend operation.
// The normal copy operation is a regular color write, not an attachment clear.
// Nonlinear clear values are still stored as normalized components.
// Fast color clears are metadata driven and must be handled explicitly when
// that metadata path is implemented; render-pass load must preserve contents.
// Metadata clears are materialized during image discovery; render-pass loads preserve contents.
uint32_t width = 0;
uint32_t height = 0;
uint32_t pitch = 0;
@@ -160,9 +160,6 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
break;
default: EXIT("unknown tile mode: %u\n", static_cast<uint32_t>(rt.attrib3.tile_mode));
}
if (!tile && levels > 1) {
EXIT("linear mipmapped render targets are unsupported\n");
}
if (samples > 1 && (!tile || levels != 1)) {
EXIT("multisampled render targets require a single-mip tiled surface\n");
}
@@ -203,8 +200,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
" layer=%u/%u\n",
rt.attrib3.dimension, rt.attrib3.depth, view.base_layer, view.image_layers);
}
// PPSA28068: the linear EULA target and its sampled view must share the padded pitch.
if (!tile || volume || texture_tile) {
if (samples == 1) {
pitch = TileGetTexturePitch(transfer_format, width, rt.attrib3.tile_mode);
} else {
pitch = TileGetRenderTargetPitch(width, bytes_per_element, rt.attrib.num_fragments);
@@ -232,37 +228,26 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
}
size = volume_layout.block_slice_size;
backing_size = volume_layout.total_size;
} else if (tile) {
} else {
TileSizeAlign layout {};
bool valid_layout = false;
if (texture_tile) {
if (samples == 1) {
TileGetTextureSize(transfer_format, width, height, levels, rt.attrib3.tile_mode,
&layout, mip_sizes, mip_padded);
valid_layout = layout.size != 0 && layout.align == texture_tile_layout.block.block_size;
valid_layout = layout.size != 0 && layout.align != 0 &&
(rt.attrib3.tile_mode != Prospero::TileMode::kRenderTarget ||
levels <= std::bit_width(std::max(width, height)));
} else {
valid_layout =
levels == 1 ? TileGetRenderTargetSize(width, height, pitch, bytes_per_element,
layout, rt.attrib.num_fragments)
: TileGetRenderTargetMipLayout(width, height, pitch, bytes_per_element,
levels, layout, mip_sizes, mip_padded);
valid_layout = TileGetRenderTargetSize(width, height, pitch, bytes_per_element,
layout, rt.attrib.num_fragments);
mip_sizes[0] = {layout.size, 0, 0, 0, 0, 0};
mip_padded[0] = {pitch, height};
}
if (!valid_layout) {
EXIT("unsupported render-target layout: %ux%u pitch=%u bytes=%u levels=%u\n", width,
height, pitch, bytes_per_element, levels);
}
size = layout.size;
EXIT_IF(size > UINT32_MAX);
if (levels == 1) {
mip_sizes[0] = {static_cast<uint32_t>(size), 0, 0, 0, 0, 0};
mip_padded[0] = {pitch, height};
}
} else {
size = static_cast<uint64_t>(pitch) * height * bytes_per_element * samples;
if (size > UINT32_MAX) {
EXIT("linear render-target slice exceeds the supported layout size\n");
}
mip_sizes[0] = {static_cast<uint32_t>(size), 0, 0, 0, 0, 0};
mip_padded[0] = {pitch, height};
}
if (size == 0 || (!volume && size > UINT64_MAX / view.image_layers)) {
EXIT("render-target memory footprint is invalid\n");
@@ -304,15 +289,25 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
desc.info.samples = samples;
desc.info.tile_mode = rt.attrib3.tile_mode;
const bool has_dcc = rt.info.dcc_compression_enable && rt.dcc_addr.addr != 0;
if (has_dcc) {
const bool has_cmask = !rt.info.dcc_compression_enable && rt.info.cmask_fast_clear_enable &&
rt.cmask.addr != 0 && samples == 1 &&
!rt.info.fmask_compression_enable &&
!rt.attrib3.write_vrs_rate_hint_to_cmask;
if (has_dcc || has_cmask) {
TileSizeAlign metadata_size {};
(void)TileGetDccSize(width, height, volume ? depth : view.image_layers, bytes_per_element,
levels, rt.attrib3.tile_mode, metadata_size, rt.attrib.num_fragments);
desc.info.metadata.kind = ImageMetadataKind::Dcc;
desc.info.metadata.range = {rt.dcc_addr.addr, metadata_size.size};
desc.info.metadata.dcc_clear_word = rt.clear_word0.word0;
desc.info.metadata.dcc_clear_register_valid = true;
desc.info.metadata.dcc_alpha_msb = DccAlphaOnMsb(rt.info);
const auto layers = volume ? depth : view.image_layers;
if (has_dcc) {
(void)TileGetDccSize(width, height, layers, bytes_per_element, levels,
rt.attrib3.tile_mode, metadata_size, rt.attrib.num_fragments);
desc.info.metadata.dcc_alpha_msb = DccAlphaOnMsb(rt.info);
} else {
(void)TileGetCmaskSize(width, height, layers, levels, metadata_size);
}
// 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_register_valid = true;
}
for (uint32_t level = 0; level < levels; level++) {
if (volume) {
-11
View File
@@ -219,17 +219,6 @@ static void RtCheck(const HW::RenderTarget& rt) {
}
}
if (rt.info.cmask_fast_clear_enable || rt.info.dcc_compression_enable) {
static bool logged = false;
if (!logged) {
LOGF("RenderTarget: temporary: ignoring PS5 color metadata fast_clear=%s dcc=%s "
"cmask=0x%016" PRIx64 " dcc_addr=0x%016" PRIx64 "\n",
rt.info.cmask_fast_clear_enable ? "true" : "false",
rt.info.dcc_compression_enable ? "true" : "false", rt.cmask.addr,
rt.dcc_addr.addr);
logged = true;
}
}
if (rt.info.blend_bypass) {
static bool logged = false;
if (!logged) {
+10 -5
View File
@@ -627,12 +627,11 @@ void Validate(const ImageInfo& info) {
}
break;
case ImageMetadataKind::Dcc:
if (info.metadata.range.address == 0 ||
info.metadata.range.address >= TRACKER_ADDRESS_SIZE ||
(info.metadata.range.size != 0 &&
info.metadata.range.size > TRACKER_ADDRESS_SIZE - info.metadata.range.address) ||
case ImageMetadataKind::Cmask:
if (!GuestRange {info.metadata.range.address,
std::max<uint64_t>(info.metadata.range.size, 1)}.Valid() ||
info.metadata.compression == VideoOutCompression::Unsupported) {
EXIT("invalid DCC metadata\n");
EXIT("invalid color metadata\n");
}
break;
}
@@ -690,6 +689,12 @@ Image::Image(GraphicContext& graphics, CommandScheduler& scheduler, const ImageI
create.extent.width, create.extent.height, create.extent.depth,
static_cast<int>(create.format), create.arrayLayers, create.mipLevels);
}
SetVulkanObjectNameF(
graphics.device, backing.image,
"Kyty.Image[guest=0x{:016x} size=0x{:x} extent={}x{}x{} format={} mips={} layers={} samples={}]",
info.data.address, info.data.size, info.extent.width, info.extent.height, info.extent.depth,
static_cast<uint32_t>(info.pixel_format), info.resources.levels, info.resources.layers,
info.samples);
}
uint64_t Image::HashGuestEdges() const {
@@ -17,17 +17,17 @@ namespace Libs::Graphics {
enum class VideoOutCompression : uint8_t { Uncompressed, Dcc256_256_0, Dcc256_64_64, Unsupported };
enum class ImageMetadataKind : uint8_t { None, Htile, Dcc };
enum class ImageMetadataKind : uint8_t { None, Htile, Dcc, Cmask };
struct ImageMetadataInfo {
GuestRange range;
ImageMetadataKind kind = ImageMetadataKind::None;
uint32_t control = 0;
uint32_t dcc_clear_word = 0;
VideoOutCompression compression = VideoOutCompression::Uncompressed;
bool stencil_compressed = false;
bool dcc_clear_register_valid = false;
bool dcc_alpha_msb = true;
ImageMetadataKind kind = ImageMetadataKind::None;
uint32_t control = 0;
uint32_t clear_word = 0;
VideoOutCompression compression = VideoOutCompression::Uncompressed;
bool stencil_compressed = false;
bool clear_register_valid = false;
bool dcc_alpha_msb = true;
};
struct ImageSubresources {
@@ -391,6 +391,12 @@ vk::ImageView Image::FindView(const ImageViewInfo& view_info) {
view_info.level_count, view_info.base_layer, view_info.layer_count,
static_cast<vk::ImageUsageFlags::MaskType>(view_info.usage));
}
SetVulkanObjectNameF(
m_graphics.device, view,
"Kyty.ImageView[guest=0x{:016x} format={} aspect=0x{:x} mip={}+{} layer={}+{}]",
info.data.address, static_cast<uint32_t>(normalized.format),
static_cast<vk::ImageAspectFlags::MaskType>(normalized.aspect), normalized.base_level,
normalized.level_count, normalized.base_layer, normalized.layer_count);
views.push_back({normalized, view});
return view;
}
@@ -196,8 +196,8 @@ bool IsSupportedDepthTextureEncoding(const ShaderTextureResource& descriptor, bo
constexpr uint32_t htile_control = 0x00280000u;
const uint32_t expected_control = htile_control | (descriptor.MsaaDepth() ? (1u << 10u) : 0u);
const auto metadata_addr = descriptor.MetaAddr() << 8u;
return metadata_control == expected_control && metadata_addr != 0 &&
metadata_addr < TRACKER_ADDRESS_SIZE && (metadata_addr & 0x7fffu) == 0 &&
return metadata_control == expected_control && GuestRange {metadata_addr, 1}.Valid() &&
(metadata_addr & 0x7fffu) == 0 &&
descriptor.TileMode() == Prospero::TileMode::kDepth;
}
@@ -335,8 +335,10 @@ void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource,
const bool raw_sint_storage = format == Prospero::BufferFormat::k32SInt && uint_resource &&
resource.written && !resource.read && !resource.atomic;
const auto numeric_class = Prospero::SampledTextureNumericClass(format);
const bool raw_float_atomic = format == Prospero::BufferFormat::k32Float && uint_resource &&
resource.atomic;
const bool format_ok =
raw_sint_storage ||
raw_sint_storage || raw_float_atomic ||
(numeric_class != Prospero::TextureNumericClass::Unsupported &&
numeric_class != Prospero::TextureNumericClass::Sint &&
uint_resource == (numeric_class == Prospero::TextureNumericClass::Uint) &&
@@ -512,19 +514,50 @@ static ImageViewInfo TextureViewInfo(const ShaderRecompiler::IR::ImageResource&
return view;
}
static bool TextureViewPreservesMipLayout(const TileSurfaceDescription& description,
uint32_t view_levels) {
static bool ResolveTextureMipView(const TileSurfaceDescription& description, bool metadata,
uint32_t view_levels, uint32_t& levels, uint32_t& base_level) {
TileSurfaceLayout physical {};
TileSurfaceLayout view {};
auto view_description = description;
view_description.levels = view_levels;
return TileGetTiledTextureLayout(description, physical) &&
TileGetTiledTextureLayout(view_description, view) &&
physical.first_tail_level == view.first_tail_level &&
physical.block_slice_size == view.block_slice_size &&
physical.total_size == view.total_size &&
std::equal(std::begin(physical.mips), std::begin(physical.mips) + description.levels,
std::begin(view.mips));
view_description.levels = levels;
if (!TileGetTiledTextureLayout(description, physical) ||
!TileGetTiledTextureLayout(view_description, view)) {
return false;
}
if (physical.first_tail_level == view.first_tail_level &&
physical.block_slice_size == view.block_slice_size &&
physical.total_size == view.total_size &&
std::equal(std::begin(physical.mips), std::begin(physical.mips) + description.levels,
std::begin(view.mips))) {
return true;
}
if (metadata || ((description.layers > 1 || description.depth > 1) &&
physical.block_slice_size != view.block_slice_size)) {
return false;
}
// T# addresses the last mip. A view can select the same stored subresources
// with different mip indices; inaccessible mips need no host representation.
for (uint32_t base = 0; base + view_levels <= description.levels; ++base) {
bool matches = true;
for (uint32_t i = 0; i < view_levels; ++i) {
const auto source = base_level + i;
const auto target = base + i;
if (physical.mips[target] != view.mips[source] ||
(target >= physical.first_tail_level) != (source >= view.first_tail_level) ||
std::max(description.width >> target, 1u) != std::max(description.width >> source, 1u) ||
std::max(description.height >> target, 1u) != std::max(description.height >> source, 1u) ||
std::max(description.depth >> target, 1u) != std::max(description.depth >> source, 1u)) {
matches = false;
break;
}
}
if (matches) {
levels = description.levels;
base_level = base;
return true;
}
}
return false;
}
TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageResource& resource,
@@ -558,7 +591,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
const auto view_levels = multisampled || single_storage_mip
? 1u
: static_cast<uint32_t>(last_level - base_level) + 1u;
const auto levels =
auto levels =
multisampled ? 1u : std::max(physical_levels, base_level + view_levels);
const auto tile = descriptor.TileMode();
const bool depth_tile = tile == Prospero::TileMode::kDepth;
@@ -600,12 +633,13 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
type == Prospero::ImageType::kColor2DArray ||
type == Prospero::ImageType::kColor2DMsaaArray;
const auto image_layers = layered ? depth : 1u;
auto view_base = static_cast<uint32_t>(base_level);
if (levels > physical_levels) {
const TileSurfaceDescription physical {
format, tile, volume ? TileSurfaceDimension::Dim3D : TileSurfaceDimension::Dim2D,
width, height, volume ? depth : 1u, physical_levels, image_layers};
// Texture mip views take precedence over the resource count, but must keep its storage layout.
if (!TextureViewPreservesMipLayout(physical, levels)) {
if (!ResolveTextureMipView(physical, !resource.r128 && descriptor.MetaCompress(),
view_levels, levels, view_base)) {
EXIT("unsupported texture mip view changes physical layout: base=%u last=%u max=%u "
"extent=%ux%ux%u tile=%u\n",
base_level, last_level, max_mip, width, height, depth,
@@ -640,7 +674,8 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
pixel_format = depth_format->depth_attachment_format;
}
}
const auto storage_view_format = storage && format == Prospero::BufferFormat::k32SInt
const auto storage_view_format = storage && (resource.atomic ||
format == Prospero::BufferFormat::k32SInt)
? vk::Format::eR32Uint
: SrgbStorageViewFormat(pixel_format);
const auto view_format = storage && storage_view_format != vk::Format::eUndefined
@@ -676,6 +711,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
}
desc.view_info = TextureViewInfo(resource, descriptor, view_format, surface_format, storage,
view_levels, desc.info.resources.layers);
desc.view_info.base_level = view_base;
desc.type = storage ? TextureCache::BindingType::Storage : TextureCache::BindingType::Texture;
auto id = texture_cache.FindImage(desc, shader_conversion);
@@ -702,14 +738,15 @@ static vk::Sampler NativeSampler(RenderContext& context,
const ShaderRecompiler::IR::CompiledShaderInfo& program,
uint32_t index,
const ShaderRecompiler::IR::DescriptorValue& value) {
auto descriptor = DecodeNativeDescriptor<ShaderSamplerResource>(value);
if (!program.info.samplers[index].depth_compare) {
auto descriptor = DecodeNativeDescriptor<ShaderSamplerResource>(value);
const auto& sampler = program.info.samplers[index];
if (!sampler.depth_compare) {
descriptor.fields[0] &= ~(0x7u << 12u);
}
if (program.info.samplers[index].force_point_filtering) {
if (sampler.force_point_filtering) {
descriptor.SetPointFiltering();
}
return context.GetSamplerCache().GetSampler(descriptor);
return context.GetSamplerCache().GetSampler(descriptor, sampler.integer_border);
}
static vk::DescriptorBufferInfo NativeUpload(RenderContext& context,
@@ -795,9 +832,14 @@ void RenderExecutor::FindBuffers(PreparedBindings& prepared) {
auto& cache = m_context.GetBufferCache();
prepared.buffer_sources.clear();
prepared.buffer_sources.reserve(program.info.buffers.size());
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[i]);
const auto& layout = program.bindings;
if (layout.memory_offset_count == 0) {
return;
}
const auto& resources = layout.descriptors.front().resources;
prepared.buffer_sources.reserve(resources.size());
for (const auto resource: resources) {
auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
const auto address = descriptor.Base48();
const auto requested_size = descriptor.GetSize();
if (address == 0 || requested_size == 0) {
@@ -815,10 +857,10 @@ void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
const auto& program = *prepared.runtime->program;
const auto& snapshot = *prepared.runtime->resources;
const auto& layout = program.bindings;
EXIT_IF(prepared.buffer_sources.size() != program.info.buffers.size());
EXIT_IF(prepared.buffer_sources.size() != layout.memory_offset_count);
prepared.buffers.clear();
prepared.buffers.reserve(program.info.buffers.size());
prepared.buffers.reserve(layout.memory_offset_count);
EXIT_IF(prepared.shader_data.size() != layout.ShaderDataDwords());
std::fill(prepared.shader_data.begin() + layout.memory_offset_dword,
prepared.shader_data.end(), 0);
@@ -827,11 +869,12 @@ void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
const auto shift = (index % 4u) * 8u;
prepared.shader_data[dword] |= offset << shift;
};
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
for (uint32_t i = 0; i < layout.memory_offset_count; i++) {
const auto resource = layout.descriptors.front().resources[i];
uint32_t buffer_offset = 0;
prepared.buffers.push_back(NativeStorageBuffer(m_context, prepared.buffer_sources[i],
program.info.buffers[i], program.stage, i,
buffer_offset));
program.info.buffers[resource],
program.stage, resource, buffer_offset));
pack_memory_offset(i, buffer_offset);
}
if (ShaderRecompiler::IR::FindBinding(
@@ -1042,8 +1085,8 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer,
} else {
switch (binding.kind) {
case BindingKind::Buffers:
for (const auto resource: binding.resources) {
const auto& view = descriptors.buffers.at(resource);
EXIT_IF(descriptors.buffers.size() != binding.resources.size());
for (const auto& view: descriptors.buffers) {
EXIT_IF(view.buffer == nullptr);
m_descriptor_buffers.push_back(view);
}
@@ -29,7 +29,6 @@
#include <span>
#include <spirv-tools/libspirv.hpp>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>
#include <xxhash.h>
@@ -116,33 +115,22 @@ void DumpShaderSpirv(const char* stage_name, uint64_t shader_hash,
}
void DumpShaderOriginal(const char* stage_name, uint64_t shader_hash,
std::span<const uint32_t> code, const std::string& decoded_dump) {
std::span<const uint32_t> code) {
if (!Config::GraphicsDebugDumpEnabled()) {
return;
}
EXIT_IF(code.empty());
static std::atomic_int id = 0;
const auto base = Config::GetShaderLogFolder() / "original" /
fmt::format("{:04d}_new_shader_{}_{:016x}", id++, stage_name, shader_hash);
Common::File::CreateDirectories(base.parent_path());
for (const auto& [suffix, data, size]: {
std::tuple {".bin", static_cast<const void*>(code.data()), code.size_bytes()},
std::tuple {".rdna2", static_cast<const void*>(decoded_dump.data()),
decoded_dump.size()},
}) {
if (size == 0) {
continue;
}
auto path = base;
path += suffix;
Common::File file(path);
if (file.IsInvalid()) {
const auto path_text = Common::PathToString(path);
LOGF_COLOR(Log::Color::BrightRed, "Can't create file: %s\n", path_text.c_str());
} else {
file.Write(data, size);
}
const auto path = Config::GetShaderLogFolder() / "original" /
fmt::format("{:04d}_new_shader_{}_{:016x}.bin", id++, stage_name, shader_hash);
Common::File::CreateDirectories(path.parent_path());
Common::File file(path);
if (file.IsInvalid()) {
const auto path_text = Common::PathToString(path);
LOGF_COLOR(Log::Color::BrightRed, "Can't create file: %s\n", path_text.c_str());
return;
}
file.Write(code.data(), code.size_bytes());
}
bool ValidateShaderSpirv(const char* label, uint64_t shader_hash,
@@ -225,25 +213,13 @@ struct PipelineCache::ProgramCache {
static constexpr std::size_t MaxStaticKeyWords = 13 + ShaderVertexInputInfo::RES_MAX * 13;
Permutation CompilePermutation(const ShaderParams& params,
Permutation CompilePermutation(const char* stage_name,
const ShaderRecompiler::CompileOptions& options,
ShaderRecompiler::TranslateResult translated,
ShaderRecompiler::IR::ResourceSpecialization specialization,
uint32_t push_data_start_dword) {
const char* stage_name = nullptr;
switch (options.stage) {
case ShaderType::Vertex: stage_name = "vs"; break;
case ShaderType::Mesh: stage_name = "ms"; break;
case ShaderType::Local: stage_name = "ls"; break;
case ShaderType::TessellationControl: stage_name = "hs"; break;
case ShaderType::TessellationEvaluation: stage_name = "ds"; break;
case ShaderType::Pixel: stage_name = "ps"; break;
case ShaderType::Compute: stage_name = "cs"; break;
default: EXIT("invalid pipeline shader stage\n");
}
auto result = ShaderRecompiler::CompileProgram(std::move(translated), options,
specialization, push_data_start_dword);
DumpShaderOriginal(stage_name, options.shader_hash, params.code, result.decoded_dump);
if (!ValidateShaderSpirv(options.dump_label, options.shader_hash, result.spirv)) {
DumpShaderSpirv(stage_name, options.shader_hash, result.spirv);
EXIT("%s failed hash=0x%016" PRIx64 ": SPIR-V validation failed\n", options.dump_label,
@@ -321,14 +297,15 @@ struct PipelineCache::ProgramCache {
stage_input.compute = &input_info;
}
const char* label = nullptr;
const char* stage_name = nullptr;
switch (stage) {
case ShaderType::Vertex: label = "ShaderRecompiler VS"; break;
case ShaderType::Mesh: label = "ShaderRecompiler MS"; break;
case ShaderType::Local: label = "ShaderRecompiler LS"; break;
case ShaderType::TessellationControl: label = "ShaderRecompiler HS"; break;
case ShaderType::TessellationEvaluation: label = "ShaderRecompiler DS"; break;
case ShaderType::Pixel: label = "ShaderRecompiler PS"; break;
case ShaderType::Compute: label = "ShaderRecompiler CS"; break;
case ShaderType::Vertex: label = "ShaderRecompiler VS"; stage_name = "vs"; break;
case ShaderType::Mesh: label = "ShaderRecompiler MS"; stage_name = "ms"; break;
case ShaderType::Local: label = "ShaderRecompiler LS"; stage_name = "ls"; break;
case ShaderType::TessellationControl: label = "ShaderRecompiler HS"; stage_name = "hs"; break;
case ShaderType::TessellationEvaluation: label = "ShaderRecompiler DS"; stage_name = "ds"; break;
case ShaderType::Pixel: label = "ShaderRecompiler PS"; stage_name = "ps"; break;
case ShaderType::Compute: label = "ShaderRecompiler CS"; stage_name = "cs"; break;
default: EXIT("invalid pipeline shader stage\n");
}
ShaderRecompiler::CompileOptions options;
@@ -351,6 +328,7 @@ struct PipelineCache::ProgramCache {
} else {
options.wave_size = input_info.wave_size;
}
DumpShaderOriginal(stage_name, options.shader_hash, params.code);
auto translated = ShaderRecompiler::TranslateProgram(params.code, options);
if (translated.skip_dispatch) {
entry = programs.try_emplace(lookup_key, ShaderRecompiler::IR::ResourcePlan {}).first;
@@ -365,7 +343,7 @@ struct PipelineCache::ProgramCache {
entry->second.specialization));
}
entry->second.permutations.push_back(CompilePermutation(
params, options, std::move(translated), entry->second.specialization, push_data_cursor));
stage_name, options, std::move(translated), entry->second.specialization, push_data_cursor));
const auto& permutation = entry->second.permutations.back();
input_info.stage = {.program = &permutation.program, .resources = &entry->second.resources};
permutation.program.bindings.AdvancePushData(push_data_cursor);
+20 -27
View File
@@ -473,15 +473,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
}
const auto image_view = cache.FindRenderTarget(target.image_id, target.desc);
auto& image = cache.GetImage(target.image_id);
SetVulkanObjectNameF(m_context.GetGraphics().device, image.backing.image,
"Kyty.MRT{}.Image[guest=0x{:016x} size=0x{:x} format={}]",
target.target_slot, image.info.data.address, image.info.data.size,
static_cast<uint32_t>(image.info.pixel_format));
SetVulkanObjectNameF(m_context.GetGraphics().device, image_view,
"Kyty.MRT{}.View[guest=0x{:016x} mip={} layer={}+{}]",
target.target_slot, image.info.data.address,
target.desc.view_info.base_level, target.desc.view_info.base_layer,
target.desc.view_info.layer_count);
EXIT_IF(image.backing.samples != target.desc.info.samples || image_view == nullptr);
const auto& view = target.desc.view_info;
const auto layout = image.binding.is_bound ? vk::ImageLayout::eGeneral
@@ -522,14 +513,6 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
EXIT("failed to consume HTile clear state\n");
}
auto& image = cache.GetImage(depth.image_id);
SetVulkanObjectNameF(m_context.GetGraphics().device, image.backing.image,
"Kyty.DepthTarget.Image[guest=0x{:016x} size=0x{:x} format={}]",
image.info.data.address, image.info.data.size,
static_cast<uint32_t>(image.info.pixel_format));
SetVulkanObjectNameF(m_context.GetGraphics().device, image_view,
"Kyty.DepthTarget.View[guest=0x{:016x} layer={}+{}]",
image.info.data.address, depth.desc.view_info.base_layer,
depth.desc.view_info.layer_count);
EXIT_IF(image_view == nullptr || image.backing.samples != depth.desc.info.samples);
const auto draw_writes = depth.AttachmentWriteAspects();
vk::ImageAspectFlags sampled_aspects;
@@ -592,12 +575,17 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
return state;
}
static bool DrawHasActivePixelShader(const CommandBuffer& buffer) {
const auto& ctx = buffer.GetRegisters();
const auto& sh_regs = ctx.GetShaderRegisters();
const bool has_color_output = (ctx.GetRenderTargetMask() & sh_regs.m_cbShaderMask) != 0;
return buffer.GetShaders().GetPs().ps_regs.data_addr != 0 &&
(has_color_output || PixelShaderHasDepthOrCoverageSideEffects(sh_regs));
static uint32_t DrawColorOutputMask(const HW::Context& ctx) {
const auto& sh_regs = ctx.GetShaderRegisters();
const auto write_mask = ctx.GetRenderTargetMask() & sh_regs.m_cbShaderMask;
uint32_t output_mask = 0;
for (uint32_t slot = 0; slot < RENDER_COLOR_ATTACHMENTS_MAX; slot++) {
if (sh_regs.target_output_mode[slot] != 0 &&
render_target_mask_slot(write_mask, slot) != 0) {
output_mask |= 1u << slot;
}
}
return output_mask;
}
enum class CbColorMode : uint8_t {
@@ -864,7 +852,7 @@ static bool ResolvePrimitiveRestart(const CommandBuffer& buffer,
}
static void RefreshShaders(CommandBuffer& buffer, const DrawCallInfo& draw,
DrawRenderState& state) {
uint32_t color_output_mask, DrawRenderState& state) {
auto& ctx = buffer.GetRegisters();
auto& sh_ctx = buffer.GetShaders();
@@ -878,7 +866,7 @@ static void RefreshShaders(CommandBuffer& buffer, const DrawCallInfo& draw,
target_export_mapping {};
for (uint32_t slot = 0; slot < RENDER_COLOR_ATTACHMENTS_MAX; slot++) {
const auto& rt = ctx.GetRenderTarget(slot);
if (rt.base.addr != 0 && render_target_mask_slot(ctx.GetRenderTargetMask(), slot) != 0) {
if ((color_output_mask & (1u << slot)) != 0 && rt.base.addr != 0) {
target_export_mapping[slot] =
TextureGetRenderTargetFormat(rt.info.format, rt.info.channel_type,
rt.info.channel_order)
@@ -897,8 +885,12 @@ static void RefreshShaders(CommandBuffer& buffer, const DrawCallInfo& draw,
bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCallInfo& draw,
uint32_t render_target_slice_offset,
DrawRenderState& state) {
state.ps_active = DrawHasActivePixelShader(buffer);
RefreshShaders(buffer, draw, state);
const auto& shader_regs = buffer.GetRegisters().GetShaderRegisters();
const auto color_output_mask = DrawColorOutputMask(buffer.GetRegisters());
state.ps_active = buffer.GetShaders().GetPs().ps_regs.data_addr != 0 &&
(color_output_mask != 0 ||
PixelShaderHasDepthOrCoverageSideEffects(shader_regs));
RefreshShaders(buffer, draw, color_output_mask, state);
uint32_t mrt_mask = 0;
if (state.ps_active) {
for (const auto& output: state.ps_input_info.stage.program->info.outputs) {
@@ -907,6 +899,7 @@ bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCal
}
}
}
mrt_mask &= color_output_mask;
if (draw.IsIndexed()) {
LogDrawPhase(draw.Name(), "ResolveRenderColorTarget");
}
+1 -21
View File
@@ -112,26 +112,6 @@ void WriteAtEndOfPipe64(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::Write);
}
void WriteAtEndOfPipeClockCounter(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), 0,
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::Write);
LOGF_COLOR(Log::Color::BrightGreen,
"EndOfPipe Signal!!! [0x%016" PRIx64 "] <- Clock: 0x%016" PRIx64 "\n",
reinterpret_cast<uint64_t>(dst_gpu_addr), value);
}
void WriteAtEndOfPipeClockCounterWithWriteBack(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), 0,
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::WriteBack);
LOGF_COLOR(Log::Color::BrightGreen,
"EndOfPipe Signal!!! [0x%016" PRIx64 "] <- Clock: 0x%016" PRIx64 "\n",
reinterpret_cast<uint64_t>(dst_gpu_addr), value);
}
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,
@@ -192,7 +172,7 @@ uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index, int f
"\n",
result, handle, index, flip_mode, flip_arg);
}
video_out.WaitForSubmitSlot();
video_out.WaitForSubmitSlot(handle);
}
}
-4
View File
@@ -25,10 +25,6 @@ void WriteAtEndOfPipe64(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst
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 WriteAtEndOfPipeClockCounter(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst_gpu_addr,
uint64_t value);
void WriteAtEndOfPipeClockCounterWithWriteBack(uint64_t submit_id, CommandBuffer& buffer,
uint64_t* dst_gpu_addr, uint64_t value);
void WriteAtEndOfPipeWithWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value);
void WriteAtEndOfPipeWithWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
@@ -0,0 +1,15 @@
#version 450
layout(location = 0) in vec2 uv;
layout(location = 0) out vec4 color;
layout(binding = 0) uniform sampler2D source;
layout(push_constant) uniform AlphaControl {
uint premultiplied;
};
void main() {
color = texture(source, uv);
if (premultiplied == 0u) {
color.rgb *= color.a;
}
}
+10 -2
View File
@@ -4,6 +4,7 @@
#include "common/common.h"
#include <memory>
#include <span>
namespace Libs::Graphics {
@@ -15,6 +16,11 @@ struct WindowContext;
class Presenter final {
public:
struct Frame;
struct Layer {
Frame* frame;
int bus;
bool premultiplied_alpha;
};
explicit Presenter(WindowContext& window);
~Presenter();
@@ -23,11 +29,13 @@ public:
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info);
[[nodiscard]] Frame& PrepareBlankFrame(uint32_t width, uint32_t height, bool opaque,
CommandBuffer* producer = nullptr);
[[nodiscard]] Frame* PrepareLastFrame();
[[nodiscard]] bool PresentLastFrame();
[[nodiscard]] bool IsGuestPaused() const noexcept;
[[nodiscard]] bool NeedsSystemOverlayRefresh() const noexcept;
[[nodiscard]] RenderContext& Renderer() const noexcept;
void Present(Frame& frame, bool reuse = false);
void Present(Frame& frame);
void Present(std::span<const Layer> layers);
void ClearLayer(int bus);
void Discard(Frame& frame);
private:
+195 -73
View File
@@ -51,6 +51,8 @@ constexpr int VIDEO_OUT_BUS_TYPE_OVERLAY = 1;
constexpr int VIDEO_OUT_BUS_TYPE_SUB = 2;
constexpr int VIDEO_OUT_FLIP_MODE_VSYNC = 1;
constexpr int VIDEO_OUT_FLIP_MODE_VSYNC_MULTI = 4;
constexpr int VIDEO_OUT_FLIP_MODE_SLAVE = 8;
constexpr int VIDEO_OUT_FLIP_MODE_MASTER = 9;
constexpr int VIDEO_OUT_BUFFER_INDEX_BLACK = -2;
constexpr int VIDEO_OUT_BUFFER_INDEX_BLANK = -1;
constexpr int VIDEO_OUT_BUFFER_NUM_MAX = 16;
@@ -63,6 +65,7 @@ constexpr uint64_t VIDEO_OUT_REFRESH_RATE_119_88HZ = 13;
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED = 0;
constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED = 1;
constexpr uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY = 8;
constexpr uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED = 32;
enum class VideoOutEventKind : uintptr_t {
Flip = VIDEO_OUT_EVENT_FLIP,
@@ -183,6 +186,9 @@ struct VideoOutConfig {
uint64_t generation = 0;
bool opened = false;
bool closing = false;
int bus = 0;
VideoOutConfig* master = nullptr;
uint32_t slave_count = 0;
int flip_rate = 0;
uint64_t output_mode = VIDEO_OUT_OUTPUT_MODE_DEFAULT;
float gamma = 1.0f;
@@ -206,7 +212,7 @@ public:
void Cancel(VideoOutConfig& cfg);
void Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer);
void Complete(uint64_t request_id);
void WaitForSubmitSlot();
void WaitForSubmitSlot(VideoOutConfig& cfg);
bool Flip(uint32_t micros);
void GetFlipStatus(VideoOutConfig& cfg, VideoOutFlipStatus& out);
void Wait(VideoOutConfig& cfg, int index);
@@ -216,7 +222,9 @@ private:
struct Request {
uint64_t id;
uint64_t group;
VideoOutConfig* cfg;
VideoOutConfig* master;
uint64_t generation;
int index;
int64_t flip_arg;
@@ -224,6 +232,7 @@ private:
FlipRequestSource source;
RequestState state;
Graphics::Presenter::Frame* frame;
bool premultiplied_alpha;
};
Graphics::Presenter& m_presenter;
@@ -258,6 +267,7 @@ public:
VideoOutConfig* Get(int handle);
VideoOutConfig* Get(int handle, uint64_t& generation);
bool IsOpened(int handle);
int SetFlipMaster(int slave_handle, int master_handle);
void Init(uint32_t width, uint32_t height);
FlipQueue& GetFlipQueue() { return m_flip_queue; }
@@ -498,7 +508,8 @@ static bool IsSpecialBufferIndex(int index) {
}
static bool IsValidFlipMode(int mode) {
return mode >= VIDEO_OUT_FLIP_MODE_VSYNC && mode <= VIDEO_OUT_FLIP_MODE_VSYNC_MULTI;
return (mode >= VIDEO_OUT_FLIP_MODE_VSYNC && mode <= VIDEO_OUT_FLIP_MODE_VSYNC_MULTI) ||
mode == VIDEO_OUT_FLIP_MODE_SLAVE || mode == VIDEO_OUT_FLIP_MODE_MASTER;
}
static int ReserveFlipRequest(VideoOutDriver::Impl& driver, int handle, int index, int flip_mode,
@@ -508,13 +519,17 @@ static int ReserveFlipRequest(VideoOutDriver::Impl& driver, int handle, int inde
return VIDEO_OUT_ERROR_INVALID_HANDLE;
}
if (!IsValidFlipMode(flip_mode)) {
return VIDEO_OUT_ERROR_INVALID_VALUE;
return VIDEO_OUT_ERROR_INVALID_FLIP_MODE;
}
if (!IsValidBufferIndex(index)) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
}
Common::LockGuard lock(video_out->mutex);
if ((video_out->master != nullptr) != (flip_mode == VIDEO_OUT_FLIP_MODE_SLAVE) ||
(video_out->slave_count != 0) != (flip_mode == VIDEO_OUT_FLIP_MODE_MASTER)) {
return VIDEO_OUT_ERROR_INVALID_FLIP_MODE;
}
if (video_out->closing ||
(!IsSpecialBufferIndex(index) && !video_out->buffers[index].Occupied())) {
return VIDEO_OUT_ERROR_INVALID_INDEX;
@@ -532,8 +547,8 @@ Graphics::ImageInfo BufferAttributeGroup::ImageInfo(const VideoOutBuffer& buffer
if (attribute.reserved0 != 0 || attribute.aspect_ratio != 0 || attribute.width == 0 ||
attribute.height == 0 || attribute.width > 16384 || attribute.height > 16384 ||
attribute.pitch_in_pixel != 0 ||
(attribute.option != 0 &&
attribute.option != VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY) ||
(attribute.option & ~(VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY |
VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED)) != 0 ||
attribute.tiling_mode != 0 || attribute.pad0 != 0 || attribute.reserved1[0] != 0 ||
attribute.reserved1[1] != 0 || attribute.reserved1[2] != 0 || buffer.data_address == 0 ||
compression == Graphics::VideoOutCompression::Unsupported) {
@@ -616,6 +631,11 @@ VideoOutDriver::Impl::~Impl() {
m_present_thread.request_stop();
m_present_thread.join();
}
for (int handle = 1; handle < VIDEO_OUT_NUM_MAX; handle++) {
if (m_video_out_ctx[handle].master != nullptr) {
(void)Close(handle);
}
}
for (int handle = 1; handle < VIDEO_OUT_NUM_MAX; handle++) {
(void)Close(handle);
}
@@ -660,6 +680,7 @@ int VideoOutDriver::Impl::Open(int bus_type, int index) {
config.closing = false;
config.opened = true;
config.bus = bus_type;
if (++config.generation == 0) {
EXIT("video-out port generation wrapped\n");
}
@@ -686,11 +707,13 @@ bool VideoOutDriver::Impl::Close(int handle) {
VideoOutEventQueues pre_vblank_events;
VideoOutEventQueues vblank_events;
VideoOutEventQueues output_mode_events;
VideoOutConfig* master = nullptr;
{
Common::LockGuard config_lock(config.mutex);
if (config.closing) {
return false;
}
EXIT_IF(config.slave_count != 0);
config.opened = false;
config.closing = true;
if (++config.generation == 0) {
@@ -703,7 +726,9 @@ bool VideoOutDriver::Impl::Close(int handle) {
vblank_events = std::move(config.events->vblank);
output_mode_events = std::move(config.events->output_mode);
}
config.flip_rate = 0;
config.flip_rate = 0;
master = config.master;
config.master = nullptr;
for (const auto& buffer: config.buffers) {
if (buffer.Occupied() &&
@@ -720,8 +745,12 @@ bool VideoOutDriver::Impl::Close(int handle) {
}
config.vblank_cond.SignalAll();
}
if (master != nullptr) {
Common::LockGuard master_lock(master->mutex);
master->slave_count--;
}
m_flip_queue.Cancel(config);
m_presenter.ClearLayer(config.bus);
DeleteVideoOutEvents(flip_events, VideoOutEventKind::Flip);
DeleteVideoOutEvents(pre_vblank_events, VideoOutEventKind::PreVblankStart);
DeleteVideoOutEvents(vblank_events, VideoOutEventKind::Vblank);
@@ -759,6 +788,31 @@ bool VideoOutDriver::Impl::IsOpened(int handle) {
return handle > 0 && handle < VIDEO_OUT_NUM_MAX && m_video_out_ctx[handle].opened;
}
int VideoOutDriver::Impl::SetFlipMaster(int slave_handle, int master_handle) {
Common::LockGuard lock(m_mutex);
if (slave_handle <= 0 || slave_handle >= VIDEO_OUT_NUM_MAX ||
master_handle <= 0 || master_handle >= VIDEO_OUT_NUM_MAX ||
!m_video_out_ctx[slave_handle].opened || !m_video_out_ctx[master_handle].opened) {
return VIDEO_OUT_ERROR_INVALID_HANDLE;
}
auto& slave = m_video_out_ctx[slave_handle];
auto& master = m_video_out_ctx[master_handle];
if (&slave == &master) {
return VIDEO_OUT_ERROR_UNSUPPORTED_OPERATION;
}
Common::LockGuard first_lock(m_video_out_ctx[std::min(slave_handle, master_handle)].mutex);
Common::LockGuard second_lock(m_video_out_ctx[std::max(slave_handle, master_handle)].mutex);
if (slave.master != nullptr || slave.slave_count != 0 || master.master != nullptr) {
return VIDEO_OUT_ERROR_UNSUPPORTED_OPERATION;
}
if (slave.flip_status.flipPendingNum != 0 || master.flip_status.flipPendingNum != 0) {
return VIDEO_OUT_ERROR_RESOURCE_BUSY;
}
slave.master = &master;
master.slave_count++;
return OK;
}
void VideoOutDriver::Impl::VblankBegin() {
Common::LockGuard lock(m_mutex);
@@ -821,9 +875,7 @@ void VideoOutDriver::Impl::PresentThread(std::stop_token token) {
const auto period = std::max(frequency / refresh, uint64_t {1});
if (m_presenter.IsGuestPaused()) {
if (auto* frame = m_presenter.PrepareLastFrame(); frame != nullptr) {
m_presenter.Present(*frame, true);
}
(void)m_presenter.PresentLastFrame();
const auto frame_end = Common::Timer::QueryPerformanceCounter();
total_wait +=
static_cast<int64_t>(period) - static_cast<int64_t>(frame_end - frame_begin);
@@ -833,8 +885,7 @@ void VideoOutDriver::Impl::PresentThread(std::stop_token token) {
VblankBegin();
bool presented = m_flip_queue.Flip(0);
if (!presented && m_presenter.NeedsSystemOverlayRefresh()) {
if (auto* frame = m_presenter.PrepareLastFrame(); frame != nullptr) {
m_presenter.Present(*frame, true);
if (m_presenter.PresentLastFrame()) {
presented = true;
} else {
uint32_t width = 0;
@@ -877,25 +928,57 @@ bool FlipQueue::Reserve(VideoOutConfig& cfg, int index, int64_t flip_arg, FlipRe
uint64_t& request_id) {
Common::LockGuard lock(m_mutex);
if (m_requests.size() + m_cpu_requests.size() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) {
// A newer slave request replaces the one not yet captured by a master flip.
if (cfg.master != nullptr) {
for (auto* queue: {&m_requests, &m_cpu_requests}) {
for (auto it = queue->begin(); it != queue->end();) {
if (it->cfg != &cfg || it->group != 0) {
++it;
continue;
}
cfg.flip_status.flipPendingNum--;
cfg.flip_status.gcQueueNum -= it->source == FlipRequestSource::GpuEop;
if (it->state == RequestState::Ready) {
m_presenter.Discard(*it->frame);
it = queue->erase(it);
} else {
auto cancelled = it++;
m_cancelled_requests.splice(m_cancelled_requests.end(), *queue, cancelled);
}
}
}
m_done_cond_var.SignalAll();
}
if (cfg.flip_status.flipPendingNum >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) {
return false;
}
auto& pending = source == FlipRequestSource::GpuEop ? m_requests : m_cpu_requests;
Request r {};
r.id = m_next_request_id++;
r.group = cfg.master == nullptr ? r.id : 0;
r.cfg = &cfg;
r.master = cfg.master;
r.generation = cfg.generation;
r.index = index;
r.flip_arg = flip_arg;
r.submit_ptc = LibKernel::KernelGetProcessTimeCounter();
r.source = source;
r.state = RequestState::Reserved;
if (cfg.slave_count != 0) {
for (auto* queue: {&m_requests, &m_cpu_requests}) {
for (auto& slave: *queue) {
if (slave.master == &cfg && slave.group == 0) {
slave.group = r.id;
}
}
}
}
pending.push_back(r);
request_id = r.id;
cfg.flip_status.flipPendingNum = static_cast<int>(m_requests.size() + m_cpu_requests.size());
cfg.flip_status.flipPendingNum++;
cfg.flip_status.submitProcessTimeCounter = r.submit_ptc;
if (source == FlipRequestSource::GpuEop) {
cfg.flip_status.gcQueueNum++;
@@ -917,7 +1000,9 @@ FlipQueue::~FlipQueue() {
void FlipQueue::Cancel(VideoOutConfig& cfg) {
std::vector<Graphics::Presenter::Frame*> frames;
m_mutex.Lock();
while (m_processing && !m_requests.empty() && m_requests.front().cfg == &cfg) {
while (m_processing && std::any_of(m_requests.begin(), m_requests.end(), [&cfg](const auto& r) {
return r.cfg == &cfg && r.state == RequestState::Presenting;
})) {
m_done_cond_var.Wait(&m_mutex);
}
for (auto* queue: {&m_requests, &m_cpu_requests}) {
@@ -991,6 +1076,7 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
uint32_t width = 0;
uint32_t height = 0;
bool current = false;
bool premultiplied_alpha = false;
{
Common::LockGuard lock(cfg->mutex);
current = cfg->opened && !cfg->closing && cfg->generation == generation;
@@ -1012,6 +1098,9 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
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;
}
}
}
@@ -1051,6 +1140,7 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
EXIT("video-out request changed while recording, id=%" PRIu64 "\n", request_id);
}
prepared->frame = frame;
prepared->premultiplied_alpha = premultiplied_alpha;
}
void FlipQueue::Complete(uint64_t request_id) {
@@ -1082,9 +1172,12 @@ void FlipQueue::Complete(uint64_t request_id) {
}
}
void FlipQueue::WaitForSubmitSlot() {
void FlipQueue::WaitForSubmitSlot(VideoOutConfig& cfg) {
Common::LockGuard lock(m_mutex);
while (m_requests.size() + m_cpu_requests.size() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) {
auto belongs_to_port = [&cfg](const auto& request) { return request.cfg == &cfg; };
while (std::count_if(m_requests.begin(), m_requests.end(), belongs_to_port) +
std::count_if(m_cpu_requests.begin(), m_cpu_requests.end(), belongs_to_port) >=
VIDEO_OUT_FLIP_QUEUE_CAPACITY) {
if (m_requests.empty()) {
EXIT("video-out queue is saturated by CPU flips queued behind the current EOP\n");
}
@@ -1111,75 +1204,90 @@ bool FlipQueue::Flip(uint32_t micros) {
m_mutex.Lock();
if (m_requests.empty()) {
m_submit_cond_var.WaitFor(&m_mutex, micros);
if (m_requests.empty()) {
m_mutex.Unlock();
return false;
}
}
if (m_processing) {
EXIT("video-out flip queue processing is already active\n");
}
if (m_requests.front().state != RequestState::Ready) {
EXIT_IF(m_processing);
const auto owner = std::find_if(m_requests.begin(), m_requests.end(),
[](const auto& r) { return r.group == r.id; });
if (owner == m_requests.end()) {
m_mutex.Unlock();
return false;
}
m_processing = true;
auto r = m_requests.front();
m_mutex.Unlock();
r.cfg->mutex.Lock();
if (!IsFlipDueLocked(*r.cfg, r.generation)) {
r.cfg->mutex.Unlock();
Common::LockGuard queue_lock(m_mutex);
m_processing = false;
m_done_cond_var.SignalAll();
const uint64_t group = owner->id;
if (std::any_of(m_cpu_requests.begin(), m_cpu_requests.end(),
[group](const auto& r) { return r.group == group; }) ||
std::any_of(m_requests.begin(), m_requests.end(), [group](const auto& r) {
return r.group == group && r.state != RequestState::Ready;
})) {
m_mutex.Unlock();
return false;
}
m_mutex.Lock();
if (m_requests.empty() || m_requests.front().id != r.id ||
m_requests.front().state != RequestState::Ready || !m_processing) {
EXIT("video-out request changed before presentation, id=%" PRIu64 "\n", r.id);
std::array<Request, VideoOutDriver::Impl::VIDEO_OUT_BUSES.size()> requests {};
size_t count = 0;
for (auto& request: m_requests) {
if (request.group == group) {
EXIT_IF(count == requests.size());
request.state = RequestState::Presenting;
requests[count++] = request;
}
}
m_requests.front().state = RequestState::Presenting;
m_processing = true;
m_mutex.Unlock();
m_presenter.Present(*r.frame);
// Lock each port in bus order, then present and complete the whole group at one Vblank.
std::sort(requests.begin(), requests.begin() + count,
[](const auto& a, const auto& b) { return a.cfg->bus < b.cfg->bus; });
bool due = true;
std::array<Graphics::Presenter::Layer, VideoOutDriver::Impl::VIDEO_OUT_BUSES.size()> layers {};
for (size_t i = 0; i < count; i++) {
auto& r = requests[i];
r.cfg->mutex.Lock();
due &= r.cfg->opened && !r.cfg->closing && r.cfg->generation == r.generation;
if (r.id == group) {
due &= IsFlipDueLocked(*r.cfg, r.generation);
}
layers[i] = {r.frame, r.cfg->bus, r.premultiplied_alpha};
}
if (due) {
m_presenter.Present(std::span(layers.data(), count));
}
m_mutex.Lock();
if (m_requests.empty() || m_requests.front().id != r.id ||
m_requests.front().state != RequestState::Presenting) {
EXIT("video-out flip queue changed while processing its front request\n");
for (auto it = m_requests.begin(); it != m_requests.end();) {
if (it->group != group) {
++it;
continue;
}
if (!due) {
it++->state = RequestState::Ready;
continue;
}
auto& r = *it;
r.cfg->flip_status.count++;
r.cfg->flip_status.processTime = LibKernel::KernelGetProcessTime();
r.cfg->flip_status.processTimeCounter = LibKernel::KernelGetProcessTimeCounter();
r.cfg->flip_status.submitProcessTimeCounter = r.submit_ptc;
r.cfg->flip_status.flipArg = r.flip_arg;
r.cfg->flip_status.currentBuffer = r.index;
r.cfg->flip_status.flipPendingNum--;
r.cfg->flip_status.gcQueueNum -= r.source == FlipRequestSource::GpuEop;
TriggerVideoOutEvents(*r.cfg, VideoOutEventKind::Flip, reinterpret_cast<void*>(r.flip_arg));
it = m_requests.erase(it);
}
m_requests.pop_front();
r.cfg->flip_status.count++;
r.cfg->flip_status.processTime = LibKernel::KernelGetProcessTime();
r.cfg->flip_status.processTimeCounter = LibKernel::KernelGetProcessTimeCounter();
r.cfg->flip_status.submitProcessTimeCounter = r.submit_ptc;
r.cfg->flip_status.flipArg = r.flip_arg;
r.cfg->flip_status.currentBuffer = r.index;
r.cfg->flip_status.flipPendingNum = static_cast<int>(m_requests.size() + m_cpu_requests.size());
if (r.source == FlipRequestSource::GpuEop && r.cfg->flip_status.gcQueueNum > 0) {
r.cfg->flip_status.gcQueueNum--;
}
TriggerVideoOutEvents(*r.cfg, VideoOutEventKind::Flip, reinterpret_cast<void*>(r.flip_arg));
m_processing = false;
m_done_cond_var.SignalAll();
m_submit_slot_cond_var.Signal();
m_submit_slot_cond_var.SignalAll();
m_mutex.Unlock();
r.cfg->mutex.Unlock();
Graphics::RenderDocOnGuestFlip(m_presenter.Renderer());
if (Config::GraphicsDebugDumpEnabled() &&
Config::GetPrintfDirection() != Config::LogDirection::Silent) {
LOGF("Flip done: %d\n", r.index);
for (size_t i = count; i != 0; i--) {
requests[i - 1].cfg->mutex.Unlock();
}
return true;
if (due) {
Graphics::RenderDocOnGuestFlip(m_presenter.Renderer());
if (Config::GraphicsDebugDumpEnabled() &&
Config::GetPrintfDirection() != Config::LogDirection::Silent) {
LOGF("Flip done: %d\n", requests[0].index);
}
}
return due;
}
void FlipQueue::GetFlipStatus(VideoOutConfig& cfg, VideoOutFlipStatus& out) {
@@ -1188,6 +1296,18 @@ void FlipQueue::GetFlipStatus(VideoOutConfig& cfg, VideoOutFlipStatus& out) {
out = cfg.flip_status;
}
KYTY_SYSV_ABI int VideoOutSetFlipMaster(int slave_handle, int master_handle) {
PRINT_NAME();
return DriverState().SetFlipMaster(slave_handle, master_handle);
}
KYTY_SYSV_ABI void VideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* attribute,
uint64_t option) {
PRINT_NAME();
EXIT_IF(attribute == nullptr);
attribute->option |= option;
}
KYTY_SYSV_ABI int VideoOutOpen(int user_id, int bus_type, int index, const void* param) {
PRINT_NAME();
@@ -1467,7 +1587,7 @@ KYTY_SYSV_ABI int VideoOutSubmitFlip(int handle, int index, int flip_mode, int64
uint64_t request_id = 0;
const int result = ReserveFlipRequest(DriverState(), handle, index, flip_mode, flip_arg,
FlipRequestSource::Cpu, request_id);
if (result == VIDEO_OUT_ERROR_INVALID_VALUE) {
if (result == VIDEO_OUT_ERROR_INVALID_FLIP_MODE) {
LOGF("\t unsupported flip_mode = %d\n", flip_mode);
}
if (result != OK) {
@@ -1504,8 +1624,10 @@ void VideoOutDriver::CompleteFlip(uint64_t request_id) {
m_impl->GetFlipQueue().Complete(request_id);
}
void VideoOutDriver::WaitForSubmitSlot() {
m_impl->GetFlipQueue().WaitForSubmitSlot();
void VideoOutDriver::WaitForSubmitSlot(int handle) {
auto* cfg = m_impl->Get(handle);
EXIT_IF(cfg == nullptr);
m_impl->GetFlipQueue().WaitForSubmitSlot(*cfg);
}
void VideoOutDriver::WaitFlipDone(int handle, int index) {
+4 -1
View File
@@ -37,7 +37,7 @@ public:
void PrepareFlip(uint64_t request_id, Graphics::CommandBuffer& buffer);
void CompleteFlip(uint64_t request_id);
void SubmitFlipPreparation(uint64_t request_id);
void WaitForSubmitSlot();
void WaitForSubmitSlot(int handle);
void WaitFlipDone(int handle, int index);
[[nodiscard]] Impl& State() noexcept;
@@ -52,6 +52,9 @@ void VideoOutShutdown();
KYTY_SYSV_ABI int VideoOutOpen(int user_id, int bus_type, int index, const void* param);
KYTY_SYSV_ABI int VideoOutClose(int handle);
KYTY_SYSV_ABI int VideoOutSetFlipMaster(int slave_handle, int master_handle);
KYTY_SYSV_ABI void VideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* attribute,
uint64_t option);
KYTY_SYSV_ABI void VideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribute,
uint64_t pixel_format, uint32_t tiling_mode,
uint32_t width, uint32_t height, uint64_t option,
+330 -144
View File
@@ -4,6 +4,8 @@
#include "common/logging/log.h"
#include "common/profiler.h"
#include "common/threads.h"
#include "gpu_blit_shaders/gpu_blit_fs_triangle_spv.h"
#include "gpu_blit_shaders/gpu_video_out_overlay_spv.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
@@ -14,7 +16,7 @@
#include "graphics/presentation/window/windowInternal.h"
#include <algorithm>
#include <deque>
#include <array>
#include <limits>
#include <memory>
#include <vector>
@@ -25,10 +27,10 @@
namespace Libs::Graphics {
struct Presenter::Frame {
VulkanImage image;
uint64_t present_tick = 0;
bool busy = false;
bool reusing_last = false;
VulkanImage image;
vk::ImageView view = nullptr;
uint64_t present_tick = 0;
bool busy = false;
void Configure(GraphicContext& graphics, vk::Extent2D extent, vk::Format format);
void Transit(vk::CommandBuffer command, vk::ImageLayout layout, vk::AccessFlags2 access);
@@ -43,6 +45,7 @@ public:
~FramePool() {
m_scheduler.Wait(m_scheduler.CurrentTick() - 1);
for (auto& frame: m_frames) {
m_window.graphic_ctx.device.destroyImageView(frame->view, nullptr);
if (frame->image.image != nullptr) {
m_window.graphic_ctx.DeleteImage(frame->image);
}
@@ -60,6 +63,7 @@ public:
}
m_format = format;
m_frames.reserve(count);
m_free.reserve(count);
for (uint32_t i = 0; i < count; i++) {
auto frame = std::make_unique<Presenter::Frame>();
m_free.push_back(frame.get());
@@ -83,60 +87,45 @@ public:
return m_format;
}
Presenter::Frame* Acquire() {
Presenter::Frame* Acquire(vk::Extent2D extent, vk::Format format) {
m_mutex.Lock();
if (m_frames.empty()) {
EXIT("prepared-frame pool was used before swapchain initialization\n");
}
while (m_free.empty()) {
m_available.Wait(&m_mutex);
// A synchronized flip may need more frames than the swapchain has images.
// Queue capacity bounds growth; waiting here can prevent its master from recording.
if (m_free.empty()) {
auto frame = std::make_unique<Presenter::Frame>();
m_free.push_back(frame.get());
m_frames.push_back(std::move(frame));
}
auto* frame = m_free.front();
m_free.pop_front();
const auto compatible = std::ranges::find_if(m_free, [&](const auto* frame) {
return frame->image.extent.width == extent.width &&
frame->image.extent.height == extent.height && frame->image.format == format;
});
auto* frame = compatible == m_free.end() ? m_free.back() : *compatible;
if (compatible != m_free.end()) {
*compatible = m_free.back();
}
m_free.pop_back();
if (frame->busy) {
EXIT("prepared-frame pool returned an invalid frame\n");
}
if (m_last_frame == frame) {
m_last_frame = nullptr;
}
frame->busy = true;
frame->reusing_last = false;
frame->busy = true;
m_mutex.Unlock();
WaitForFrame(*frame);
m_scheduler.Wait(frame->present_tick);
return frame;
}
Presenter::Frame* AcquireLast() {
m_mutex.Lock();
auto* frame = m_last_frame;
if (frame == nullptr) {
m_mutex.Unlock();
return nullptr;
}
auto free = std::find(m_free.begin(), m_free.end(), frame);
if (free == m_free.end() || frame->busy) {
m_mutex.Unlock();
EXIT("last submitted frame is not available for reuse\n");
}
m_free.erase(free);
m_last_frame = nullptr;
frame->busy = true;
frame->reusing_last = true;
m_mutex.Unlock();
WaitForFrame(*frame);
return frame;
}
void ValidateForPresent(Presenter::Frame* frame, bool reuse) {
void ValidateForPresent(Presenter::Frame* frame) {
Common::LockGuard lock(m_mutex);
if (frame == nullptr || !frame->busy || frame->reusing_last != reuse) {
if (frame == nullptr || !frame->busy) {
EXIT("prepared frame has invalid presentation ownership\n");
}
}
void Release(Presenter::Frame* frame, bool make_last = false) {
void Release(Presenter::Frame* frame) {
if (frame == nullptr) {
EXIT("cannot release a null prepared frame\n");
}
@@ -144,26 +133,17 @@ public:
if (!frame->busy) {
EXIT("prepared frame was released twice\n");
}
frame->busy = false;
frame->reusing_last = false;
if (make_last) {
m_last_frame = frame;
}
frame->busy = false;
m_free.push_back(frame);
m_available.Signal();
}
private:
void WaitForFrame(Presenter::Frame& frame) { m_scheduler.Wait(frame.present_tick); }
WindowContext& m_window;
CommandScheduler& m_scheduler;
Common::Mutex m_mutex;
Common::CondVar m_available;
std::vector<std::unique_ptr<Presenter::Frame>> m_frames;
std::deque<Presenter::Frame*> m_free;
Presenter::Frame* m_last_frame = nullptr;
vk::Format m_format = vk::Format::eUndefined;
std::vector<Presenter::Frame*> m_free;
vk::Format m_format = vk::Format::eUndefined;
};
void Presenter::Frame::Configure(GraphicContext& graphics, vk::Extent2D extent, vk::Format format) {
@@ -181,13 +161,16 @@ void Presenter::Frame::Configure(GraphicContext& graphics, vk::Extent2D extent,
const auto features = graphics.GetFormatProperties(format).optimalTilingFeatures;
const auto required =
vk::FormatFeatureFlagBits::eBlitSrc | vk::FormatFeatureFlagBits::eSampledImageFilterLinear |
vk::FormatFeatureFlagBits::eTransferSrc | vk::FormatFeatureFlagBits::eTransferDst;
vk::FormatFeatureFlagBits::eSampledImage | vk::FormatFeatureFlagBits::eTransferSrc |
vk::FormatFeatureFlagBits::eTransferDst;
if ((features & required) != required) {
EXIT("prepared presentation format lacks optimal blit support: format=%d features=0x%x\n",
static_cast<int>(format), static_cast<vk::FormatFeatureFlags::MaskType>(features));
}
if (dst.image != nullptr) {
graphics.device.destroyImageView(view, nullptr);
view = nullptr;
graphics.DeleteImage(dst);
}
@@ -200,7 +183,8 @@ void Presenter::Frame::Configure(GraphicContext& graphics, vk::Extent2D extent,
create.format = format;
create.tiling = vk::ImageTiling::eOptimal;
create.initialLayout = vk::ImageLayout::eUndefined;
create.usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst;
create.usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst |
vk::ImageUsageFlagBits::eSampled;
create.sharingMode = vk::SharingMode::eExclusive;
create.samples = vk::SampleCountFlagBits::e1;
if (!graphics.CreateImage(create, dst)) {
@@ -258,7 +242,6 @@ void Presenter::Frame::CopyFrom(CommandBuffer& command_buffer, Image& source) {
EXIT_IF(copy.srcSubresource.layerCount != copy.dstSubresource.layerCount);
command.copyImage(source.backing.image, vk::ImageLayout::eTransferSrcOptimal, image.image,
vk::ImageLayout::eTransferDstOptimal, copy);
Transit(command, vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead);
}
void Presenter::Frame::Clear(CommandBuffer& command_buffer, const vk::ClearColorValue& color) {
@@ -267,7 +250,6 @@ void Presenter::Frame::Clear(CommandBuffer& command_buffer, const vk::ClearColor
Transit(command, vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite);
const vk::ImageSubresourceRange range {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
command.clearColorImage(image.image, vk::ImageLayout::eTransferDstOptimal, &color, 1, &range);
Transit(command, vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead);
}
class Swapchain final {
@@ -283,9 +265,9 @@ public:
[[nodiscard]] bool NeedsResize() const;
[[nodiscard]] Status AcquireNextImage();
[[nodiscard]] bool PrepareSystemOverlay();
void RecordPresentCommands(CommandBuffer& command, VulkanImage& source,
bool draw_system_overlay);
uint64_t Submit(CommandScheduler& scheduler);
void RecordPresentCommands(CommandBuffer& command, Presenter::Frame* source,
const Presenter::Layer& overlay, bool draw_system_overlay);
uint64_t Submit(CommandScheduler& scheduler);
[[nodiscard]] Status Present();
[[nodiscard]] uint32_t ImageCount() const noexcept {
@@ -295,20 +277,25 @@ public:
private:
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;
vk::Extent2D m_extent {};
WindowContext& m_window;
vk::SwapchainKHR m_handle = nullptr;
vk::Format m_format = vk::Format::eUndefined;
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<vk::Semaphore> m_render_complete;
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<vk::Semaphore> m_render_complete;
std::unique_ptr<SystemOverlay> m_system_overlay;
uint32_t m_image_index = static_cast<uint32_t>(-1);
uint32_t m_frame_index = 0;
vk::DescriptorSetLayout m_overlay_descriptors = nullptr;
vk::PipelineLayout m_overlay_layout = nullptr;
vk::Pipeline m_overlay_pipeline = nullptr;
vk::Sampler m_overlay_sampler = nullptr;
uint32_t m_image_index = static_cast<uint32_t>(-1);
uint32_t m_frame_index = 0;
};
struct Presenter::Impl {
@@ -347,12 +334,15 @@ struct Presenter::Impl {
cache.UpdateImage(image_id);
return image;
}
void Present();
RenderContext& renderer;
WindowContext& window;
Swapchain swapchain;
CommandScheduler present_scheduler;
FramePool frames;
Common::Mutex present_mutex;
std::array<Layer, 2> layers {};
std::atomic<uint64_t> presented_overlay_revision {0};
};
@@ -370,7 +360,7 @@ void Swapchain::Create() {
EXIT_IF(m_window_extent.width == 0);
EXIT_IF(m_window_extent.height == 0);
m_window.RefreshSurfaceCapabilities();
const auto& surface = m_window.surface_capabilities;
const auto& surface = m_window.surface_capabilities;
EXIT_NOT_IMPLEMENTED(surface.formats.empty());
m_extent = surface.capabilities.currentExtent;
@@ -442,7 +432,7 @@ void Swapchain::Create() {
LOGF("warning: requested present mode is unavailable; falling back to Fifo\n");
create_info.presentMode = vk::PresentModeKHR::eFifo;
}
create_info.clipped = VK_TRUE;
create_info.clipped = VK_TRUE;
RequireVulkanSuccess(graphics.device.createSwapchainKHR(&create_info, nullptr, &m_handle),
"vkCreateSwapchainKHR");
EXIT_IF(m_handle == nullptr);
@@ -483,8 +473,8 @@ void Swapchain::Create() {
graphics.device.createSemaphore(&semaphore_info, nullptr, &m_render_complete[i]),
"create swapchain render-complete semaphore");
}
m_image_index = static_cast<uint32_t>(-1);
m_frame_index = 0;
m_image_index = static_cast<uint32_t>(-1);
m_frame_index = 0;
}
Swapchain::~Swapchain() {
@@ -505,6 +495,14 @@ void Swapchain::Destroy() {
if (m_system_overlay != nullptr) {
m_system_overlay->ReleaseVulkan();
}
graphics.device.destroyPipeline(m_overlay_pipeline, nullptr);
graphics.device.destroyPipelineLayout(m_overlay_layout, nullptr);
graphics.device.destroyDescriptorSetLayout(m_overlay_descriptors, nullptr);
graphics.device.destroySampler(m_overlay_sampler, nullptr);
m_overlay_pipeline = nullptr;
m_overlay_layout = nullptr;
m_overlay_descriptors = nullptr;
m_overlay_sampler = nullptr;
for (const auto semaphore: m_image_acquired) {
if (semaphore != nullptr) {
@@ -544,8 +542,8 @@ void Swapchain::Recreate(bool surface_lost) {
// Surface recreation goes through SDL_Vulkan_CreateSurface, which touches the
// window's view/layer and must run on the main thread on macOS.
EXIT_IF(!SDL_RunOnMainThread(
[](void* window) { static_cast<WindowContext*>(window)->RecreateSurface(); },
&m_window, true));
[](void* window) { static_cast<WindowContext*>(window)->RecreateSurface(); }, &m_window,
true));
#else
m_window.RecreateSurface();
#endif
@@ -592,14 +590,147 @@ bool Swapchain::PrepareSystemOverlay() {
return m_system_overlay->PrepareFrame(m_extent, m_format, ImageCount());
}
void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& source,
bool draw_system_overlay) {
if (source.state.layout != vk::ImageLayout::eTransferSrcOptimal) {
EXIT("invalid prepared presentation image, vk_image=%p layout=%d\n",
static_cast<void*>(source.image), static_cast<int>(source.state.layout));
void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer) {
auto device = m_window.graphic_ctx.device;
if (m_overlay_pipeline == nullptr) {
vk::SamplerCreateInfo sampler {};
sampler.magFilter = vk::Filter::eLinear;
sampler.minFilter = vk::Filter::eLinear;
sampler.addressModeU = vk::SamplerAddressMode::eClampToEdge;
sampler.addressModeV = vk::SamplerAddressMode::eClampToEdge;
sampler.addressModeW = vk::SamplerAddressMode::eClampToEdge;
RequireVulkanSuccess(device.createSampler(&sampler, nullptr, &m_overlay_sampler),
"create video-out overlay sampler");
const vk::DescriptorSetLayoutBinding binding {0, vk::DescriptorType::eCombinedImageSampler,
1, vk::ShaderStageFlagBits::eFragment};
vk::DescriptorSetLayoutCreateInfo descriptors {};
descriptors.flags = vk::DescriptorSetLayoutCreateFlagBits::ePushDescriptorKHR;
descriptors.bindingCount = 1;
descriptors.pBindings = &binding;
RequireVulkanSuccess(
device.createDescriptorSetLayout(&descriptors, nullptr, &m_overlay_descriptors),
"create video-out overlay descriptor layout");
const vk::PushConstantRange alpha {vk::ShaderStageFlagBits::eFragment, 0, sizeof(uint32_t)};
vk::PipelineLayoutCreateInfo layout {};
layout.setLayoutCount = 1;
layout.pSetLayouts = &m_overlay_descriptors;
layout.pushConstantRangeCount = 1;
layout.pPushConstantRanges = &alpha;
RequireVulkanSuccess(device.createPipelineLayout(&layout, nullptr, &m_overlay_layout),
"create video-out overlay pipeline layout");
const auto vertex = CompileSPV(GPU_BLIT_FS_TRIANGLE_SPV, device);
const auto fragment = CompileSPV(GPU_VIDEO_OUT_OVERLAY_SPV, device);
std::array<vk::PipelineShaderStageCreateInfo, 2> stages {};
stages[0].stage = vk::ShaderStageFlagBits::eVertex;
stages[0].module = vertex;
stages[0].pName = "main";
stages[1].stage = vk::ShaderStageFlagBits::eFragment;
stages[1].module = fragment;
stages[1].pName = "main";
vk::PipelineVertexInputStateCreateInfo vertex_input {};
vk::PipelineInputAssemblyStateCreateInfo assembly {};
assembly.topology = vk::PrimitiveTopology::eTriangleList;
vk::PipelineViewportStateCreateInfo viewport {};
viewport.viewportCount = 1;
viewport.scissorCount = 1;
vk::PipelineRasterizationStateCreateInfo rasterization {};
rasterization.lineWidth = 1.0f;
vk::PipelineMultisampleStateCreateInfo multisample {};
multisample.rasterizationSamples = vk::SampleCountFlagBits::e1;
vk::PipelineColorBlendAttachmentState attachment {};
attachment.blendEnable = VK_TRUE;
attachment.srcColorBlendFactor = vk::BlendFactor::eOne;
attachment.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha;
attachment.srcAlphaBlendFactor = vk::BlendFactor::eOne;
attachment.dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha;
attachment.colorWriteMask = vk::ColorComponentFlagBits::eR |
vk::ColorComponentFlagBits::eG |
vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA;
vk::PipelineColorBlendStateCreateInfo blend {};
blend.attachmentCount = 1;
blend.pAttachments = &attachment;
const std::array dynamic_states {vk::DynamicState::eViewport, vk::DynamicState::eScissor};
vk::PipelineDynamicStateCreateInfo dynamic {};
dynamic.dynamicStateCount = static_cast<uint32_t>(dynamic_states.size());
dynamic.pDynamicStates = dynamic_states.data();
vk::PipelineRenderingCreateInfo rendering {};
rendering.colorAttachmentCount = 1;
rendering.pColorAttachmentFormats = &m_format;
vk::GraphicsPipelineCreateInfo create {};
create.pNext = &rendering;
create.stageCount = static_cast<uint32_t>(stages.size());
create.pStages = stages.data();
create.pVertexInputState = &vertex_input;
create.pInputAssemblyState = &assembly;
create.pViewportState = &viewport;
create.pRasterizationState = &rasterization;
create.pMultisampleState = &multisample;
create.pColorBlendState = &blend;
create.pDynamicState = &dynamic;
create.layout = m_overlay_layout;
RequireVulkanSuccess(
device.createGraphicsPipelines(nullptr, 1, &create, nullptr, &m_overlay_pipeline),
"create video-out overlay pipeline");
device.destroyShaderModule(fragment, nullptr);
device.destroyShaderModule(vertex, nullptr);
}
auto& frame = *layer.frame;
if (frame.view == nullptr) {
vk::ImageViewCreateInfo view {};
view.image = frame.image.image;
view.viewType = vk::ImageViewType::e2D;
view.format = frame.image.format;
view.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
RequireVulkanSuccess(device.createImageView(&view, nullptr, &frame.view),
"create video-out overlay image view");
}
const vk::DescriptorImageInfo image {m_overlay_sampler, frame.view,
vk::ImageLayout::eShaderReadOnlyOptimal};
vk::WriteDescriptorSet write {};
write.dstBinding = 0;
write.descriptorCount = 1;
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;
command.pushConstants(m_overlay_layout, vk::ShaderStageFlagBits::eFragment, 0,
sizeof(premultiplied), &premultiplied);
command.bindPipeline(vk::PipelineBindPoint::eGraphics, m_overlay_pipeline);
vk::RenderingAttachmentInfo attachment {};
attachment.imageView = m_image_views[m_image_index];
attachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal;
attachment.loadOp = vk::AttachmentLoadOp::eLoad;
attachment.storeOp = vk::AttachmentStoreOp::eStore;
vk::RenderingInfo rendering {};
rendering.renderArea.extent = m_extent;
rendering.layerCount = 1;
rendering.colorAttachmentCount = 1;
rendering.pColorAttachments = &attachment;
command.beginRendering(&rendering);
const vk::Viewport viewport {
0.0f, 0.0f, static_cast<float>(m_extent.width), static_cast<float>(m_extent.height),
0.0f, 1.0f};
const vk::Rect2D scissor {{0, 0}, m_extent};
command.setViewport(0, 1, &viewport);
command.setScissor(0, 1, &scissor);
command.draw(3, 1, 0, 0);
command.endRendering();
}
void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame* source,
const Presenter::Layer& overlay, bool draw_system_overlay) {
EXIT_IF(m_image_index >= m_images.size());
auto vk_command = command.Handle();
auto vk_command = command.Handle();
const bool draw_overlay = overlay.frame != nullptr;
const bool draw_attachment = draw_overlay || draw_system_overlay;
if (source != nullptr) {
source->Transit(vk_command, vk::ImageLayout::eTransferSrcOptimal,
vk::AccessFlagBits2::eTransferRead);
}
if (draw_overlay) {
overlay.frame->Transit(vk_command, vk::ImageLayout::eShaderReadOnlyOptimal,
vk::AccessFlagBits2::eShaderRead);
}
vk::ImageMemoryBarrier to_transfer {};
to_transfer.sType = vk::StructureType::eImageMemoryBarrier;
@@ -619,49 +750,68 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& sourc
vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlags {}, 0,
nullptr, 0, nullptr, 1, &to_transfer);
vk::ImageBlit region {};
region.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
region.srcSubresource.mipLevel = 0;
region.srcSubresource.baseArrayLayer = 0;
region.srcSubresource.layerCount = 1;
region.srcOffsets[1].x = static_cast<int>(source.extent.width);
region.srcOffsets[1].y = static_cast<int>(source.extent.height);
region.srcOffsets[1].z = 1;
region.dstSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
region.dstSubresource.mipLevel = 0;
region.dstSubresource.baseArrayLayer = 0;
region.dstSubresource.layerCount = 1;
region.dstOffsets[1].x = static_cast<int>(m_extent.width);
region.dstOffsets[1].y = static_cast<int>(m_extent.height);
region.dstOffsets[1].z = 1;
vk_command.blitImage(source.image, vk::ImageLayout::eTransferSrcOptimal,
m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal, 1, &region,
vk::Filter::eLinear);
if (source != nullptr) {
vk::ImageBlit region {};
region.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
region.srcSubresource.mipLevel = 0;
region.srcSubresource.baseArrayLayer = 0;
region.srcSubresource.layerCount = 1;
region.srcOffsets[1].x = static_cast<int>(source->image.extent.width);
region.srcOffsets[1].y = static_cast<int>(source->image.extent.height);
region.srcOffsets[1].z = 1;
region.dstSubresource.aspectMask = vk::ImageAspectFlagBits::eColor;
region.dstSubresource.mipLevel = 0;
region.dstSubresource.baseArrayLayer = 0;
region.dstSubresource.layerCount = 1;
region.dstOffsets[1].x = static_cast<int>(m_extent.width);
region.dstOffsets[1].y = static_cast<int>(m_extent.height);
region.dstOffsets[1].z = 1;
vk_command.blitImage(source->image.image, vk::ImageLayout::eTransferSrcOptimal,
m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal, 1,
&region, vk::Filter::eLinear);
} else {
const vk::ClearColorValue black {};
vk_command.clearColorImage(m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal,
&black, 1, &to_transfer.subresourceRange);
}
vk::ImageMemoryBarrier to_present {};
to_present.sType = vk::StructureType::eImageMemoryBarrier;
to_present.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
to_present.dstAccessMask = draw_system_overlay ? vk::AccessFlagBits::eColorAttachmentRead |
vk::AccessFlagBits::eColorAttachmentWrite
: vk::AccessFlagBits::eMemoryRead;
to_present.oldLayout = vk::ImageLayout::eTransferDstOptimal;
to_present.newLayout = draw_system_overlay ? vk::ImageLayout::eColorAttachmentOptimal
: vk::ImageLayout::ePresentSrcKHR;
to_present.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_present.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_present.image = m_images[m_image_index];
to_present.sType = vk::StructureType::eImageMemoryBarrier;
to_present.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
to_present.dstAccessMask = draw_attachment ? vk::AccessFlagBits::eColorAttachmentRead |
vk::AccessFlagBits::eColorAttachmentWrite
: vk::AccessFlagBits::eMemoryRead;
to_present.oldLayout = vk::ImageLayout::eTransferDstOptimal;
to_present.newLayout = draw_attachment ? vk::ImageLayout::eColorAttachmentOptimal
: vk::ImageLayout::ePresentSrcKHR;
to_present.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_present.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
to_present.image = m_images[m_image_index];
to_present.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor;
to_present.subresourceRange.baseMipLevel = 0;
to_present.subresourceRange.levelCount = 1;
to_present.subresourceRange.baseArrayLayer = 0;
to_present.subresourceRange.layerCount = 1;
vk_command.pipelineBarrier(
vk::PipelineStageFlagBits::eTransfer,
draw_system_overlay ? vk::PipelineStageFlagBits::eColorAttachmentOutput
: vk::PipelineStageFlagBits::eAllCommands,
vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1, &to_present);
if (draw_system_overlay) {
m_system_overlay->Record(vk_command, m_image_views[m_image_index]);
vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
draw_attachment ? vk::PipelineStageFlagBits::eColorAttachmentOutput
: vk::PipelineStageFlagBits::eAllCommands,
vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1,
&to_present);
if (draw_attachment) {
if (draw_overlay) {
DrawOverlay(vk_command, overlay);
if (draw_system_overlay) {
to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal;
vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput,
vk::PipelineStageFlagBits::eColorAttachmentOutput,
vk::DependencyFlagBits::eByRegion, 0, nullptr, 0,
nullptr, 1, &to_present);
}
}
if (draw_system_overlay) {
m_system_overlay->Record(vk_command, m_image_views[m_image_index]);
}
to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite;
to_present.dstAccessMask = vk::AccessFlagBits::eMemoryRead;
to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal;
@@ -721,19 +871,18 @@ Presenter::~Presenter() = default;
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(buffer.IsInvalid());
auto* frame = m_impl->frames.Acquire();
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
auto& image = m_impl->ResolveSurface(info);
if (image.backing.format == vk::Format::eUndefined) {
EXIT("unsupported presentation source, image=%p\n", static_cast<const void*>(&image));
}
auto frame_format = info.pixel_format;
switch (frame_format) {
case vk::Format::eR8G8B8A8Srgb: frame_format = vk::Format::eR8G8B8A8Unorm; break;
case vk::Format::eB8G8R8A8Srgb: frame_format = vk::Format::eB8G8R8A8Unorm; break;
default: break;
}
auto* frame = m_impl->frames.Acquire({info.extent.width, info.extent.height}, frame_format);
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
auto& image = m_impl->ResolveSurface(info);
if (image.backing.format == vk::Format::eUndefined) {
EXIT("unsupported presentation source, image=%p\n", static_cast<const void*>(&image));
}
frame->Configure(m_impl->window.graphic_ctx,
{image.backing.extent.width, image.backing.extent.height}, frame_format);
frame->CopyFrom(buffer, image);
@@ -744,7 +893,7 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height,
CommandBuffer* producer) {
KYTY_PROFILER_FUNCTION();
auto format = m_impl->frames.GetFormat();
auto* frame = m_impl->frames.Acquire();
auto* frame = m_impl->frames.Acquire({width, height}, format);
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
frame->Configure(m_impl->window.graphic_ctx, {width, height}, format);
vk::ClearColorValue clear {};
@@ -760,8 +909,13 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height,
return *frame;
}
Presenter::Frame* Presenter::PrepareLastFrame() {
return m_impl->frames.AcquireLast();
bool Presenter::PresentLastFrame() {
Common::LockGuard lock(m_impl->present_mutex);
if (m_impl->layers[0].frame == nullptr && m_impl->layers[1].frame == nullptr) {
return false;
}
m_impl->Present();
return true;
}
bool Presenter::IsGuestPaused() const noexcept {
@@ -778,44 +932,76 @@ RenderContext& Presenter::Renderer() const noexcept {
return m_impl->renderer;
}
void Presenter::Present(Frame& frame, bool reuse) {
void Presenter::Present(Frame& frame) {
const Layer layer {&frame, 0, false};
Present(std::span(&layer, 1));
}
void Presenter::Present(std::span<const Layer> layers) {
Common::LockGuard lock(m_impl->present_mutex);
for (const auto& layer: layers) {
EXIT_IF(layer.bus < 0 || layer.bus >= static_cast<int>(m_impl->layers.size()));
m_impl->frames.ValidateForPresent(layer.frame);
auto& previous = m_impl->layers[layer.bus];
if (previous.frame != nullptr) {
m_impl->frames.Release(previous.frame);
}
previous = layer;
}
m_impl->Present();
}
void Presenter::ClearLayer(int bus) {
if (static_cast<size_t>(bus) >= m_impl->layers.size()) {
return;
}
Common::LockGuard lock(m_impl->present_mutex);
auto& layer = m_impl->layers[bus];
if (layer.frame != nullptr) {
m_impl->frames.Release(layer.frame);
layer = {};
}
}
void Presenter::Impl::Present() {
KYTY_PROFILER_FUNCTION();
m_impl->frames.ValidateForPresent(&frame, reuse);
const auto overlay_visual = GetSystemOverlayVisualState();
auto& swapchain = m_impl->swapchain;
// Some window systems keep presenting an old swapchain after a resize.
if (swapchain.NeedsResize()) {
m_impl->RecoverSwapchain(Swapchain::Status::Recreate);
RecoverSwapchain(Swapchain::Status::Recreate);
}
for (uint32_t attempt = 0; attempt < 2; attempt++) {
auto status = swapchain.AcquireNextImage();
if (status != Swapchain::Status::Success) {
m_impl->RecoverSwapchain(status);
RecoverSwapchain(status);
continue;
}
{
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
auto& command = m_impl->present_scheduler.BeginCommand();
Common::LockGuard render_lock(renderer.GetMutex());
auto& command = present_scheduler.BeginCommand();
const bool draw_system_overlay =
overlay_visual.active && swapchain.PrepareSystemOverlay();
swapchain.RecordPresentCommands(command, frame.image, draw_system_overlay);
frame.present_tick = swapchain.Submit(m_impl->present_scheduler);
swapchain.RecordPresentCommands(command, layers[0].frame, layers[1],
draw_system_overlay);
const auto tick = swapchain.Submit(present_scheduler);
for (const auto& layer: layers) {
if (layer.frame != nullptr) {
layer.frame->present_tick = tick;
}
}
}
status = swapchain.Present();
if (status != Swapchain::Status::Success) {
m_impl->RecoverSwapchain(status);
RecoverSwapchain(status);
continue;
}
m_impl->presented_overlay_revision.store(overlay_visual.revision,
std::memory_order_release);
m_impl->window.UpdateTitle();
m_impl->frames.Release(&frame, true);
presented_overlay_revision.store(overlay_visual.revision, std::memory_order_release);
window.UpdateTitle();
return;
}
LOGF("Vulkan presentation retry exhausted; dropping frame\n");
m_impl->frames.Release(&frame, reuse);
}
void Presenter::Discard(Frame& frame) {
@@ -564,8 +564,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
vk::PhysicalDeviceVulkan11Properties properties11 {};
properties11.pNext = &subgroup_size_control;
vk::PhysicalDeviceFloatControlsProperties float_controls {};
float_controls.pNext = &properties11;
vk::PhysicalDeviceProperties2 properties2 {};
properties2.pNext = &properties11;
properties2.pNext = &float_controls;
if (graphics.mesh_shader_enabled) {
subgroup_size_control.pNext = &graphics.mesh_shader_properties;
@@ -628,6 +630,15 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
device_features.vertexPipelineStoresAndAtomics = VK_TRUE;
graphics.sample_rate_shading_enabled = true;
device_features.shaderInt64 = VK_TRUE;
device_features.shaderFloat64 =
supported_features2.features.shaderFloat64 &&
float_controls.shaderSignedZeroInfNanPreserveFloat64 &&
float_controls.shaderRoundingModeRTEFloat32;
// if (device_features.shaderFloat64 && !float_controls.shaderDenormPreserveFloat64) {
// Log::WriteToConsoleAndLog(
// "WARNING: Vulkan device does not guarantee FP64 denormal preservation; "
// "continuing with native FP64 arithmetic. Very small values may be flushed to zero.\n");
// }
vk::PhysicalDeviceRobustness2FeaturesEXT robustness2 {};
#if defined(__APPLE__)
@@ -15,7 +15,6 @@
#include "graphics/shader/recompiler/ir/passes/ResourceMaterialization.h"
#include "graphics/shader/recompiler/ir/passes/ResourceTracking.h"
#include "graphics/shader/recompiler/ir/passes/ShaderInfoCollection.h"
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
#include "graphics/shader/recompiler/ir/passes/SsaRewrite.h"
#include <algorithm>
@@ -548,30 +547,40 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " CFG BuildGraph\n", GetDumpLabel(options),
StageName(options.stage), options.shader_hash);
auto cfg = CFG::BuildGraph(decoded);
auto native_cfg = CFG::BuildGraph(decoded);
CFG::Graph structured_cfg;
auto* selected_cfg = &native_cfg;
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " CFG BuildGraph blocks=%" PRIu64
" loops=%" PRIu64 " back_edges=%" PRIu64 " elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
static_cast<uint64_t>(cfg.blocks.size()), static_cast<uint64_t>(cfg.natural_loops.size()),
static_cast<uint64_t>(cfg.back_edges.size()), phase_ms());
if (cfg.irreducible) {
LogDispatcherFallback(options, cfg, "build");
static_cast<uint64_t>(native_cfg.blocks.size()),
static_cast<uint64_t>(native_cfg.natural_loops.size()),
static_cast<uint64_t>(native_cfg.back_edges.size()), phase_ms());
if (native_cfg.irreducible) {
LogDispatcherFallback(options, native_cfg, "build");
} else {
LOGF("%s phase begin: stage=%s hash=0x%016" PRIx64 " CFG Structurize\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash);
if (!CFG::Structurize(cfg)) {
LogDispatcherFallback(options, cfg, "structurize");
structured_cfg = CFG::Structurize(native_cfg);
if (structured_cfg.unsupported) {
native_cfg.unsupported = true;
native_cfg.failure_kind = structured_cfg.failure_kind;
native_cfg.failure_block = structured_cfg.failure_block;
native_cfg.unsupported_reason = structured_cfg.unsupported_reason;
LogDispatcherFallback(options, native_cfg, "structurize");
} else {
selected_cfg = &structured_cfg;
LOGF("%s structured CFG success: blocks=%" PRIu64 "\n", GetDumpLabel(options),
static_cast<uint64_t>(cfg.blocks.size()));
static_cast<uint64_t>(selected_cfg->blocks.size()));
}
LOGF("%s phase end: stage=%s hash=0x%016" PRIx64 " CFG Structurize blocks=%" PRIu64
" loops=%" PRIu64 " elapsed_ms=%" PRIu64 "\n",
GetDumpLabel(options), StageName(options.stage), options.shader_hash,
static_cast<uint64_t>(cfg.blocks.size()),
static_cast<uint64_t>(cfg.natural_loops.size()), phase_ms());
static_cast<uint64_t>(selected_cfg->blocks.size()),
static_cast<uint64_t>(selected_cfg->natural_loops.size()), phase_ms());
}
const auto& cfg = *selected_cfg;
Frontend::EmbeddedFetchPlan embedded_fetch;
if ((options.stage == ShaderType::Vertex || options.stage == ShaderType::Local) &&
options.input_info.vertex != nullptr && options.input_info.vertex->fetch_embedded) {
@@ -614,15 +623,21 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
IR::EliminateDeadCode(ir.blocks);
}
LowerTessellationMemory(ir, options);
IR::BuildSrtPlan(ir);
IR::EliminateDeadCode(ir.blocks);
IR::TrackResources(ir);
std::string cfg_dump;
if (options.dump_ir) {
cfg_dump = CFG::GraphToString(cfg);
if (options.early_dump) {
LOGF("%s native IR before resource tracking:\n%s", GetDumpLabel(options),
MakeIrDump(cfg_dump, ir).c_str());
}
}
IR::TrackResources(ir, decoded, native_cfg);
IR::EliminateDeadCode(ir.blocks);
TranslateResult result;
result.program = std::move(ir);
if (options.dump_ir) {
result.decoded_dump = std::move(decoded_dump);
result.cfg_dump = CFG::GraphToString(cfg);
result.cfg_dump = std::move(cfg_dump);
}
return result;
}
@@ -634,6 +649,35 @@ CompileResult CompileProgram(TranslateResult translated, const CompileOptions& o
const auto emit_begin = std::chrono::steady_clock::now();
auto& ir = translated.program;
IR::ApplyResourceSpecialization(ir, specialization);
// The resource plan owns host descriptor evaluation now. Keep only dependencies consumed
// by GPU memory operations; bound descriptor dwords must not retain shader instructions.
for (auto& inst: ir.value_storage) {
inst.Invalidate();
}
for (auto* block: ir.blocks) {
for (auto& inst: *block) {
const auto op = inst.GetOpcode();
uint32_t first = 0;
if (op == IR::ValueOpcode::GetBufferResource) {
if (std::ranges::any_of(inst.Uses(), [&](const IR::Use& use) {
return ir.memory_info[use.user->Flags<IR::MemoryFlags>().index].kind ==
IR::ResourceKind::IndirectBuffer;
})) {
continue;
}
} else if (op == IR::ValueOpcode::GetImageResource) {
const auto resource = inst.Flags<uint32_t>();
first = resource < ir.info.images.size() &&
ir.info.images[resource].indirect_root == resource ? 1u : 0u;
} else if (op != IR::ValueOpcode::GetSamplerResource) {
continue;
}
for (size_t index = first; index < inst.NumArgs(); index++) {
inst.SetArg(index, IR::Value(0u));
}
}
}
ir.value_storage.clear();
IR::RemoveIdentities(ir.blocks);
IR::EliminateDeadCode(ir.blocks);
@@ -3,6 +3,7 @@
#include "common/assert.h"
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/recompiler/ir/passes/BindingLayout.h"
#include <algorithm>
#include <array>
@@ -34,9 +35,6 @@ void ValidateNativeProgram(const IR::Program& program) {
present[index] = true;
expected[index] = std::move(resources);
};
if (!program.info.buffers.empty()) {
Expect(Kind::Buffers, Dense(program.info.buffers.size()));
}
for (uint32_t i = 0; i < program.info.images.size(); i++) {
const auto kind = IR::DescriptorBindingForImage(program.info.images[i]);
if (!kind.has_value()) {
@@ -54,23 +52,9 @@ void ValidateNativeProgram(const IR::Program& program) {
if (!program.info.samplers.empty()) {
Expect(Kind::Samplers, Dense(program.info.samplers.size()));
}
bool uses_gds = false;
for (const auto* block: program.blocks) {
for (const auto& inst: *block) {
if (IR::SharedAccessOf(inst.GetOpcode()) == IR::SharedAccess::None) {
continue;
}
const auto index = inst.Flags<IR::MemoryFlags>().index;
if (index >= program.memory_info.size()) {
Fail(program, "shared operation has invalid memory metadata");
}
const auto kind = program.memory_info[index].kind;
if (kind != IR::ResourceKind::Lds && kind != IR::ResourceKind::Gds) {
Fail(program, "shared operation has invalid resource kind");
}
uses_gds |= kind == IR::ResourceKind::Gds;
}
}
auto& buffers = expected[static_cast<size_t>(Kind::Buffers)];
const bool uses_gds = IR::CollectMemoryResources(program, buffers);
present[static_cast<size_t>(Kind::Buffers)] = !buffers.empty();
if (uses_gds) {
Expect(Kind::Gds);
}
@@ -78,12 +62,7 @@ void ValidateNativeProgram(const IR::Program& program) {
Expect(Kind::BdaPagetable);
Expect(Kind::FaultBuffer);
}
const bool uses_flattened_runtime =
!program.srt_reads.empty() ||
std::ranges::any_of(program.info.images, [](const IR::ImageResource& image) {
return image.indirect_search_iterations != 0u;
});
if (uses_flattened_runtime) {
if (IR::UsesFlattenedSrt(program)) {
Expect(Kind::FlattenedSrt);
}
if (program.bindings.ShaderDataDwords() != 0 && !program.bindings.UsesPushData()) {
@@ -109,7 +88,7 @@ void ValidateNativeProgram(const IR::Program& program) {
if ((program.bindings.UsesPushData() &&
!IR::PushData::CanFit(program.bindings.push_data_start_dword, shader_data_dwords)) ||
program.bindings.memory_offset_dword != program.bindings.user_data_registers.size() ||
program.bindings.memory_offset_count != program.info.buffers.size() ||
program.bindings.memory_offset_count != buffers.size() ||
has_shader_data_storage != (shader_data_dwords != 0 && !program.bindings.UsesPushData()) ||
!std::is_sorted(program.bindings.user_data_registers.begin(),
program.bindings.user_data_registers.end()) ||
@@ -198,6 +177,7 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
SpirvRequirements requirements {};
for (const auto* block: program.blocks) {
for (const auto& inst: *block) {
requirements.float64 |= inst.GetType() == IR::Type::F64;
if (IR::BufferAccessOf(inst.GetOpcode()) == IR::BufferAccess::Atomic &&
inst.GetType() == IR::Type::U64) {
requirements.buffer_int64_atomics = true;
@@ -291,9 +291,25 @@ uint32_t EmitConvertU32F32(EmitterState& state, uint32_t arg0) {
return EmitF32ToU32(state, arg0, false);
}
uint32_t EmitConvertF32S32(EmitterState& state, uint32_t arg0) {
const auto signed_value = Unary(state, spv::OpBitcast, TypeI32(state), arg0);
return EmitNative<spv::OpConvertSToF, IR::Type::F32>(state, signed_value);
uint32_t EmitConvertF32F64(EmitterState& state, uint32_t arg0) {
const auto converted = Unary(state, spv::OpFConvert, TypeF32(state), arg0);
const auto source = EmitNative<spv::OpCompositeExtract, IR::Type::U32>(
state, Unary(state, spv::OpBitcast, TypeU64(state), arg0), 1u);
const auto exponent = EmitAndConstant(state, source, 0x7ff00000u);
const auto overflow =
Binary(state, spv::OpLogicalAnd, TypeBool(state),
EmitCompareU32Constant(state, spv::OpUGreaterThan, exponent, 0x47e00000u),
EmitCompareU32Constant(state, spv::OpULessThan, exponent, 0x7ff00000u));
const auto clamped = EmitOrU32(state, EmitAndConstant(state, source, 0x80000000u),
ConstantU32(state, 0x7f7fffffu));
return EmitFlushF32DenormToSignedZero(
state, Select(state, TypeF32(state), overflow,
Unary(state, spv::OpBitcast, TypeF32(state), clamped), converted));
}
uint32_t EmitConvertF64F32(EmitterState& state, uint32_t arg0) {
return EmitNative<spv::OpFConvert, IR::Type::F64>(state,
EmitFlushF32DenormToSignedZero(state, arg0));
}
uint32_t EmitCompositeExtractU64(EmitterState& state, uint32_t arg0, IR::Value arg1) {
@@ -453,6 +469,11 @@ uint32_t EmitFPRecip32(EmitterState& state, uint32_t arg0) {
return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), source);
}
uint32_t EmitFPRecip64(EmitterState& state, uint32_t arg0) {
const auto one = state.builder.Constant(spv::OpConstant, TypeF64(state), 0u, 0x3ff00000u);
return EmitNative<spv::OpFDiv, IR::Type::F64>(state, one, arg0);
}
uint32_t EmitFPRecipIFlag32(EmitterState& state, uint32_t arg0) {
// Integer-to-float inputs used by IFLAG cannot be denormal.
return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), arg0);
@@ -203,9 +203,7 @@ uint32_t EmitFlushF32DenormToSignedZero(EmitterState& state, uint32_t value) {
const auto bits = state.builder.AllocateId();
const auto abs_bits = state.builder.AllocateId();
const auto sign_bits = state.builder.AllocateId();
const auto non_zero = state.builder.AllocateId();
const auto subnormal = state.builder.AllocateId();
const auto flush = state.builder.AllocateId();
const auto selected = state.builder.AllocateId();
const auto ret = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitcast, TypeU32(state), bits, value);
@@ -213,12 +211,9 @@ uint32_t EmitFlushF32DenormToSignedZero(EmitterState& state, uint32_t value) {
ConstantU32(state, 0x7fffffffu));
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), sign_bits, bits,
ConstantU32(state, 0x80000000u));
state.builder.AddFunction(spv::OpINotEqual, TypeBool(state), non_zero, abs_bits,
ConstantU32(state, 0));
state.builder.AddFunction(spv::OpULessThan, TypeBool(state), subnormal, abs_bits,
ConstantU32(state, 0x00800000u));
state.builder.AddFunction(spv::OpLogicalAnd, TypeBool(state), flush, non_zero, subnormal);
state.builder.AddFunction(spv::OpSelect, TypeU32(state), selected, flush, sign_bits, bits);
state.builder.AddFunction(spv::OpSelect, TypeU32(state), selected, subnormal, sign_bits, bits);
state.builder.AddFunction(spv::OpBitcast, TypeF32(state), ret, selected);
return ret;
}
@@ -636,6 +636,29 @@ uint32_t EmitDppUpdateU32(ValueEmitContext& ctx, const IR::Inst& inst) {
ctx.Arg(inst, 1));
}
uint32_t EmitConditionRef(ValueEmitContext& ctx, const IR::Inst& inst) {
if (ctx.other_half == nullptr) return ctx.Arg(inst, 0);
// A native scalar branch makes one decision for both emulated wave halves.
if (ctx.half != 0) return ctx.other_half->Def(IR::Value(&inst));
const auto kind = inst.Flags<CFG::BranchCondition>();
if (kind == CFG::BranchCondition::ScalarInstruction) return ctx.Arg(inst, 0);
const auto ballot = ctx.Ballot(inst.Arg(0));
const auto low = ctx.state.builder.AllocateId();
const auto high = ctx.state.builder.AllocateId();
const auto combined = ctx.state.builder.AllocateId();
const auto result = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), low, ballot, 0);
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), high, ballot, 1);
const bool zero = kind == CFG::BranchCondition::ExecZero ||
kind == CFG::BranchCondition::VccZero || kind == CFG::BranchCondition::SccZero;
ctx.state.builder.AddFunction(zero ? spv::OpBitwiseAnd : spv::OpBitwiseOr,
TypeU32(ctx.state), combined, low, high);
ctx.state.builder.AddFunction(zero ? spv::OpIEqual : spv::OpINotEqual,
TypeBool(ctx.state), result, combined,
ConstantU32(ctx.state, zero ? ~0u : 0u));
return result;
}
uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate) {
return ctx.Ballot(predicate);
}
@@ -877,7 +877,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
return;
}
const auto atomic_opcode = ImageAtomicOpcode(op);
if (atomic_opcode != spv::OpNop) {
if (image_info.access == IR::ImageAccess::Atomic) {
const auto dimension = image.dimension;
ctx.Define(inst, EmitValueOrZeroIfCondition(state, ctx.Arg(inst, 3), [&]() {
const auto pointer = state.builder.AllocateId();
@@ -887,6 +887,15 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
StorageImageDescriptorPointer(state, mem.resource),
CoordU32(ctx, mem, *address, dimension),
ConstantU32(state, 0));
if (op == IR::ValueOpcode::ImageAtomicFMin32 ||
op == IR::ValueOpcode::ImageAtomicFMax32) {
return AtomicUpdate(state, pointer, IR::ResourceKind::Image,
[&](uint32_t old) {
return EmitFloatAtomicReplacement(
state, old, ctx.Arg(inst, 2),
op == IR::ValueOpcode::ImageAtomicFMax32);
});
}
const auto old = state.builder.AllocateId();
state.builder.AddFunction(atomic_opcode, TypeU32(state), old, pointer,
ConstantU32(state, spv::ScopeDevice),
@@ -2,6 +2,8 @@
#include "graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h"
#include <bit>
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
// Operand signatures distinguish SPIR-V IDs (uint32_t), raw literals/values
// (IR::Value), and instructions whose metadata or operand loads must stay lazy.
@@ -13,14 +15,25 @@ inline constexpr auto EmitConvertU32U16 = EmitBitCastU16F16;
inline constexpr auto EmitConvertU32U8 = EmitBitCastU16F16;
inline constexpr auto EmitBitCastU32F32 = EmitBitCastU16F16;
EMIT_NATIVE(BitCastF32U32, OpBitcast, F32, uint32_t)
EMIT_NATIVE(BitCastU64F64, OpBitcast, U64, uint32_t)
EMIT_NATIVE(BitCastF64U64, OpBitcast, F64, uint32_t)
uint32_t EmitConvertU16U32(EmitterState& state, uint32_t arg0);
uint32_t EmitConvertU8U32(EmitterState& state, uint32_t arg0);
uint32_t EmitConvertF16F32(EmitterState& state, uint32_t arg0);
inline constexpr auto EmitConvertF32F16 = EmitF16BitsToF32;
uint32_t EmitConvertS32F32(EmitterState& state, uint32_t arg0);
uint32_t EmitConvertU32F32(EmitterState& state, uint32_t arg0);
uint32_t EmitConvertF32S32(EmitterState& state, uint32_t arg0);
template <IR::Type type>
uint32_t EmitConvertSigned32ToFloat(EmitterState& state, uint32_t arg0) {
const auto signed_value = Unary(state, spv::OpBitcast, TypeI32(state), arg0);
return EmitNative<spv::OpConvertSToF, type>(state, signed_value);
}
inline constexpr auto EmitConvertF32S32 = EmitConvertSigned32ToFloat<IR::Type::F32>;
inline constexpr auto EmitConvertF64S32 = EmitConvertSigned32ToFloat<IR::Type::F64>;
uint32_t EmitConvertF32F64(EmitterState& state, uint32_t arg0);
uint32_t EmitConvertF64F32(EmitterState& state, uint32_t arg0);
EMIT_NATIVE(ConvertF32U32, OpConvertUToF, F32, uint32_t)
EMIT_NATIVE(ConvertF64U32, OpConvertUToF, F64, uint32_t)
EMIT_NATIVE(CompositeConstructU64, OpCompositeConstruct, U64, uint32_t, uint32_t)
EMIT_NATIVE(CompositeConstructU32x2, OpCompositeConstruct, U32x2, uint32_t, uint32_t)
EMIT_NATIVE(CompositeConstructU32x3, OpCompositeConstruct, U32x3, uint32_t, uint32_t, uint32_t)
@@ -103,23 +116,43 @@ EMIT_NATIVE(LogicalOr, OpLogicalOr, U1, uint32_t, uint32_t)
EMIT_NATIVE(LogicalAnd, OpLogicalAnd, U1, uint32_t, uint32_t)
EMIT_NATIVE(LogicalXor, OpLogicalNotEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(LogicalNot, OpLogicalNot, U1, uint32_t)
EMIT_NATIVE(FPOrdEqual32, OpFOrdEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordEqual32, OpFUnordEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdNotEqual32, OpFOrdNotEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordNotEqual32, OpFUnordNotEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdLessThan32, OpFOrdLessThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordLessThan32, OpFUnordLessThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdGreaterThan32, OpFOrdGreaterThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordGreaterThan32, OpFUnordGreaterThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdLessThanEqual32, OpFOrdLessThanEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordLessThanEqual32, OpFUnordLessThanEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdGreaterThanEqual32, OpFOrdGreaterThanEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordGreaterThanEqual32, OpFUnordGreaterThanEqual, U1, uint32_t, uint32_t)
template <spv::Op opcode>
uint32_t EmitFloatCompare32(ValueEmitContext& ctx, const IR::Inst& inst) {
// SPIR-V's DenormFlushToZero mode does not require comparison operands to flush.
const bool flush = inst.Flags<IR::FPCompareFlags>().flush_input_denorms;
const auto operand = [&](size_t index) {
const auto value = inst.Arg(index);
if (flush && value.IsImmediate()) {
const auto bits = std::bit_cast<uint32_t>(value.F32Value());
return ConstantF32(ctx.state, (bits & 0x7fffffffu) < 0x00800000u
? bits & 0x80000000u : bits);
}
const auto id = ctx.Arg(inst, index);
return flush ? EmitFlushF32DenormToSignedZero(ctx.state, id) : id;
};
return EmitNative<opcode, IR::Type::U1>(ctx.state, operand(0), operand(1));
}
inline constexpr auto EmitFPOrdEqual32 = EmitFloatCompare32<spv::OpFOrdEqual>;
inline constexpr auto EmitFPUnordEqual32 = EmitFloatCompare32<spv::OpFUnordEqual>;
inline constexpr auto EmitFPOrdNotEqual32 = EmitFloatCompare32<spv::OpFOrdNotEqual>;
inline constexpr auto EmitFPUnordNotEqual32 = EmitFloatCompare32<spv::OpFUnordNotEqual>;
inline constexpr auto EmitFPOrdLessThan32 = EmitFloatCompare32<spv::OpFOrdLessThan>;
inline constexpr auto EmitFPUnordLessThan32 = EmitFloatCompare32<spv::OpFUnordLessThan>;
inline constexpr auto EmitFPOrdGreaterThan32 = EmitFloatCompare32<spv::OpFOrdGreaterThan>;
inline constexpr auto EmitFPUnordGreaterThan32 = EmitFloatCompare32<spv::OpFUnordGreaterThan>;
inline constexpr auto EmitFPOrdLessThanEqual32 = EmitFloatCompare32<spv::OpFOrdLessThanEqual>;
inline constexpr auto EmitFPUnordLessThanEqual32 = EmitFloatCompare32<spv::OpFUnordLessThanEqual>;
inline constexpr auto EmitFPOrdGreaterThanEqual32 = EmitFloatCompare32<spv::OpFOrdGreaterThanEqual>;
inline constexpr auto EmitFPUnordGreaterThanEqual32 = EmitFloatCompare32<spv::OpFUnordGreaterThanEqual>;
uint32_t EmitFPIsNan32(EmitterState& state, uint32_t arg0);
inline constexpr auto EmitFPCmpClass32 = EmitClassMaskF32;
EMIT_NATIVE(FPAdd32, OpFAdd, F32, uint32_t, uint32_t)
EMIT_NATIVE(FPSub32, OpFSub, F32, uint32_t, uint32_t)
EMIT_NATIVE(FPMul32, OpFMul, F32, uint32_t, uint32_t)
EMIT_NATIVE(FPMul64, OpFMul, F64, uint32_t, uint32_t)
inline constexpr auto EmitFPFma64 =
EmitGlsl<GLSLstd450Fma, IR::Type::F64, uint32_t, uint32_t, uint32_t>;
uint32_t EmitFPRecip64(EmitterState& state, uint32_t arg0);
inline constexpr auto EmitFPFma32 =
EmitGlsl<GLSLstd450Fma, IR::Type::F32, uint32_t, uint32_t, uint32_t>;
uint32_t EmitFPMin32(EmitterState& state, uint32_t arg0, uint32_t arg1);
@@ -169,6 +202,7 @@ uint32_t EmitDppUpdateU32(ValueEmitContext& ctx, const IR::Inst& in
uint32_t EmitWqmU64(EmitterState& state, uint32_t value);
uint32_t EmitLaneId(EmitterState& state);
uint32_t EmitBallot(ValueEmitContext& ctx, IR::Value predicate);
uint32_t EmitConditionRef(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitReadFirstLane(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitReadLane(ValueEmitContext& ctx, const IR::Inst& inst);
uint32_t EmitWriteLane(ValueEmitContext& ctx, const IR::Inst& inst);
@@ -271,6 +305,8 @@ inline constexpr auto EmitImageAtomicUMax32 = EmitImage;
inline constexpr auto EmitImageAtomicAnd32 = EmitImage;
inline constexpr auto EmitImageAtomicOr32 = EmitImage;
inline constexpr auto EmitImageAtomicXor32 = EmitImage;
inline constexpr auto EmitImageAtomicFMin32 = EmitImage;
inline constexpr auto EmitImageAtomicFMax32 = EmitImage;
void EmitUnreachable(ValueEmitContext& ctx, const IR::Inst& inst);
inline constexpr auto EmitPhi = EmitUnreachable;
inline constexpr auto EmitTessellationBase = EmitUnreachable;
@@ -68,6 +68,7 @@ struct SpirvRequirements {
bool pixel_valid_mask = false;
bool buffer_int64_atomics = false;
bool coherent_buffers = false;
bool float64 = false;
};
SpirvRequirements AnalyzeProgramRequirements(const IR::Program& program);
@@ -139,6 +140,7 @@ uint32_t TypeU32Pair(EmitterState& state);
uint32_t TypeI32(EmitterState& state);
uint32_t TypeI32Pair(EmitterState& state);
uint32_t TypeF32(EmitterState& state);
uint32_t TypeF64(EmitterState& state);
uint32_t TypeU32Vector(EmitterState& state, uint32_t components);
uint32_t TypeU32Composite(EmitterState& state, uint32_t components);
@@ -167,6 +169,10 @@ template <spv::Op opcode, IR::Type type, typename... Args>
uint32_t EmitNative(EmitterState& state, Args... args) {
const auto result = state.builder.AllocateId();
state.builder.AddFunction(opcode, TypeId(state, type), result, args...);
if constexpr (type == IR::Type::F64 &&
(opcode == spv::OpFMul || opcode == spv::OpFDiv || opcode == spv::OpExtInst)) {
state.builder.AddAnnotation(spv::OpDecorate, result, spv::DecorationNoContraction);
}
return result;
}
@@ -146,7 +146,12 @@ uint32_t GuestAddress(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Mem
ctx.Fail(inst, "has no address base pair");
return ConstantDeviceAddress(state, 0);
}
const auto base = DeviceAddressFromWords(state, ctx.Arg(*handle, 0), ctx.Arg(*handle, 1));
auto base_low = ctx.Arg(*handle, 0);
if (mem.kind == IR::ResourceKind::ScalarAddress) {
base_low = Binary(state, spv::OpBitwiseAnd, TypeU32(state), base_low,
ConstantU32(state, ~3u));
}
const auto base = DeviceAddressFromWords(state, base_low, ctx.Arg(*handle, 1));
address = Binary(state, spv::OpIAdd, TypeScalarU64(state), base,
Unary(state, spv::OpUConvert, TypeScalarU64(state), low));
}
@@ -706,26 +711,66 @@ uint32_t FormattedOutOfBoundsValue(ValueEmitContext& ctx, const IR::MemoryInfo&
return ConstructU32Composite(ctx.state, components, values);
}
void StoreFormattedInBounds(ValueEmitContext& ctx, const IR::MemoryInfo& mem,
const PreparedFormattedMemory& plan, uint32_t component,
uint32_t data) {
if (component >= plan.info.component_count) return;
const auto bits = plan.info.component_bits[component];
if (bits == 16u && (plan.info.type == Format::ComponentType::Snorm ||
plan.info.type == Format::ComponentType::Float)) {
uint32_t EncodeFormattedStoreComponent(ValueEmitContext& ctx,
const Format::BufferFormatInfo& info,
uint32_t component, uint32_t data) {
const auto bits = info.component_bits[component];
if (info.type == Format::ComponentType::Unorm) {
const auto value = EmitBitCastF32U32(ctx.state, data);
const auto clamped = EmitFPMin32(
ctx.state, EmitFPMax32(ctx.state, value, ConstantF32Value(ctx.state, 0.0f)),
ConstantF32Value(ctx.state, 1.0f));
const auto scaled = EmitFPMul32(
ctx.state, clamped, ConstantF32Value(ctx.state, float((1u << bits) - 1u)));
return Unary(ctx.state, spv::OpConvertFToU, TypeU32(ctx.state),
EmitFPRoundEven32(ctx.state, scaled));
}
if (bits == 16u && (info.type == Format::ComponentType::Snorm ||
info.type == Format::ComponentType::Float)) {
const auto value = EmitBitCastF32U32(ctx.state, data);
const auto pair = EmitCompositeConstructF32x2(ctx.state, value,
ConstantF32Value(ctx.state, 0.0f));
data = plan.info.type == Format::ComponentType::Float
? EmitPackHalf2x16(ctx.state, pair)
: EmitPackSnorm2x16(ctx.state, pair);
}
if (bits == 8u || bits == 16u) {
StoreSubwordInBounds(ctx, mem, plan.resource, plan.addresses[component],
plan.indices[component], bits, data);
} else {
StoreWordInBounds(ctx, plan.resource, plan.indices[component], data);
return info.type == Format::ComponentType::Float ? EmitPackHalf2x16(ctx.state, pair)
: EmitPackSnorm2x16(ctx.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,
uint32_t components) {
// RDNA2 formatted stores transfer the entire format, zero-filling missing VGPRs.
const auto plan = PrepareFormattedMemory(ctx, inst, mem, resource, info, info.component_count,
FormattedAccess::Store);
EmitIfCondition(ctx.state, plan.in_bounds, [&]() {
auto packed = ConstantU32(ctx.state, 0);
for (uint32_t component = 0; component < info.component_count; component++) {
auto value = ConstantU32(ctx.state, 0);
if (component < components) {
value = data;
if (components != 1u) {
value = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state),
value, data, component);
}
}
value = EncodeFormattedStoreComponent(ctx, info, component, value);
const auto bits = info.component_bits[component];
if (info.packed_bitfield) {
packed = EmitBitFieldInsert(
ctx.state, packed, value,
ConstantU32(ctx.state, info.component_bit_offset[component]),
ConstantU32(ctx.state, bits));
} else if (bits == 8u || bits == 16u) {
StoreSubwordInBounds(ctx, mem, resource, plan.addresses[component],
plan.indices[component], bits, value);
} else {
StoreWordInBounds(ctx, resource, plan.indices[component], value);
}
}
if (info.packed_bitfield) StoreWordInBounds(ctx, resource, plan.indices[0], packed);
});
}
void FormattedStore(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem) {
@@ -737,11 +782,7 @@ void FormattedStore(ValueEmitContext& ctx, const IR::Inst& inst, const IR::Memor
StoreWordPrepared(ctx, inst, RebaseRawComponent(mem, 0u), resource, data);
return;
}
const auto plan = PrepareFormattedMemory(ctx, inst, mem, resource, info, 1u,
FormattedAccess::Store);
EmitIfCondition(ctx.state, plan.in_bounds, [&]() {
StoreFormattedInBounds(ctx, mem, plan, 0u, data);
});
StoreFormattedPrepared(ctx, inst, mem, resource, info, data, 1u);
});
}
@@ -859,16 +900,7 @@ void StoreWideBuffer(ValueEmitContext& ctx, const IR::Inst& inst, uint32_t compo
const auto info = Format::GetFormatInfo(
mem.formatted ? BufferFormat(ctx, mem) : Prospero::BufferFormat::kInvalid);
if (info.type != Format::ComponentType::Unknown) {
const auto plan = PrepareFormattedMemory(ctx, inst, mem, resource, info, components,
FormattedAccess::Store);
EmitIfCondition(state, plan.in_bounds, [&]() {
for (uint32_t component = 0; component < components; component++) {
const auto data = state.builder.AllocateId();
state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(state), data,
composite, component);
StoreFormattedInBounds(ctx, mem, plan, component, data);
}
});
StoreFormattedPrepared(ctx, inst, mem, resource, info, composite, components);
return;
}
for (uint32_t component = 0; component < components; component++) {
@@ -943,7 +975,14 @@ void DefineGetBdaPointer(EmitterState& state) {
state.builder.AddFunction(spv::OpFunctionParameter, type, address);
EmitLabel(state, entry_label);
const auto page64 = Binary(state, spv::OpShiftRightLogical, type, address,
const auto extended = Binary(state, spv::OpUGreaterThanEqual, TypeBool(state), address,
ConstantDeviceAddress(state, LibKernel::Memory::kExtendedMemoryBase));
const auto packed = Select(state, type, extended,
Binary(state, spv::OpISub, type, address,
ConstantDeviceAddress(state,
LibKernel::Memory::kExtendedMemoryBase - LOWER_ADDRESS_SIZE)),
address);
const auto page64 = Binary(state, spv::OpShiftRightLogical, type, packed,
ConstantDeviceAddress(state, BufferCache::CACHING_PAGEBITS));
const auto page = Unary(state, spv::OpUConvert, TypeU32(state), page64);
const auto entry_pointer = state.builder.AllocateId();
@@ -1072,16 +1111,27 @@ uint32_t EmitAppendConsume(ValueEmitContext& ctx, const IR::Inst& inst) {
if (ctx.half == 1) {
return ctx.other_half->Def(IR::Value(const_cast<IR::Inst*>(&inst)));
}
const auto m0 = ctx.Arg(inst, 0);
const auto base =
Binary(state, spv::OpShiftRightLogical, TypeU32(state), m0, ConstantU32(state, 16));
const auto size =
Binary(state, spv::OpBitwiseAnd, TypeU32(state), m0, ConstantU32(state, 0xffffu));
const auto address = Binary(state, spv::OpIAdd, TypeU32(state), base,
ConstantU32(state, ctx.Memory(inst).offset));
const auto mem = ctx.Memory(inst);
const auto offset = mem.offset & 0xfffcu;
auto address = ConstantU32(state, offset);
uint32_t region_bounds = 0;
if (mem.kind == IR::ResourceKind::Gds) {
const auto m0 = ctx.Arg(inst, 0);
const auto base =
Binary(state, spv::OpShiftRightLogical, TypeU32(state), m0, ConstantU32(state, 16));
const auto size =
Binary(state, spv::OpBitwiseAnd, TypeU32(state), m0, ConstantU32(state, 0xffffu));
address = Binary(state, spv::OpIAdd, TypeU32(state), base, address);
// The entire M0 region must fit the PS5's 48 KiB GDS partition.
region_bounds = AndCondition(
state, Binary(state, spv::OpULessThan, TypeBool(state),
ConstantU32(state, offset + 3u), size),
Binary(state, spv::OpULessThanEqual, TypeBool(state),
Binary(state, spv::OpIAdd, TypeU32(state), base, size),
ConstantU32(state, 0xc000u)));
}
const auto raw_index =
Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2));
const auto mem = ctx.Memory(inst);
const auto access = PrepareMemoryResourceAccess(state, mem);
const auto index = EmitMemoryElementIndex(state, access, raw_index);
const auto exec = ctx.Arg(inst, 1);
@@ -1100,19 +1150,17 @@ uint32_t EmitAppendConsume(ValueEmitContext& ctx, const IR::Inst& inst) {
: first;
const auto is_first = Binary(state, spv::OpIEqual, TypeBool(state),
EmitSubgroupLocalInvocationId(state), source_lane);
const auto storage_bounds = EmitMemoryElementInBounds(state, access, index);
const auto m0_bounds =
mem.kind == IR::ResourceKind::Gds
? Binary(state, spv::OpINotEqual, TypeBool(state), size, ConstantU32(state, 0))
: Binary(state, spv::OpULessThan, TypeBool(state),
ConstantU32(state, ctx.Memory(inst).offset + 3u), size);
auto bounds = EmitMemoryElementInBounds(state, access, index);
if (mem.kind == IR::ResourceKind::Gds) {
bounds = AndCondition(state, bounds, region_bounds);
}
const auto condition = AndCondition(
state, is_first,
AndCondition(state,
state.lane_count == 2 ? Binary(state, spv::OpINotEqual, TypeBool(state), count,
ConstantU32(state, 0))
: exec,
AndCondition(state, storage_bounds, m0_bounds)));
bounds));
const auto atomic = EmitValueOrZeroIfCondition(state, condition, [&]() {
const auto value = state.builder.AllocateId();
state.builder.AddFunction(append ? spv::OpAtomicIAdd : spv::OpAtomicISub, TypeU32(state),
@@ -1146,10 +1194,11 @@ void EmitReadConstBuffer(ValueEmitContext& ctx, const IR::Inst& inst) {
if (mem.planning_only) return;
auto& state = ctx.state;
mem.kind = IR::ResourceKind::ScalarBuffer;
const auto address = Binary(state, spv::OpIAdd, TypeU32(state), ctx.Arg(inst, 1),
ConstantU32(state, mem.offset));
const auto index =
Binary(state, spv::OpShiftRightLogical, TypeU32(state), address, ConstantU32(state, 2));
auto index = Binary(state, spv::OpShiftRightLogical, TypeU32(state), ctx.Arg(inst, 1),
ConstantU32(state, 2));
if (mem.offset >= 4u) {
index = Binary(state, spv::OpIAdd, TypeU32(state), index, ConstantU32(state, mem.offset >> 2u));
}
const auto access = PrepareMemoryResourceAccess(state, mem);
const auto element = EmitMemoryElementIndex(state, access, index);
const auto condition = EmitMemoryElementInBounds(state, access, element);
@@ -1171,6 +1220,8 @@ void EmitLoadMemory(ValueEmitContext& ctx, const IR::Inst& inst) {
value = LoadWideBuffer(ctx, inst, buffer_components);
else if (shared_components > 1u)
value = LoadWideShared(ctx, inst, shared_components);
else if (mem.kind == IR::ResourceKind::ScalarAddress)
value = LoadBdaDword(ctx, GuestAddress(ctx, inst, mem));
else if (address_info.access == IR::AddressAccess::Read &&
mem.kind != IR::ResourceKind::Scratch)
value = LoadBda(ctx, GuestAddress(ctx, inst, mem), ctx.Arg(inst, inst.NumArgs() - 1),
@@ -47,6 +47,10 @@ uint32_t TypeF32(EmitterState& state) {
return state.builder.Type(spv::OpTypeFloat, 32);
}
uint32_t TypeF64(EmitterState& state) {
return state.builder.Type(spv::OpTypeFloat, 64);
}
uint32_t TypeU32Vector(EmitterState& state, uint32_t components) {
return state.builder.Type(spv::OpTypeVector, TypeU32(state), components);
}
@@ -679,6 +683,19 @@ void DefineModule(EmitterState& state) {
// contract prevents host compilers from treating synthesized IEEE values as finite.
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeSignedZeroInfNanPreserve,
32u);
if (state.requirements.float64) {
EXIT_NOT_IMPLEMENTED(state.program.stage == ShaderType::Compute &&
state.input_info.compute->float_mode != 0xc0);
// MODE=0xc0 uses round-to-nearest-even and preserves FP64 input/output denormals.
state.builder.RequireCapability(spv::CapabilityFloat64);
state.builder.RequireCapability(spv::CapabilityRoundingModeRTE);
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeSignedZeroInfNanPreserve,
64u);
// FP64 denormal preservation is temporarily disabled.
// state.builder.RequireCapability(spv::CapabilityDenormPreserve);
// state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeDenormPreserve, 64u);
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeRoundingModeRTE, 32u);
}
if (const auto* cs = ShaderWorkgroupInput(state.program.stage, state.input_info)) {
uint32_t local_x = state.requirements.compute_derivatives ? 2u : 1u;
uint32_t local_y = state.requirements.compute_derivatives ? 2u : 1u;
@@ -97,30 +97,6 @@ void EmitReturn(ValueEmitContext& ctx) {
ctx.state.builder.AddFunction(spv::OpReturn);
}
uint32_t BranchCondition(ValueEmitContext& ctx, const IR::BlockInfo& info) {
// Scalar-instruction conditions already test the full wave's raw register values.
if (ctx.other_half == nullptr ||
info.terminator.condition == CFG::BranchCondition::ScalarInstruction ||
info.terminator.condition == CFG::BranchCondition::GotoVariable) {
return ctx.Def(info.condition);
}
const auto ballot = ctx.Ballot(info.condition);
const auto low = ctx.state.builder.AllocateId();
const auto high = ctx.state.builder.AllocateId();
const auto result = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), low, ballot, 0);
ctx.state.builder.AddFunction(spv::OpCompositeExtract, TypeU32(ctx.state), high, ballot, 1);
const auto kind = info.terminator.condition;
const bool zero = kind == CFG::BranchCondition::ExecZero ||
kind == CFG::BranchCondition::VccZero ||
kind == CFG::BranchCondition::SccZero;
const auto combined =
EmitBinaryU32(ctx.state, zero ? spv::OpBitwiseAnd : spv::OpBitwiseOr, low, high);
ctx.state.builder.AddFunction(zero ? spv::OpIEqual : spv::OpINotEqual, TypeBool(ctx.state),
result, combined, ConstantU32(ctx.state, zero ? ~0u : 0u));
return result;
}
void EmitStructuredTerminator(ValueEmitContext& ctx, const IR::Block* block,
const IR::BlockInfo& info) {
const auto& program = ctx.state.program;
@@ -160,7 +136,7 @@ void EmitStructuredTerminator(ValueEmitContext& ctx, const IR::Block* block,
EmitReturn(ctx);
return;
}
const auto condition = BranchCondition(ctx, info);
const auto condition = ctx.Def(info.condition);
emit_merge();
ctx.state.builder.AddFunction(spv::OpBranchConditional, condition,
ctx.Label(true_block), ctx.Label(false_block));
@@ -193,7 +169,7 @@ uint32_t EmitDispatcherNextPc(ValueEmitContext& ctx, const DispatcherFunctionSta
EmitDispatcherTarget(ctx, dispatcher, block, term.false_block);
const auto selected = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(
spv::OpSelect, TypeU32(ctx.state), selected, BranchCondition(ctx, info),
spv::OpSelect, TypeU32(ctx.state), selected, ctx.Def(info.condition),
ConstantU32(ctx.state, term.true_block), ConstantU32(ctx.state, term.false_block));
return selected;
}
@@ -440,6 +416,7 @@ uint32_t TypeId(EmitterState& state, IR::Type type) {
case IR::Type::U64: return TypeU64(state);
case IR::Type::U32x2: return TypeU32Pair(state);
case IR::Type::F32: return TypeF32(state);
case IR::Type::F64: return TypeF64(state);
case IR::Type::U32x3: return TypeU32Vector(state, 3);
case IR::Type::U32x4: return TypeU32Vector(state, 4);
case IR::Type::F32x2: return TypeF32Vector(state, 2);
File diff suppressed because it is too large. Load diff
@@ -18,10 +18,17 @@ enum class BranchCondition {
ExecZero,
ExecNonZero,
ScalarInstruction,
GotoVariable,
Expression,
Unknown
};
struct ConditionExpression {
enum class Op { Constant, Variable, Native, Not, Or };
Op op;
uint32_t lhs;
uint32_t rhs = UINT32_MAX;
};
enum class TerminatorKind { Branch, ConditionalBranch, IndirectBranch, Return, Unsupported };
enum class FailureKind {
@@ -48,8 +55,7 @@ struct Terminator {
std::vector<uint32_t> indirect_targets;
std::vector<uint32_t> indirect_selector_values;
std::vector<uint32_t> indirect_selector_targets;
uint32_t goto_variable = UINT32_MAX;
int32_t goto_value = -1;
uint32_t expression = UINT32_MAX;
bool loop_header = false;
};
@@ -59,10 +65,11 @@ struct BasicBlock {
uint32_t end_pc = 0;
uint32_t inst_begin = 0;
uint32_t inst_end = 0;
struct Assignment { uint32_t variable; uint32_t expression; };
std::vector<Assignment> assignments;
std::vector<uint32_t> predecessors;
std::vector<uint32_t> successors;
std::vector<uint32_t> dominators;
std::vector<uint32_t> post_dominators;
Terminator terminator;
};
@@ -73,12 +80,9 @@ struct BackEdge {
};
struct NaturalLoop {
uint32_t header = UINT32_MAX;
uint32_t latch = UINT32_MAX;
uint32_t merge = UINT32_MAX;
uint32_t continue_block = UINT32_MAX;
uint32_t header = UINT32_MAX;
uint32_t latch = UINT32_MAX;
std::vector<uint32_t> body_blocks;
std::vector<uint32_t> exit_blocks;
};
struct StronglyConnectedComponent {
@@ -89,6 +93,7 @@ struct StronglyConnectedComponent {
struct Graph {
std::vector<BasicBlock> blocks;
std::vector<ConditionExpression> expressions;
std::vector<BackEdge> back_edges;
std::vector<NaturalLoop> natural_loops;
std::vector<StronglyConnectedComponent> components;
@@ -105,14 +110,12 @@ struct Graph {
const BasicBlock* FindBlockByPc(uint32_t pc) const;
BasicBlock* FindBlockByPc(uint32_t pc);
bool Dominates(uint32_t dominator, uint32_t block) const;
bool PostDominates(uint32_t post_dominator, uint32_t block) const;
uint32_t FindNearestCommonPostDominator(uint32_t block_a, uint32_t block_b) const;
};
Graph BuildGraph(const Decoder::Program& program);
// Commits structured control flow on success; preserves the original graph with
// failure diagnostics on failure. failure_block is an original block ID or UINT32_MAX.
bool Structurize(Graph& graph);
// Returns structured control flow or failure diagnostics without changing the native graph.
// On failure, failure_block is an original block ID or UINT32_MAX.
Graph Structurize(const Graph& graph);
std::string BranchConditionToString(BranchCondition condition);
std::string FailureKindToString(FailureKind kind);
std::string GraphToString(const Graph& graph);
@@ -198,6 +198,8 @@ constexpr Detail::OpcodeMap MIMG_ATOMIC_OPCODE_LIST[] = {
{0x18u, Opcode::IMAGE_ATOMIC_AND},
{0x19u, Opcode::IMAGE_ATOMIC_OR},
{0x1au, Opcode::IMAGE_ATOMIC_XOR},
{0x1eu, Opcode::IMAGE_ATOMIC_FMIN},
{0x1fu, Opcode::IMAGE_ATOMIC_FMAX},
};
constexpr auto MIMG_SAMPLE_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_SAMPLE_OPCODE_LIST);
@@ -579,6 +579,8 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::IMAGE_ATOMIC_AND:
case Opcode::IMAGE_ATOMIC_OR:
case Opcode::IMAGE_ATOMIC_XOR:
case Opcode::IMAGE_ATOMIC_FMIN:
case Opcode::IMAGE_ATOMIC_FMAX:
case Opcode::IMAGE_LOAD:
case Opcode::IMAGE_LOAD_MIP:
case Opcode::IMAGE_GET_RESINFO:
@@ -154,6 +154,10 @@ enum class Opcode {
V_CUBEMA_F32,
V_CNDMASK_B32,
V_DOT2C_F32_F16,
V_CVT_F64_I32,
V_CVT_F32_F64,
V_CVT_F64_F32,
V_CVT_F64_U32,
V_CVT_F32_I32,
V_CVT_F32_U32,
V_CVT_U32_F32,
@@ -173,6 +177,7 @@ enum class Opcode {
V_CVT_F32_UBYTE1,
V_CVT_F32_UBYTE2,
V_CVT_F32_UBYTE3,
V_RCP_F64,
V_RCP_F32,
V_RCP_IFLAG_F32,
V_FRACT_F32,
@@ -234,6 +239,8 @@ enum class Opcode {
V_MAD_I32_I24,
V_MAD_U32_U24,
V_MAD_U64_U32,
V_FMA_F64,
V_MUL_F64,
V_FMA_F32,
V_FMA_F16,
V_PACK_B32_F16,
@@ -571,6 +578,8 @@ enum class Opcode {
IMAGE_ATOMIC_AND,
IMAGE_ATOMIC_OR,
IMAGE_ATOMIC_XOR,
IMAGE_ATOMIC_FMIN,
IMAGE_ATOMIC_FMAX,
IMAGE_SAMPLE,
IMAGE_GATHER4_L,
IMAGE_GATHER4_LZ,
@@ -87,6 +87,11 @@ constexpr OpcodeMap VOP1_OPCODE_LIST[] = {
{0x00u, Opcode::V_NOP},
{0x01u, Opcode::V_MOV_B32},
{0x02u, Opcode::V_READFIRSTLANE_B32},
{0x04u, Opcode::V_CVT_F64_I32},
{0x0fu, Opcode::V_CVT_F32_F64},
{0x10u, Opcode::V_CVT_F64_F32},
{0x16u, Opcode::V_CVT_F64_U32},
{0x2fu, Opcode::V_RCP_F64},
{0x05u, Opcode::V_CVT_F32_I32},
{0x06u, Opcode::V_CVT_F32_U32},
{0x07u, Opcode::V_CVT_U32_F32},
@@ -146,6 +151,11 @@ constexpr OpcodeMap VOP3_ENCODED_VOP1_OPCODE_LIST[] = {
{0x00u, Opcode::V_NOP},
{0x01u, Opcode::V_MOV_B32},
{0x02u, Opcode::V_READFIRSTLANE_B32},
{0x04u, Opcode::V_CVT_F64_I32},
{0x0fu, Opcode::V_CVT_F32_F64},
{0x10u, Opcode::V_CVT_F64_F32},
{0x16u, Opcode::V_CVT_F64_U32},
{0x2fu, Opcode::V_RCP_F64},
{0x05u, Opcode::V_CVT_F32_I32},
{0x06u, Opcode::V_CVT_F32_U32},
{0x07u, Opcode::V_CVT_U32_F32},
@@ -267,6 +277,8 @@ constexpr OpcodeMap VOP3_OPCODE_LIST[] = {
{0x146u, Opcode::V_CUBETC_F32},
{0x147u, Opcode::V_CUBEMA_F32},
{0x14bu, Opcode::V_FMA_F32},
{0x14cu, Opcode::V_FMA_F64},
{0x165u, Opcode::V_MUL_F64},
{0x148u, Opcode::V_BFE_U32},
{0x149u, Opcode::V_BFE_I32},
{0x14au, Opcode::V_BFI_B32},
@@ -452,6 +464,9 @@ bool IsVopcCompareExec(Opcode opcode);
bool IsVop1FloatSourceOpcode(Opcode opcode) {
switch (opcode) {
case Opcode::V_CVT_F32_F64:
case Opcode::V_CVT_F64_F32:
case Opcode::V_RCP_F64:
case Opcode::V_MOV_B32:
case Opcode::V_CVT_F32_F16:
case Opcode::V_CVT_U32_F32:
@@ -522,9 +537,12 @@ constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
{Opcode::V_CVT_F32_U32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_I32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_UBYTE0, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
// The source field extract and the destination insert are independent (Translate.cpp), so a
// partial destination may combine with any source selector.
{Opcode::V_NOT_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(),
SdwaSelFull(), false},
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_FFBL_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_FFBH_U32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_F16, SdwaSelWords() | SdwaSelFull(), 0, 0, true},
{Opcode::V_CVT_F16_F32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), true},
@@ -707,6 +725,12 @@ void ApplyDppModifier(Operand& operand, uint32_t modifier, uint32_t encoding) {
void DecodeVop1Dpp(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
uint32_t opcode, uint32_t vdst, Instruction& inst) {
if (inst.opcode == Opcode::V_CVT_F64_I32 || inst.opcode == Opcode::V_CVT_F32_F64 ||
inst.opcode == Opcode::V_CVT_F64_F32 || inst.opcode == Opcode::V_CVT_F64_U32 ||
inst.opcode == Opcode::V_RCP_F64) {
SetUnsupported(inst, Family::VOP1, opcode, "FP64 instructions do not support DPP");
return;
}
const auto modifier = code[word_index + 1u];
const auto src0 = modifier & 0xffu;
SetRawWords(inst, code, word_index, 2);
@@ -756,6 +780,7 @@ bool IsVop2FloatOpcode(Opcode opcode) {
bool IsVop1FloatResultOpcode(Opcode opcode) {
switch (opcode) {
case Opcode::V_CVT_F32_F64:
case Opcode::V_CVT_F32_I32:
case Opcode::V_CVT_F32_U32:
case Opcode::V_CVT_F32_F16:
@@ -1176,6 +1201,7 @@ void DecodeVopcDpp(uint32_t pc, std::span<const uint32_t> code, uint32_t word_in
uint32_t NativeVop3SourceCount(Opcode opcode) {
switch (opcode) {
case Opcode::V_MUL_F64:
case Opcode::V_MUL_LO_U32:
case Opcode::V_MUL_HI_U32:
case Opcode::V_MUL_LO_I32:
@@ -1326,6 +1352,8 @@ bool SupportsNativeVop3SourceModifiers(Opcode opcode) {
case Opcode::V_MAX_F32:
case Opcode::V_MAC_F32:
case Opcode::V_MAD_F32:
case Opcode::V_MUL_F64:
case Opcode::V_FMA_F64:
case Opcode::V_FMA_F32:
case Opcode::V_PACK_B32_F16:
case Opcode::V_CUBEID_F32:
@@ -1358,6 +1386,7 @@ bool SupportsNativeVop3ResultModifiers(Opcode opcode) {
case Opcode::V_MAD_F32:
case Opcode::V_FMA_F32:
case Opcode::V_FMA_F16:
case Opcode::V_CUBEID_F32:
case Opcode::V_CVT_PKRTZ_F16_F32:
case Opcode::V_LDEXP_F32:
case Opcode::V_MIN3_F32:
@@ -44,7 +44,8 @@ void Translator::EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpc
half ? IR::Value(ReadF16AsF32(inst.src0)) : ReadOperand(inst.src0, IR::Type::F32);
const auto rhs =
half ? IR::Value(ReadF16AsF32(inst.src1)) : ReadOperand(inst.src1, IR::Type::F32);
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), false, cmpx);
const IR::FPCompareFlags flags{.flush_input_denorms = !half && flush_f32_inputs};
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs}, flags)), false, cmpx);
}
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx) {
@@ -223,7 +223,7 @@ Decoder::Operand MemorySourceAt(const Decoder::Instruction& decoded, uint32_t in
const bool store_or_atomic = decoded.opcode == Decoder::Opcode::IMAGE_STORE ||
decoded.opcode == Decoder::Opcode::IMAGE_STORE_MIP ||
(decoded.opcode >= Decoder::Opcode::IMAGE_ATOMIC_SWAP &&
decoded.opcode <= Decoder::Opcode::IMAGE_ATOMIC_XOR);
decoded.opcode <= Decoder::Opcode::IMAGE_ATOMIC_FMAX);
if (store_or_atomic) {
return index == 0u ? decoded.dst : decoded.src0;
}
@@ -235,6 +235,7 @@ Decoder::Operand MemorySourceAt(const Decoder::Instruction& decoded, uint32_t in
return index == 0u ? decoded.src0 : MakeImmediate(decoded.offset & 0xffffu);
case Decoder::Opcode::DS_CONSUME:
case Decoder::Opcode::DS_APPEND:
return decoded.gds ? MakeM0Operand() : MakeImmediate(0);
case Decoder::Opcode::DS_READ_ADDTID_B32: return MakeM0Operand();
case Decoder::Opcode::DS_WRITE_ADDTID_B32:
return index == 0u ? decoded.src1 : MakeM0Operand();
@@ -845,8 +846,7 @@ void Translator::DS_MINMAX_F32(const Decoder::Instruction& inst, IR::ValueOpcode
void Translator::DS_APPEND_CONSUME(const Decoder::Instruction& inst, IR::ValueOpcode opcode) {
const auto memory = MemoryInfoFromDecoded(inst);
WriteOperand(inst.dst, ir.Emit(opcode,
{ReadU32(MemorySourceAt(inst, 0)), ir.GetExec(), ir.GetExecLo(),
ir.GetExecHi()},
{ReadU32(MemorySourceAt(inst, 0)), ir.GetExec()},
AddMemoryInfo(memory, inst.pc)));
}
@@ -1026,6 +1026,10 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicOr32);
case Decoder::Opcode::IMAGE_ATOMIC_XOR:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicXor32);
case Decoder::Opcode::IMAGE_ATOMIC_FMIN:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicFMin32);
case Decoder::Opcode::IMAGE_ATOMIC_FMAX:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicFMax32);
case Decoder::Opcode::FLAT_LOAD_UBYTE:
case Decoder::Opcode::FLAT_LOAD_SBYTE:
@@ -4,6 +4,7 @@
#include <algorithm>
#include <array>
#include <bit>
#include <unordered_map>
#include <utility>
@@ -225,9 +226,28 @@ IR::Value Translator::ReadOperand(const Decoder::Operand& operand, IR::Type type
}
return ir.INotEqual(ReadRawU32(operand), IR::U32(IR::Value(0u)));
}
if (type == IR::Type::U64) {
const auto pair = ReadU32Pair(operand);
return ir.ConstructU64(pair[0], pair[1]);
if (type == IR::Type::U64 || type == IR::Type::F64) {
auto pair = ReadU32Pair(operand);
if (type == IR::Type::F64) {
if (operand.kind == Decoder::OperandKind::LiteralConstant) {
pair = {IR::U32(IR::Value(0u)), IR::U32(IR::Value(operand.value))};
} else if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
const auto bits = operand.value == 0x3e22f983u
? 0x3fc45f306dc9c882ull
: std::bit_cast<uint64_t>(static_cast<double>(
std::bit_cast<float>(operand.value)));
pair = {IR::U32(IR::Value(static_cast<uint32_t>(bits))),
IR::U32(IR::Value(static_cast<uint32_t>(bits >> 32u)))};
}
if (operand.absolute) {
pair[1] = ir.BitwiseAnd(pair[1], IR::U32(IR::Value(0x7fffffffu)));
}
if (operand.negate) {
pair[1] = ir.BitwiseXor(pair[1], IR::U32(IR::Value(0x80000000u)));
}
}
const auto bits = ir.ConstructU64(pair[0], pair[1]);
return type == IR::Type::F64 ? ir.Emit(IR::ValueOpcode::BitCastF64U64, {bits}) : IR::Value(bits);
}
auto bits = ApplyBitSourceModifiers(operand, ReadRawU32(operand));
if (TypesOverlap(type, IR::Type::F32) && !TypesOverlap(type, IR::Type::U32)) {
@@ -297,6 +317,7 @@ void Translator::WriteRawU32(const Decoder::Operand& operand, IR::U32 value) {
case Decoder::OperandKind::Sgpr: {
const auto reg = static_cast<IR::ScalarReg>(operand.reg);
ir.SetScalarReg(reg, value);
program.scalar_writes.push_back({current_pc, reg});
ir.SetScalarMaskTag(reg, IR::U1(IR::Value(false)));
if (IR::RegIndex(reg) > 0u) {
ir.SetScalarMaskTag(static_cast<IR::ScalarReg>(IR::RegIndex(reg) - 1u),
@@ -403,6 +424,10 @@ void Translator::WriteOperand(const Decoder::Operand& operand, IR::Value value)
Write16Bits(operand, IR::U32(ir.Emit(IR::ValueOpcode::ConvertU32U16, {bits})));
return;
}
if (type == IR::Type::F64) {
value = ir.Emit(IR::ValueOpcode::BitCastU64F64, {value});
type = IR::Type::U64;
}
if (type == IR::Type::U64) {
WriteU32Pair(operand, {ir.CompositeExtract(value, 0), ir.CompositeExtract(value, 1)});
return;
@@ -675,11 +700,13 @@ std::array<IR::U32, 2> Translator::WriteMask(const Decoder::Operand& operand, IR
IR::U1(IR::Value(false)));
}
ir.SetScalarReg(reg, mask[0]);
program.scalar_writes.push_back({current_pc, reg});
// A wave32 VALU mask destination must not overwrite the neighboring SGPR.
if ((write_64 || program.wave_size == 64u) &&
IR::RegIndex(reg) + 1u < IR::NumScalarRegs) {
const auto high = static_cast<IR::ScalarReg>(IR::RegIndex(reg) + 1u);
ir.SetScalarReg(high, mask[1]);
program.scalar_writes.push_back({current_pc, high});
ir.SetThreadBitScalarReg(high, IR::U1(IR::Value(false)));
ir.SetScalarMaskTag(high, IR::U1(IR::Value(false)));
}
@@ -722,55 +749,61 @@ void Translator::WriteCompareResult(const Decoder::Operand& operand, IR::U1 valu
WriteMask(operand, ir.LogicalAnd(ir.GetExec(), value));
}
void Translator::AddBranchCondition(const CFG::BasicBlock& source, IR::BlockInfo& info) {
if (source.terminator.goto_value >= 0) {
if (source.terminator.goto_variable == UINT32_MAX) {
EXIT("block %u sets an invalid goto variable", source.id);
void Translator::AddBranchCondition(const CFG::Graph& graph, const CFG::BasicBlock& source,
IR::BlockInfo& info) {
const auto native_condition = [&](CFG::BranchCondition kind) -> IR::U1 {
IR::U1 condition;
switch (kind) {
case CFG::BranchCondition::Always: condition = IR::U1(IR::Value(true)); break;
case CFG::BranchCondition::SccZero: condition = ir.LogicalNot(ir.GetScc()); break;
case CFG::BranchCondition::SccNonZero: condition = ir.GetScc(); break;
case CFG::BranchCondition::VccZero: condition = ir.LogicalNot(ir.GetVcc()); break;
case CFG::BranchCondition::VccNonZero: condition = ir.GetVcc(); break;
case CFG::BranchCondition::ExecZero: condition = ir.LogicalNot(ir.GetExec()); break;
case CFG::BranchCondition::ExecNonZero: condition = ir.GetExec(); break;
case CFG::BranchCondition::ScalarInstruction:
EXIT_IF(instruction_branch_condition.IsEmpty());
condition = instruction_branch_condition;
break;
default: EXIT("block %u has an invalid native branch condition", source.id);
}
ir.SetGotoVariable(source.terminator.goto_variable,
IR::U1(IR::Value(source.terminator.goto_value != 0)));
return IR::U1(ir.Emit(IR::ValueOpcode::ConditionRef, {condition}, kind));
};
const auto expression = [&](auto&& self, uint32_t index) -> IR::U1 {
const auto& value = graph.expressions.at(index);
switch (value.op) {
case CFG::ConditionExpression::Op::Constant: return IR::U1(IR::Value(value.lhs != 0));
case CFG::ConditionExpression::Op::Variable: return ir.GetGotoVariable(value.lhs);
case CFG::ConditionExpression::Op::Native:
return native_condition(static_cast<CFG::BranchCondition>(value.lhs));
case CFG::ConditionExpression::Op::Not: return ir.LogicalNot(self(self, value.lhs));
case CFG::ConditionExpression::Op::Or:
return ir.LogicalOr(self(self, value.lhs), self(self, value.rhs));
}
EXIT("invalid CFG condition expression");
};
for (const auto& assignment: source.assignments) {
ir.SetGotoVariable(assignment.variable, expression(expression, assignment.expression));
}
if (source.terminator.kind == CFG::TerminatorKind::IndirectBranch) {
if (source.terminator.indirect_selector_code != UINT32_MAX) {
info.indirect_target = ReadScalarCode(source.terminator.indirect_selector_code);
} else if (source.terminator.indirect_pc_sgpr != UINT32_MAX) {
info.indirect_target =
ir.GetScalarReg(static_cast<IR::ScalarReg>(source.terminator.indirect_pc_sgpr));
const auto& term = source.terminator;
if (term.kind == CFG::TerminatorKind::IndirectBranch) {
if (term.indirect_selector_code != UINT32_MAX) {
info.indirect_target = ReadScalarCode(term.indirect_selector_code);
} else if (term.indirect_pc_sgpr != UINT32_MAX) {
info.indirect_target = ir.GetScalarReg(static_cast<IR::ScalarReg>(term.indirect_pc_sgpr));
} else {
EXIT("block %u has no indirect branch selector", source.id);
}
ir.Emit(IR::ValueOpcode::ReferenceU32, {info.indirect_target});
return;
}
if (source.terminator.kind != CFG::TerminatorKind::ConditionalBranch) {
return;
if (term.kind == CFG::TerminatorKind::ConditionalBranch) {
const auto condition = term.expression != UINT32_MAX
? expression(expression, term.expression)
: native_condition(term.condition);
info.condition = condition;
ir.Emit(IR::ValueOpcode::Reference, {condition});
}
// EXEC and VCC are invocation-local Boolean masks. Branching on that Boolean lets inactive
// invocations leave the region without reconstructing a host-subgroup mask.
IR::U1 condition;
switch (source.terminator.condition) {
case CFG::BranchCondition::Always: condition = IR::U1(IR::Value(true)); break;
case CFG::BranchCondition::SccZero: condition = ir.LogicalNot(ir.GetScc()); break;
case CFG::BranchCondition::SccNonZero: condition = ir.GetScc(); break;
case CFG::BranchCondition::VccZero: condition = ir.LogicalNot(ir.GetVcc()); break;
case CFG::BranchCondition::VccNonZero: condition = ir.GetVcc(); break;
case CFG::BranchCondition::ExecZero: condition = ir.LogicalNot(ir.GetExec()); break;
case CFG::BranchCondition::ExecNonZero: condition = ir.GetExec(); break;
case CFG::BranchCondition::ScalarInstruction:
EXIT_IF(instruction_branch_condition.IsEmpty());
condition = instruction_branch_condition;
break;
case CFG::BranchCondition::GotoVariable:
if (source.terminator.goto_variable == UINT32_MAX) {
EXIT("block %u reads an invalid goto variable", source.id);
}
condition = ir.GetGotoVariable(source.terminator.goto_variable);
break;
case CFG::BranchCondition::Unknown:
EXIT("block %u has an unknown branch condition", source.id);
}
info.condition = condition;
ir.Emit(IR::ValueOpcode::Reference, {condition});
}
namespace {
@@ -842,6 +875,16 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
include_vector(inst.src1);
include_vector(inst.src2);
include_vector(inst.src3);
switch (inst.opcode) {
case Decoder::Opcode::V_CVT_F64_I32:
case Decoder::Opcode::V_CVT_F64_F32:
case Decoder::Opcode::V_CVT_F64_U32: include_vector(inst.dst, 2u); break;
case Decoder::Opcode::V_FMA_F64: include_vector(inst.src2, 2u); [[fallthrough]];
case Decoder::Opcode::V_MUL_F64: include_vector(inst.src1, 2u); [[fallthrough]];
case Decoder::Opcode::V_RCP_F64: include_vector(inst.dst, 2u); [[fallthrough]];
case Decoder::Opcode::V_CVT_F32_F64: include_vector(inst.src0, 2u); break;
default: break;
}
if (inst.family == Decoder::Family::DS) {
switch (inst.opcode) {
case Decoder::Opcode::DS_WRITE_B64:
@@ -1240,9 +1283,11 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
builtin(IR::StageInputKind::InstanceIndex));
}
}
const bool flush_f32_inputs = options.stage == ShaderType::Compute &&
(options.input_info.compute->float_mode & 0x10u) == 0;
for (const auto& cfg_block: cfg.blocks) {
const auto typed_index = block_indices.at(cfg_block.id);
Translator translator(result, result.blocks[typed_index], vector_limit);
Translator translator(result, result.blocks[typed_index], vector_limit, flush_f32_inputs);
for (uint32_t index = cfg_block.inst_begin; index < cfg_block.inst_end; index++) {
const auto& instruction = decoded.instructions[index];
if (IsCodeTableLoad(cfg, instruction.pc)) {
@@ -1265,7 +1310,7 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
}
translator.TranslateInstruction(instruction);
}
translator.AddBranchCondition(cfg_block, result.block_info[typed_index]);
translator.AddBranchCondition(cfg, cfg_block, result.block_info[typed_index]);
}
IR::ValidateProgram(result, false);
return result;
@@ -9,13 +9,15 @@ namespace Libs::Graphics::ShaderRecompiler::Frontend {
class Translator {
public:
Translator(IR::Program& program, IR::Block* block, uint32_t vector_limit)
: program(program), ir(block), current_vector_limit(vector_limit) {}
Translator(IR::Program& program, IR::Block* block, uint32_t vector_limit,
bool flush_f32_inputs)
: program(program), ir(block), current_vector_limit(vector_limit),
flush_f32_inputs(flush_f32_inputs) {}
void TranslateInstruction(const Decoder::Instruction& inst);
void TranslateEmbeddedFetch(const Decoder::Instruction& inst, uint32_t attribute,
uint32_t component_count, const ShaderBufferResource& resource);
void AddBranchCondition(const CFG::BasicBlock& source, IR::BlockInfo& info);
void AddBranchCondition(const CFG::Graph& graph, const CFG::BasicBlock& source, IR::BlockInfo& info);
private:
const Decoder::Operand& SourceAt(const Decoder::Instruction& inst, uint32_t index);
@@ -261,6 +263,7 @@ private:
Decoder::Opcode current_opcode = Decoder::Opcode::UNKNOWN;
uint32_t current_pc = 0;
uint32_t current_vector_limit = 1;
bool flush_f32_inputs;
};
} // namespace Libs::Graphics::ShaderRecompiler::Frontend
@@ -297,6 +297,10 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CVT_F32_UBYTE3: V_CVT_F32_UBYTE(inst, 3); return;
case O::V_CVT_F32_U32: V_CVT_F32_U32(inst); return;
case O::V_CVT_F32_I32: V_CVT_F32_I32(inst); return;
case O::V_CVT_F64_I32: return FloatUnary(inst, IR::ValueOpcode::ConvertF64S32);
case O::V_CVT_F32_F64: return FloatUnary(inst, IR::ValueOpcode::ConvertF32F64);
case O::V_CVT_F64_F32: return FloatUnary(inst, IR::ValueOpcode::ConvertF64F32);
case O::V_CVT_F64_U32: return FloatUnary(inst, IR::ValueOpcode::ConvertF64U32);
case O::V_CVT_U32_F32: V_CVT_U32_F32(inst); return;
case O::V_CVT_I32_F32: V_CVT_I32_F32(inst); return;
case O::V_CVT_F16_F32: V_CVT_F16_F32(inst); return;
@@ -394,6 +398,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_FREXP_MANT_F32: return V_FREXP_MANT_F32(inst);
case O::V_RCP_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecip32);
case O::V_RCP_F64: return FloatUnary(inst, IR::ValueOpcode::FPRecip64);
case O::V_RCP_IFLAG_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecipIFlag32);
case O::V_FRACT_F32: return FloatUnary(inst, IR::ValueOpcode::FPFract32);
case O::V_TRUNC_F32: return FloatUnary(inst, IR::ValueOpcode::FPTrunc32);
@@ -410,6 +415,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_SUB_F32: return FloatBinary(inst, IR::ValueOpcode::FPSub32, false);
case O::V_SUBREV_F32: return FloatBinary(inst, IR::ValueOpcode::FPSub32, true);
case O::V_MUL_F32: return FloatBinary(inst, IR::ValueOpcode::FPMul32, false);
case O::V_MUL_F64: return FloatBinary(inst, IR::ValueOpcode::FPMul64, false);
case O::V_MIN_F32: return FloatBinary(inst, IR::ValueOpcode::FPMin32, false);
case O::V_MAX_F32: return FloatBinary(inst, IR::ValueOpcode::FPMax32, false);
case O::V_LDEXP_F32: return FloatBinary(inst, IR::ValueOpcode::FPLdexp, false);
@@ -418,6 +424,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_MADAK_F32:
case O::V_MAD_F32:
case O::V_FMA_F32: return FloatTernary(inst, IR::ValueOpcode::FPFma32, false, true);
case O::V_FMA_F64: return FloatTernary(inst, IR::ValueOpcode::FPFma64, false, false);
case O::V_MIN3_F32: return FloatTernary(inst, IR::ValueOpcode::FPMinTri32, false, false);
case O::V_MAX3_F32: return FloatTernary(inst, IR::ValueOpcode::FPMaxTri32, false, false);
case O::V_MED3_F32: return FloatTernary(inst, IR::ValueOpcode::FPMedTri32, false, false);
@@ -81,7 +81,8 @@ bool EquivalentValue(const ResourcePlan& program, Value left, Value right,
program.memory_info[li] != program.memory_info[ri]) {
return false;
}
} else if (lhs->Flags<uint64_t>() != rhs->Flags<uint64_t>()) {
} else if (lhs->GetOpcode() != ValueOpcode::ReadConst &&
lhs->Flags<uint64_t>() != rhs->Flags<uint64_t>()) {
return false;
}
for (size_t index = 0; index < lhs->NumArgs(); index++) {
+6 -3
View File
@@ -147,6 +147,7 @@ struct SamplerResource {
uint32_t first_use_pc = 0;
bool force_point_filtering = false;
bool depth_compare = false;
bool integer_border = false;
bool operator==(const SamplerResource& other) const = default;
};
@@ -435,7 +436,7 @@ struct BindingLayout {
};
struct ShaderInfo {
static constexpr uint32_t MaxBuffers = 32;
static constexpr uint32_t MaxBuffers = 64;
static constexpr uint32_t MaxImages = 64;
static constexpr uint32_t MaxSamplers = 32;
static constexpr uint32_t MaxSampledPairs = 64;
@@ -548,7 +549,7 @@ struct ResourcePlan {
std::vector<SrtRead> srt_reads;
std::vector<uint8_t> clean_flat_slots;
bool requires_specialization_memory = false;
bool has_address_writes = false;
bool capture_specialization_reads = false;
bool srt_plan_complete = false;
bool resource_tracking_complete = false;
ShaderInfo info;
@@ -582,10 +583,12 @@ struct Program: ResourcePlan {
CFG::FailureKind cfg_failure_kind = CFG::FailureKind::None;
std::string fallback_reason;
std::vector<BlockInfo> block_info;
struct ScalarWrite { uint32_t pc; ScalarReg reg; };
std::vector<ScalarWrite> scalar_writes;
// Typed memory and export instructions reference shader-local metadata by dense index.
// Decoder-only details (such as NSA register numbers) have already become IR operands.
std::vector<ExportInfo> export_info;
std::vector<Value> dynamic_reads;
bool has_address_writes = false;
bool shader_info_complete = false;
BindingLayout bindings;
bool binding_layout_complete = false;
@@ -30,6 +30,7 @@ std::string TypeName(Type type) {
"ImageResource",
"SamplerResource",
"ImageAddress",
"F64",
};
const auto bits = static_cast<uint32_t>(type);
if (bits == 0) {
+1
View File
@@ -27,6 +27,7 @@ enum class Type : uint32_t {
ImageResource = 1u << 17u,
SamplerResource = 1u << 18u,
ImageAddress = 1u << 19u,
F64 = 1u << 20u,
};
constexpr Type operator|(Type lhs, Type rhs) {
@@ -33,6 +33,7 @@ constexpr Type U32 = Type::U32;
constexpr Type U64 = Type::U64;
constexpr Type F16 = Type::F16;
constexpr Type F32 = Type::F32;
constexpr Type F64 = Type::F64;
constexpr Type U32x2 = Type::U32x2;
constexpr Type U32x3 = Type::U32x3;
constexpr Type U32x4 = Type::U32x4;
@@ -220,6 +221,8 @@ ImageOpcodeInfo ImageOpcodeInfoOf(ValueOpcode opcode) {
case ValueOpcode::ImageAtomicAnd32:
case ValueOpcode::ImageAtomicOr32:
case ValueOpcode::ImageAtomicXor32:
case ValueOpcode::ImageAtomicFMin32:
case ValueOpcode::ImageAtomicFMax32:
return {ImageAccess::Atomic, ImageResourceClass::Storage, false};
default: return {};
}
@@ -52,6 +52,10 @@ struct PermlaneFlags {
static_assert(sizeof(PermlaneFlags) <= sizeof(uint64_t));
static_assert(std::is_trivially_copyable_v<PermlaneFlags>);
struct FPCompareFlags {
bool flush_input_denorms = false;
};
struct MemoryFlags {
uint32_t index = 0;
uint32_t pc = 0;
@@ -4,6 +4,7 @@ VALUE_OPCODE(Identity, Opaque, Opaque)
VALUE_OPCODE(Void, Void)
VALUE_OPCODE(Reference, Void, U1)
VALUE_OPCODE(ReferenceU32, Void, U32)
VALUE_OPCODE(ConditionRef, U1, U1)
// Translation and register-state pseudo operations.
VALUE_OPCODE(GetUserData, U32, ScalarReg)
@@ -52,6 +53,8 @@ VALUE_OPCODE(BitCastU16F16, U16, F16)
VALUE_OPCODE(BitCastF16U16, F16, U16)
VALUE_OPCODE(BitCastU32F32, U32, F32)
VALUE_OPCODE(BitCastF32U32, F32, U32)
VALUE_OPCODE(BitCastU64F64, U64, F64)
VALUE_OPCODE(BitCastF64U64, F64, U64)
VALUE_OPCODE(ConvertU16U32, U16, U32)
VALUE_OPCODE(ConvertU32U16, U32, U16)
VALUE_OPCODE(ConvertU8U32, U8, U32)
@@ -61,7 +64,11 @@ VALUE_OPCODE(ConvertF16F32, F16, F32)
VALUE_OPCODE(ConvertS32F32, U32, F32)
VALUE_OPCODE(ConvertU32F32, U32, F32)
VALUE_OPCODE(ConvertF32S32, F32, U32)
VALUE_OPCODE(ConvertF64S32, F64, U32)
VALUE_OPCODE(ConvertF32F64, F32, F64)
VALUE_OPCODE(ConvertF64F32, F64, F32)
VALUE_OPCODE(ConvertF32U32, F32, U32)
VALUE_OPCODE(ConvertF64U32, F64, U32)
VALUE_OPCODE(CompositeConstructU64, U64, U32, U32)
VALUE_OPCODE(CompositeConstructU32x2, U32x2, U32, U32)
VALUE_OPCODE(CompositeConstructU32x3, U32x3, U32, U32, U32)
@@ -166,6 +173,9 @@ VALUE_OPCODE(FPAdd32, F32, F32, F32)
VALUE_OPCODE(FPSub32, F32, F32, F32)
VALUE_OPCODE(FPFma32, F32, F32, F32, F32)
VALUE_OPCODE(FPMul32, F32, F32, F32)
VALUE_OPCODE(FPMul64, F64, F64, F64)
VALUE_OPCODE(FPFma64, F64, F64, F64, F64)
VALUE_OPCODE(FPRecip64, F64, F64)
VALUE_OPCODE(FPMin32, F32, F32, F32)
VALUE_OPCODE(FPMax32, F32, F32, F32)
VALUE_OPCODE(FPMinTri32, F32, F32, F32, F32)
@@ -267,8 +277,8 @@ VALUE_OPCODE(SharedAtomicUMax32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicAnd32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicOr32, U32, U32, U32, U1)
VALUE_OPCODE(SharedAtomicXor32, U32, U32, U32, U1)
VALUE_OPCODE(DataAppend, U32, U32, U1, U32, U32)
VALUE_OPCODE(DataConsume, U32, U32, U1, U32, U32)
VALUE_OPCODE(DataAppend, U32, U32, U1)
VALUE_OPCODE(DataConsume, U32, U32, U1)
VALUE_OPCODE(SwizzleU32, U32, U32, U32, U1)
VALUE_OPCODE(ImageQueryDimensions, U32x4, ImageResource, ImageAddress)
@@ -284,6 +294,8 @@ VALUE_OPCODE(ImageAtomicUMax32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicAnd32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicOr32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicXor32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicFMin32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicFMax32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(GetAttribute, U32, U32, U32)
VALUE_OPCODE(GetInterpolationParameter, U32, U32, U32, U32)
VALUE_OPCODE(SetAttribute, Void, U32x4, U1)
@@ -46,28 +46,54 @@ void AddBinding(BindingLayout& layout, DescriptorBindingKind kind,
layout.descriptors.push_back({kind, std::move(resources)});
}
bool UsesGds(const Program& program) {
} // namespace
bool CollectMemoryResources(const Program& program, std::vector<uint32_t>& buffers) {
std::array<bool, ShaderInfo::MaxBuffers> live_buffers {};
bool uses_gds = false;
for (const auto* block: program.blocks) {
for (const auto& inst: *block) {
if (SharedAccessOf(inst.GetOpcode()) == SharedAccess::None) {
const auto op = inst.GetOpcode();
if (BufferAccessOf(op) == BufferAccess::None &&
SharedAccessOf(op) == SharedAccess::None) {
continue;
}
const auto index = inst.Flags<MemoryFlags>().index;
if (index >= program.memory_info.size()) {
BindingFail("typed shader contains invalid shared-memory metadata");
BindingFail("typed shader contains invalid memory metadata");
}
const auto kind = program.memory_info[index].kind;
if (kind != ResourceKind::Lds && kind != ResourceKind::Gds) {
BindingFail("typed shader contains invalid shared-memory metadata");
const auto& memory = program.memory_info[index];
if (memory.planning_only) {
continue;
}
uses_gds |= kind == ResourceKind::Gds;
if (SharedAccessOf(op) != SharedAccess::None) {
if (memory.kind != ResourceKind::Lds && memory.kind != ResourceKind::Gds) {
BindingFail("typed shader contains invalid shared-memory metadata");
}
uses_gds |= memory.kind == ResourceKind::Gds;
} else if (memory.kind == ResourceKind::Buffer || memory.kind == ResourceKind::ScalarBuffer) {
EXIT_IF(memory.resource >= program.info.buffers.size());
live_buffers.at(memory.resource) = true;
}
}
}
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
if (live_buffers.at(i)) {
buffers.push_back(i);
}
}
return uses_gds;
}
} // namespace
bool UsesFlattenedSrt(const Program& program) {
return std::ranges::any_of(program.blocks, [](const Block* block) {
return std::ranges::any_of(*block, [](const Inst& inst) {
return inst.GetOpcode() == ValueOpcode::ReadConst;
});
}) || std::ranges::any_of(program.info.images, [](const ImageResource& image) {
return image.indirect_search_iterations != 0u;
});
}
void AllocateBindings(Program& program, uint32_t push_data_start_dword) {
if (!program.shader_info_complete || program.binding_layout_complete) {
@@ -76,18 +102,17 @@ void AllocateBindings(Program& program, uint32_t push_data_start_dword) {
: "binding layout already allocated");
}
BindingLayout next;
std::vector<uint32_t> buffers;
const bool uses_gds = CollectMemoryResources(program, buffers);
next.user_data_registers = CollectUserData(program);
next.memory_offset_dword = static_cast<uint32_t>(next.user_data_registers.size());
next.memory_offset_count = static_cast<uint32_t>(program.info.buffers.size());
next.memory_offset_count = static_cast<uint32_t>(buffers.size());
next.push_data_start_dword =
PushData::StartFor(push_data_start_dword, next.ShaderDataDwords());
if (!program.info.buffers.empty()) {
std::vector<uint32_t> resources(program.info.buffers.size());
for (uint32_t i = 0; i < resources.size(); i++) {
resources[i] = i;
}
AddBinding(next, DescriptorBindingKind::Buffers, std::move(resources));
if (!buffers.empty()) {
// Draw binding accesses this first group directly when memory_offset_count is nonzero.
AddBinding(next, DescriptorBindingKind::Buffers, std::move(buffers));
}
std::array<std::vector<uint32_t>, ImageBindingCount> image_groups;
@@ -123,19 +148,14 @@ void AllocateBindings(Program& program, uint32_t push_data_start_dword) {
}
AddBinding(next, DescriptorBindingKind::Samplers, std::move(resources));
}
if (UsesGds(program)) {
if (uses_gds) {
AddBinding(next, DescriptorBindingKind::Gds);
}
if (program.info.uses_dma) {
AddBinding(next, DescriptorBindingKind::BdaPagetable);
AddBinding(next, DescriptorBindingKind::FaultBuffer);
}
const bool uses_flattened_runtime =
!program.srt_reads.empty() ||
std::ranges::any_of(program.info.images, [](const ImageResource& image) {
return image.indirect_search_iterations != 0u;
});
if (uses_flattened_runtime) {
if (UsesFlattenedSrt(program)) {
AddBinding(next, DescriptorBindingKind::FlattenedSrt);
}
@@ -7,6 +7,10 @@ namespace Libs::Graphics::ShaderRecompiler::IR {
void AllocateBindings(Program& program, uint32_t push_data_start_dword = 0);
// Collect live direct buffer indices and report whether the shader accesses GDS.
bool CollectMemoryResources(const Program& program, std::vector<uint32_t>& buffers);
bool UsesFlattenedSrt(const Program& program);
const DescriptorBinding* FindBinding(const BindingLayout& layout, DescriptorBindingKind kind);
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -630,6 +630,11 @@ void FoldInstruction(Block& block, Block::iterator instruction,
}
}
return;
case ValueOpcode::ConditionRef: {
const auto value = Arg(inst, 0);
if (IsImmediate(value, Type::U1)) Replace(inst, value);
return;
}
case ValueOpcode::LogicalNot: {
const auto value = Arg(inst, 0);
if (IsImmediate(value, Type::U1)) {
@@ -2,6 +2,7 @@
#include "common/assert.h"
#include "graphics/guest_gpu/gpu_format.h"
#include "graphics/shader/recompiler/BufferFormat.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include "graphics/shader/shaderBindings.h"
@@ -106,14 +107,16 @@ Prospero::BufferFormat ImageConversionFormat(Prospero::BufferFormat format) {
: Prospero::BufferFormat::kInvalid;
}
bool RequiresPointSampler(const ImageResource& image) {
return image.numeric_class == Prospero::TextureNumericClass::Sint ||
image.conversion_format != Prospero::BufferFormat::kInvalid;
}
enum class SamplerClass : uint8_t { Float, Integer, PointInteger };
bool RequiresPointSampler(const ResourceSpecialization::Image& image) {
return image.numeric_class == Prospero::TextureNumericClass::Sint ||
image.conversion_format != Prospero::BufferFormat::kInvalid;
template <typename Image>
SamplerClass ClassifySampler(const Image& image) {
if (image.numeric_class == Prospero::TextureNumericClass::Sint ||
image.conversion_format != Prospero::BufferFormat::kInvalid) {
return SamplerClass::PointInteger;
}
return image.numeric_class == Prospero::TextureNumericClass::Uint ? SamplerClass::Integer
: SamplerClass::Float;
}
bool DescriptorIsCube(const DescriptorValue& descriptor) {
@@ -222,9 +225,9 @@ void MarkCleanFlatSlots(const ResourcePlan& program, const DescriptorSource* sou
}
}
bool ReadScalarTable(uint64_t base, uint64_t size, uint32_t dynamic_offset,
bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t dynamic_offset,
const SrtRuntime& runtime, std::span<uint32_t> words) {
const auto offset = static_cast<uint64_t>(dynamic_offset) & ~uint64_t {3};
const auto offset = dynamic_offset & ~uint64_t {3};
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) {
@@ -303,26 +306,24 @@ bool MaterializeIndirectImage(const ResourcePlan& program,
material.Stride() != indirect.selector_stride) {
return false;
}
// Enumerate every wrapped scalar-buffer offset that can pass the descriptor bounds.
const auto step = std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u);
const auto residue = static_cast<uint64_t>(indirect.selector_offset) % step;
const auto limit = std::min<uint64_t>(UINT32_MAX, material.GetSize() + 3u);
const auto probe_count = residue <= limit ? (limit - residue) / step + 1u : 0u;
// The first aligned offset includes the immediate added after shader U32 arithmetic.
const auto step = std::max<uint64_t>(4u,
std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u));
const uint64_t first = indirect.selector_offset;
const auto size = material.GetSize();
const auto limit = std::min(first + (uint64_t {1} << 32u) - step, size >= 4u ? size - 4u : 0u);
const auto probe_count = size >= 4u && first <= limit ? (limit - first) / step + 1u : 0u;
if (probe_count > MaxIndirectImageProbes) {
return false;
}
keys.reserve(static_cast<size_t>(probe_count) + 1u);
keys.push_back(0u);
for (uint64_t offset = residue; offset <= limit && probe_count != 0u; offset += step) {
for (uint64_t probe = 0, offset = first; probe < probe_count; ++probe, offset += step) {
uint32_t key = 0;
if (!ReadScalarTable(material.Base48(), material.GetSize(),
static_cast<uint32_t>(offset), runtime, {&key, 1})) {
if (!ReadScalarTable(material.Base48(), size, offset, runtime, {&key, 1})) {
return false;
}
keys.push_back(key);
if (limit - offset < step) {
break;
}
}
std::ranges::sort(keys);
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
@@ -379,8 +380,13 @@ bool MaterializeIndirectImage(const ResourcePlan& program,
} // namespace
struct SamplerPlan {
std::array<uint32_t, ShaderInfo::MaxSamplers> point_sampler {};
uint32_t sampler_count = 0;
struct Binding {
uint32_t source;
SamplerClass type;
};
std::array<std::array<uint32_t, 3>, ShaderInfo::MaxSamplers> mapping;
std::array<Binding, ShaderInfo::MaxSamplers> bindings;
uint32_t sampler_count = 0;
};
struct ImageRemap {
@@ -400,6 +406,9 @@ struct ImageRemap {
template <typename T>
void Apply(std::vector<T>& images) const {
EXIT_IF(images.size() != source_count);
if (count == source_count) {
return;
}
for (uint32_t index = 0; index < source_count; index++) {
if (indices[index] != UINT32_MAX && indices[index] != index) {
images[indices[index]] = std::move(images[index]);
@@ -446,6 +455,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
: Prospero::BufferFormat::kInvalid,
.descriptor_swizzle =
program.info.buffers[i].formatted ? descriptor.DstSelXYZW() : DstSel(4, 5, 6, 7),
.zero_stride_oob = descriptor.OutOfBounds() == 0u && stride == 0u,
});
}
for (uint32_t i = 0; i < specialization.images.size(); i++) {
@@ -485,7 +495,8 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
image.cube = DescriptorIsCube(descriptor);
const auto format =
static_cast<Prospero::BufferFormat>((descriptor.dwords[1] >> 20u) & 0x1ffu);
if (base.atomic && format != Prospero::BufferFormat::k32UInt) {
if (base.atomic && format != Prospero::BufferFormat::k32UInt &&
format != Prospero::BufferFormat::k32Float) {
return SpecializationFail(
fmt::format("atomic image descriptor {} uses unsupported format {}", i,
static_cast<uint32_t>(format)));
@@ -514,7 +525,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
fmt::format("storage image descriptor {} uses unsupported format {}", i,
static_cast<uint32_t>(format)));
}
if (raw_sint_storage) {
if (raw_sint_storage || base.atomic) {
image.numeric_class = Prospero::TextureNumericClass::Uint;
}
} else if (image.numeric_class == Prospero::TextureNumericClass::Unsupported ||
@@ -589,11 +600,9 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
if (!BuildSamplerPlan(program.info, specialization.images, sampler_plan)) {
return SpecializationFail("specialized sampler layout exceeds its resource limit");
}
for (uint32_t index = 0; index < program.info.samplers.size(); index++) {
const auto target = sampler_plan.point_sampler[index];
if (target != UINT32_MAX && target >= program.info.samplers.size()) {
snapshot.samplers.push_back(snapshot.samplers[index]);
}
for (uint32_t index = static_cast<uint32_t>(program.info.samplers.size());
index < sampler_plan.sampler_count; index++) {
snapshot.samplers.push_back(snapshot.samplers[sampler_plan.bindings[index].source]);
}
ImageRemap(specialization).Apply(snapshot.images);
return true;
@@ -601,29 +610,29 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
template <typename Images>
bool BuildSamplerPlan(const ShaderInfo& base, const Images& images, SamplerPlan& plan) {
if (base.samplers.size() > plan.point_sampler.size()) {
if (base.samplers.size() > plan.mapping.size()) {
return false;
}
std::array<uint8_t, ShaderInfo::MaxSamplers> usage {};
plan.point_sampler.fill(UINT32_MAX);
plan.sampler_count = static_cast<uint32_t>(base.samplers.size());
for (const auto& pair: base.sampled_pairs) {
if (pair.image >= images.size() || pair.sampler >= base.samplers.size()) {
return false;
}
usage[pair.sampler] |= RequiresPointSampler(images[pair.image]) ? 2u : 1u;
usage[pair.sampler] |= 1u << static_cast<uint32_t>(ClassifySampler(images[pair.image]));
}
for (uint32_t index = 0; index < base.samplers.size(); index++) {
if ((usage[index] & 2u) == 0u) {
continue;
}
if ((usage[index] & 1u) == 0u) {
plan.point_sampler[index] = index;
} else {
if (plan.sampler_count >= ShaderInfo::MaxSamplers) {
return false;
}
plan.point_sampler[index] = plan.sampler_count++;
auto& mapping = plan.mapping[index];
mapping.fill(UINT32_MAX);
const auto classes = usage[index] == 0u ? 1u : usage[index];
bool first = true;
for (uint32_t type = 0; type < mapping.size(); type++) {
if ((classes & (1u << type)) == 0u) continue;
const auto target = first ? index : plan.sampler_count++;
if (target >= ShaderInfo::MaxSamplers) return false;
mapping[type] = target;
plan.bindings[target] = {index, static_cast<SamplerClass>(type)};
first = false;
}
}
return true;
@@ -856,7 +865,6 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
plan.memory_info = program.memory_info;
plan.srt_plan_complete = program.srt_plan_complete;
plan.resource_tracking_complete = program.resource_tracking_complete;
plan.has_address_writes = program.has_address_writes;
std::unordered_map<const Inst*, Inst*> cloned;
std::function<Value(Value)> Clone = [&](Value value) -> Value {
@@ -915,17 +923,25 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
}
plan.clean_flat_slots.resize(plan.srt_reads.size());
bool masked_image = false;
for (const auto& image: plan.info.images) {
const auto* source = Source(plan, image.source);
if (source == nullptr || !source->indirect_image.has_value()) {
continue;
}
plan.requires_specialization_memory = true;
masked_image |= !source->indirect_image->selector_mask.IsEmpty();
MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->material_source),
plan.clean_flat_slots, source->indirect_image->selector_mask);
MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->table_source),
plan.clean_flat_slots);
}
if (masked_image) {
plan.resource_tracking_complete &= !program.has_address_writes &&
!std::ranges::any_of(plan.info.images, &ImageResource::written);
plan.capture_specialization_reads =
std::ranges::any_of(plan.info.buffers, &BufferResource::written);
}
return plan;
}
@@ -935,18 +951,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
(program.requires_specialization_memory && runtime.read_specialization_memory == nullptr)) {
return false;
}
const bool masked_image = std::ranges::any_of(program.info.images, [&](const auto& image) {
const auto* source = Source(program, image.source);
return source != nullptr && source->indirect_image.has_value() &&
!source->indirect_image->selector_mask.IsEmpty();
});
if (masked_image &&
(program.has_address_writes ||
std::ranges::any_of(program.info.images, &ImageResource::written))) {
return false;
}
const bool capture_reads = masked_image &&
std::ranges::any_of(program.info.buffers, &BufferResource::written);
const bool capture_reads = program.capture_specialization_reads;
auto& reads = program.specialization_reads;
ReadCapture capture {runtime, reads};
SrtRuntime observed = runtime;
@@ -1099,25 +1104,18 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
auto samplers = program.info.samplers;
auto sampled_pairs = program.info.sampled_pairs;
samplers.reserve(sampler_plan.sampler_count);
for (uint32_t index = 0; index < program.info.samplers.size(); index++) {
const auto target = sampler_plan.point_sampler[index];
if (target == UINT32_MAX) {
continue;
}
if (target == index) {
samplers[index].force_point_filtering = true;
} else {
EXIT_IF(target != samplers.size());
auto sampler = samplers[index];
sampler.force_point_filtering = true;
samplers.push_back(std::move(sampler));
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
const auto& binding = sampler_plan.bindings[index];
if (index >= program.info.samplers.size()) {
samplers.push_back(program.info.samplers[binding.source]);
}
samplers[index].force_point_filtering = binding.type == SamplerClass::PointInteger;
samplers[index].integer_border = binding.type != SamplerClass::Float;
}
for (auto& pair: sampled_pairs) {
if (RequiresPointSampler(images[pair.image])) {
EXIT_IF(sampler_plan.point_sampler[pair.sampler] == UINT32_MAX);
pair.sampler = sampler_plan.point_sampler[pair.sampler];
}
const auto type = static_cast<uint32_t>(ClassifySampler(images[pair.image]));
pair.sampler = sampler_plan.mapping[pair.sampler][type];
EXIT_IF(pair.sampler == UINT32_MAX);
samplers[pair.sampler].depth_compare |= images[pair.image].depth_compare;
}
@@ -1126,6 +1124,49 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
for (auto* block: program.blocks) {
for (auto it = block->begin(); it != block->end(); ++it) {
auto& inst = *it;
if (BufferAccessOf(inst.GetOpcode()) == BufferAccess::Read) {
const auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
if (memory.kind == ResourceKind::Buffer &&
specialization.buffers[memory.resource].zero_stride_oob) {
// Bounds mode 0 checks offset >= stride, so zero stride
// makes every vector read out of bounds regardless of its address.
const auto count = BufferComponentCount(inst.GetOpcode());
std::array<Value, 4> values {Value(0u), Value(0u), Value(0u), Value(0u)};
if (memory.formatted && !memory.typed) {
const auto& buffer = buffers[memory.resource];
const auto format = Format::GetFormatInfo(buffer.descriptor_format);
for (uint32_t component = 0; component < count; component++) {
if (format.type == Format::ComponentType::Unknown ||
GetDstSel(buffer.descriptor_swizzle, component) != 1u) continue;
const auto one = Format::FormattedConstantBits(
format, Format::FormattedSourceKind::One);
values[component] = Value(&*block->PrependNewInst(
it, ValueOpcode::SelectU32,
{inst.Arg(inst.NumArgs() - 1), Value(one), Value(0u)}));
}
}
Value result = values[0];
switch (inst.GetType()) {
case Type::U8: result = Value(uint8_t {0}); break;
case Type::U16: result = Value(uint16_t {0}); break;
case Type::U32x2:
result = Value(&*block->PrependNewInst(it, ValueOpcode::CompositeConstructU32x2,
{values[0], values[1]}));
break;
case Type::U32x3:
result = Value(&*block->PrependNewInst(it, ValueOpcode::CompositeConstructU32x3,
{values[0], values[1], values[2]}));
break;
case Type::U32x4:
result = Value(&*block->PrependNewInst(it, ValueOpcode::CompositeConstructU32x4,
{values[0], values[1], values[2], values[3]}));
break;
default: break;
}
inst.ReplaceUsesWith(result);
}
continue;
}
const auto image_opcode = ImageOpcodeInfoOf(inst.GetOpcode());
if (image_opcode.access == ImageAccess::None) {
continue;
@@ -1152,10 +1193,11 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
inst.ReplaceUsesWith(Value(&*result));
continue;
}
if (image_opcode.needs_sampler && RequiresPointSampler(image) &&
if (image_opcode.needs_sampler &&
memory.sampler < program.info.samplers.size()) {
EXIT_IF(sampler_plan.point_sampler[memory.sampler] == UINT32_MAX);
memory.sampler = sampler_plan.point_sampler[memory.sampler];
const auto type = static_cast<uint32_t>(ClassifySampler(image));
memory.sampler = sampler_plan.mapping[memory.sampler][type];
EXIT_IF(memory.sampler == UINT32_MAX);
}
EXIT_IF(image.indirect_root == memory.resource &&
inst.GetOpcode() != ValueOpcode::ImageSampleRaw);
@@ -12,6 +12,7 @@ struct ResourceSpecialization {
uint32_t packed_stride = 0;
Prospero::BufferFormat descriptor_format = Prospero::BufferFormat::kInvalid;
uint32_t descriptor_swizzle = DstSel(4, 5, 6, 7);
bool zero_stride_oob = false;
bool operator==(const Buffer&) const = default;
};
File diff suppressed because it is too large. Load diff
@@ -5,9 +5,8 @@
namespace Libs::Graphics::ShaderRecompiler::IR {
// Collects immutable resource topology from typed SSA handles, interns their resolved dwords in
// descriptor_sources, then writes dense indices to handle flags and MemoryInfo.
void TrackResources(Program& program);
// 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);
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -7,7 +7,6 @@
#include <bit>
#include <cmath>
#include <cstring>
#include <fmt/format.h>
#include <unordered_set>
namespace Libs::Graphics::ShaderRecompiler::IR {
@@ -23,21 +22,6 @@ namespace {
constexpr uint64_t AddressMask = 0x0000ffffffffffffull;
const char* StageName(ShaderType stage) {
switch (stage) {
case ShaderType::Vertex: return "vertex";
case ShaderType::Pixel: return "pixel";
case ShaderType::Fetch: return "fetch";
case ShaderType::Compute: return "compute";
default: return "unknown";
}
}
std::string Diagnostic(const ResourcePlan& program, uint32_t pc, const std::string& message) {
return fmt::format("shader SRT: hash=0x{:016x} stage={} pc=0x{:08x} {}", program.shader_hash,
StageName(program.stage), pc, message);
}
bool AddSignedAddress(uint64_t base, int64_t offset, uint64_t& result) {
if (base > AddressMask) {
return false;
@@ -89,6 +73,7 @@ bool IsRuntimeSelect(ValueOpcode op) {
bool IsRuntimeUniformOp(ValueOpcode op) {
switch (op) {
case ValueOpcode::ConditionRef:
case ValueOpcode::BitCastU32F32:
case ValueOpcode::BitCastF32U32:
case ValueOpcode::ConvertU32F32:
@@ -312,151 +297,6 @@ private:
std::unordered_set<const Inst*> m_validated_dependencies;
};
class PlanBuilder {
public:
explicit PlanBuilder(Program& program): m_program(program) {}
void Run() {
m_program.srt_reads.clear();
m_program.dynamic_reads.clear();
for (auto* block: m_program.blocks) {
for (auto& inst: *block) {
const auto op = inst.GetOpcode();
if (op == ValueOpcode::LoadAddressU32 || op == ValueOpcode::ReadConstBuffer) {
const auto flags = inst.Flags<MemoryFlags>();
if (flags.index < m_program.memory_info.size()) {
const auto kind = m_program.memory_info[flags.index].kind;
const bool crosswired = (op == ValueOpcode::LoadAddressU32 &&
kind == ResourceKind::ScalarBuffer) ||
(op == ValueOpcode::ReadConstBuffer &&
kind == ResourceKind::ScalarAddress);
if (crosswired) {
Fail(flags.pc,
fmt::format("{} has incompatible scalar memory metadata",
ValueOpcodeName(op)));
}
}
}
if (IsDescriptorHandle(inst.GetOpcode())) {
for (size_t index = 0; index < inst.NumArgs(); index++) {
Collect(inst.Arg(index), 0);
}
}
}
}
for (auto* block: m_program.blocks) {
for (auto& inst: *block) {
if (inst.GetOpcode() == ValueOpcode::LoadAddressU32 && IsRawRead(m_program, inst) &&
inst.Arg(1).Resolve().IsImmediate() &&
ValidateRuntimeValue(m_program, Value(&inst))) {
Collect(Value(&inst), inst.Flags<MemoryFlags>().pc);
}
}
}
PatchReads();
}
private:
struct Patch {
Inst* inst = nullptr;
uint32_t slot = 0;
bool keep = false;
};
[[noreturn]] void Fail(uint32_t pc, const std::string& message) const {
const auto diagnostic = Diagnostic(m_program, pc, message);
EXIT("shader SRT planning failed: %s", diagnostic.c_str());
std::abort();
}
void Collect(Value value, uint32_t use_pc) {
value = value.Resolve();
if (value.IsImmediate()) {
return;
}
auto* inst = value.TryInstruction();
if (inst == nullptr) {
Fail(use_pc, "invalid typed planning value");
}
const auto cycle = std::ranges::find(m_visiting, inst);
if (cycle != m_visiting.end()) {
const auto contains_phi = std::any_of(cycle, m_visiting.end(), [](const Inst* value) {
return value->GetOpcode() == ValueOpcode::Phi;
});
if (contains_phi) {
return;
}
Fail(use_pc, fmt::format("cyclic typed planning value {} without a phi",
ValueOpcodeName(inst->GetOpcode())));
}
if (std::ranges::find(m_visited, inst) != m_visited.end()) {
return;
}
m_visiting.push_back(inst);
for (size_t index = 0; index < inst->NumArgs(); index++) {
Collect(inst->Arg(index), use_pc);
}
m_visiting.pop_back();
m_visited.push_back(inst);
if (!IsRawRead(m_program, *inst)) {
return;
}
const auto offset = inst->Arg(1).Resolve();
if (!offset.IsImmediate() || offset.GetType() != Type::U32) {
if (std::ranges::find(m_program.dynamic_reads, value) ==
m_program.dynamic_reads.end()) {
m_program.dynamic_reads.push_back(value);
}
return;
}
for (uint32_t slot = 0; slot < m_program.srt_reads.size(); slot++) {
if (EquivalentValue(m_program, value, m_program.srt_reads[slot].value)) {
m_patches.push_back({inst, slot, false});
return;
}
}
const auto slot = static_cast<uint32_t>(m_program.srt_reads.size());
m_program.srt_reads.push_back({value, slot});
m_patches.push_back({inst, slot, true});
}
void PatchReads() {
for (const auto& patch: m_patches) {
auto* block = patch.inst->Parent();
auto& list = block->Instructions();
auto where =
std::ranges::find_if(list, [&](const Inst& inst) { return &inst == patch.inst; });
const auto resource =
Value(&*block->PrependNewInst(where, ValueOpcode::GetSrtResource));
const auto flat = Value(&*block->PrependNewInst(where, ValueOpcode::ReadConst,
{resource, Value(patch.slot)}));
const auto uses = patch.inst->Uses();
for (const auto& use: uses) {
use.user->SetArg(use.operand, flat);
}
for (auto& info: m_program.block_info) {
if (info.condition.Resolve() == Value(patch.inst)) {
info.condition = flat;
}
if (info.indirect_target.Resolve() == Value(patch.inst)) {
info.indirect_target = flat;
}
}
if (patch.keep) {
const auto memory = patch.inst->Flags<MemoryFlags>().index;
if (memory < m_program.memory_info.size()) {
m_program.memory_info[memory].planning_only = true;
}
block->AppendNewInst(ValueOpcode::ReferenceU32, {Value(patch.inst)});
}
}
}
Program& m_program;
std::vector<Inst*> m_visiting;
std::vector<Inst*> m_visited;
std::vector<Patch> m_patches;
};
} // namespace
@@ -616,16 +456,15 @@ bool SrtWalker::EvaluateRawRead(const Inst& inst, uint64_t& result) {
return false;
}
const auto byte_offset =
static_cast<uint64_t>(immediate) + static_cast<uint32_t>(offset);
const auto aligned = byte_offset & ~uint64_t {3};
(static_cast<uint64_t>(immediate) & ~uint64_t {3}) + (static_cast<uint32_t>(offset) & ~3u);
const auto stride = (static_cast<uint32_t>(high) >> 16u) & 0x3fffu;
const auto size = stride == 0u
? static_cast<uint64_t>(static_cast<uint32_t>(records))
: static_cast<uint64_t>(stride) * static_cast<uint32_t>(records);
if (aligned > size || size - aligned < sizeof(uint32_t)) {
if (byte_offset > size || size - byte_offset < sizeof(uint32_t)) {
return false;
}
address = ((base & ~uint64_t {3}) + byte_offset) & ~uint64_t {3};
address = (base & ~uint64_t {3}) + byte_offset;
} else {
const auto relative = (immediate & ~int64_t {3}) +
static_cast<int64_t>(static_cast<uint32_t>(offset) & ~3u);
@@ -780,14 +619,18 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
}
return false;
case ValueOpcode::FPOrdLessThanEqual32:
if (binary()) {
result = Float32(a) <= Float32(b);
return true;
}
return false;
case ValueOpcode::FPOrdGreaterThanEqual32:
if (binary()) {
result = Float32(a) >= Float32(b);
const auto operand = [&](uint64_t bits) {
if (inst.Flags<FPCompareFlags>().flush_input_denorms &&
(bits & 0x7fffffffu) < 0x00800000u) {
bits &= 0x80000000u;
}
return Float32(bits);
};
result = inst.GetOpcode() == ValueOpcode::FPOrdLessThanEqual32
? operand(a) <= operand(b)
: operand(a) >= operand(b);
return true;
}
return false;
@@ -970,6 +813,7 @@ bool SrtWalker::EvaluateInst(const Inst& inst, uint64_t& result) {
return true;
}
return false;
case ValueOpcode::ConditionRef: return Arg(inst, 0, result);
case ValueOpcode::LogicalNot:
if (Arg(inst, 0, a)) {
result = a == 0u;
@@ -1061,13 +905,5 @@ bool ValidateRuntimeValue(const ResourcePlan& program, Value value, RuntimeValue
return RuntimeValidator(program, type).Run(value);
}
void BuildSrtPlan(Program& program) {
if (program.resource_tracking_complete) {
EXIT("shader SRT planning failed: cannot rebuild SRT after resource tracking");
}
program.srt_plan_complete = false;
PlanBuilder(program).Run();
program.srt_plan_complete = true;
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -21,9 +21,6 @@ struct SrtRuntime {
enum class RuntimeValueType { Any, Integer };
// Collects reachable ReadConst values. Immediate offsets receive compact flat-buffer slots;
// dynamic offsets remain explicit and are never assigned a fake slot.
void BuildSrtPlan(Program& program);
bool ValidateRuntimeValue(const ResourcePlan& program, Value value,
RuntimeValueType type = RuntimeValueType::Any);
// Uses the strict reader for values that affect shader specialization.
+2 -19
View File
@@ -409,16 +409,6 @@ static void ShaderApplyAttribSemantics(ShaderVertexInputInfo& info,
uint32_t offset = (attrib[in.semantic] >> 14u) & 0xfffu;
uint32_t fetch_index = (attrib[in.semantic] >> 26u) & 0x1u;
if (fetch_index != 0) {
static std::atomic<uint64_t> log_count = 0;
auto log_id = log_count.fetch_add(1);
if (log_id < 64) {
LOGF("\t temporary: PS5 vertex attrib semantic %u uses fetch index %u, buffer "
"index %zu\n",
static_cast<uint32_t>(in.semantic), fetch_index, index);
}
}
EXIT_NOT_IMPLEMENTED(index >= ShaderVertexInputInfo::RES_MAX);
const auto* sharp = &buffer[index * 4];
@@ -439,14 +429,6 @@ static void ShaderApplyAttribSemantics(ShaderVertexInputInfo& info,
const auto format_raw = static_cast<uint32_t>(format);
const auto buffer_format = format_raw >> 2u;
const auto channels = (format_raw & 3u) + 1u;
static std::atomic<uint64_t> log_count = 0;
auto log_id = log_count.fetch_add(1);
if (log_id < 64) {
LOGF("\t PS5 vertex attrib semantic %u uses attrib format %u -> buffer "
"format %u, offset %u, buffer index %zu\n",
static_cast<uint32_t>(in.semantic), static_cast<uint32_t>(format),
static_cast<uint32_t>(buffer_format), offset, index);
}
// AGC vertex formats encode the buffer format above the two channel-count bits.
// The fetch prolog selects X001, XY01, XYZ1, or XYZW from that count.
r.fields[3] = (r.fields[3] & ~((0x7fu << 12u) | 0xfffu)) |
@@ -640,6 +622,7 @@ static void ShaderGetStaticInputInfoCS(const HW::ComputeShaderInfo& regs,
info.threads_num[2] = regs.cs_regs.num_thread_z;
info.lds_size_dwords = static_cast<uint32_t>(regs.cs_regs.lds_size) * 128u;
info.scratch_size_dwords = data.scratch_size_dwords;
info.float_mode = regs.cs_regs.float_mode;
info.group_id[0] = regs.cs_regs.tgid_x_en != 0;
info.group_id[1] = regs.cs_regs.tgid_y_en != 0;
info.group_id[2] = regs.cs_regs.tgid_z_en != 0;
@@ -745,7 +728,7 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
void BuildStageStaticKey(const ShaderComputeInputInfo& info, std::vector<uint32_t>& key) {
key.clear();
key.push_back(info.workgroup_register);
key.push_back(info.wave_size);
key.push_back(info.wave_size | (static_cast<uint32_t>(info.float_mode) << 8u));
key.push_back(info.host_subgroup_size);
key.push_back(info.thread_ids_num);
key.push_back(info.lds_size_dwords);
+1
View File
@@ -145,6 +145,7 @@ struct ShaderVertexInputInfo {
};
struct ShaderComputeInputInfo: ShaderWorkgroupInputInfo {
uint8_t float_mode = 0xc0;
uint32_t dispatch_threads_num[3] = {0, 0, 0};
bool group_id[3] = {false, false, false};
bool dispatch_thread_dimensions = false;
+182 -69
View File
@@ -78,8 +78,7 @@ static Graphics::RenderContext& GetGpuResources() {
}
static bool IsGpuAddressRange(uint64_t vaddr, uint64_t size) {
constexpr uint64_t GPU_ADDRESS_LIMIT = 1ull << 40u;
return vaddr != 0 && size != 0 && vaddr < GPU_ADDRESS_LIMIT && size < GPU_ADDRESS_LIMIT - vaddr;
return Graphics::GuestRange {vaddr, size}.Valid();
}
static void MapGpuRange(uint64_t vaddr, uint64_t size) {
@@ -660,6 +659,9 @@ static bool g_test_fail_next_fixed_reserve_range_add = false;
class PhysicalMemory {
public:
enum class AllocationKind { Direct, Pooled, Automatic };
using PhysicalRanges = std::vector<std::pair<uint64_t, uint64_t>>;
struct AllocatedBlock {
uint64_t start_addr;
uint64_t size;
@@ -671,7 +673,7 @@ public:
VirtualMemory::Mode mode;
GpuAccessMode gpu_mode;
int memory_type;
bool pool_expansion;
AllocationKind kind;
char name[KERNEL_MAXIMUM_NAME_LENGTH];
};
@@ -690,7 +692,10 @@ public:
}
bool Alloc(uint64_t search_start, uint64_t search_end, size_t len, size_t alignment,
uint64_t* phys_addr_out, int memory_type, bool pool_expansion = false);
uint64_t* phys_addr_out, int memory_type,
AllocationKind kind = AllocationKind::Direct);
bool ReserveAutomatic(uint64_t size, PhysicalRanges* ranges);
void RestoreAutomatic(const PhysicalRanges& ranges);
bool Available(uint64_t search_start, uint64_t search_end, size_t alignment,
uint64_t* phys_addr_out, uint64_t* size_out);
bool Release(uint64_t start, size_t len, uint64_t* vaddr, uint64_t* size,
@@ -718,12 +723,14 @@ public:
[[nodiscard]] const std::vector<AllocatedBlock>& GetMappings() const { return m_mappings; }
private:
void ConsumeFreeRange(std::map<uint64_t, uint64_t>::iterator range, uint64_t start,
uint64_t size);
void AddFreeRange(uint64_t start, uint64_t size);
static void RemoveFreeRange(std::map<uint64_t, uint64_t>& ranges, uint64_t start,
uint64_t size);
static void AddFreeRange(std::map<uint64_t, uint64_t>& ranges, uint64_t start, uint64_t size);
void ReclaimAutomatic(uint64_t start, uint64_t size);
std::map<uint64_t, AllocatedBlock> m_physical;
std::map<uint64_t, uint64_t> m_free;
std::map<uint64_t, uint64_t> m_automatic_free;
std::vector<AllocatedBlock> m_mappings;
Common::Mutex m_mutex;
};
@@ -1093,7 +1100,7 @@ void SetFlexibleMemorySize(uint64_t size) {
}
bool PhysicalMemory::Alloc(uint64_t search_start, uint64_t search_end, size_t len, size_t alignment,
uint64_t* phys_addr_out, int memory_type, bool pool_expansion) {
uint64_t* phys_addr_out, int memory_type, AllocationKind kind) {
if (phys_addr_out == nullptr) {
return false;
}
@@ -1128,10 +1135,13 @@ bool PhysicalMemory::Alloc(uint64_t search_start, uint64_t search_end, size_t le
b.prot = 0;
b.mode = VirtualMemory::Mode::NoAccess;
b.memory_type = memory_type;
b.pool_expansion = pool_expansion;
b.kind = kind;
ConsumeFreeRange(range, free_pos, len);
RemoveFreeRange(m_free, free_pos, len);
EXIT_IF(!m_physical.emplace(b.start_addr, b).second);
if (kind == AllocationKind::Automatic) {
AddFreeRange(m_automatic_free, free_pos, len);
}
*phys_addr_out = free_pos;
return true;
@@ -1180,35 +1190,99 @@ bool PhysicalMemory::Available(uint64_t search_start, uint64_t search_end, size_
return true;
}
void PhysicalMemory::ConsumeFreeRange(std::map<uint64_t, uint64_t>::iterator range, uint64_t start,
uint64_t size) {
const auto range_start = range->first;
const auto range_end = range->first + range->second;
m_free.erase(range);
if (range_start < start) {
m_free.emplace(range_start, start - range_start);
void PhysicalMemory::RemoveFreeRange(std::map<uint64_t, uint64_t>& ranges, uint64_t start,
uint64_t size) {
const auto end = start + size;
auto range = ranges.upper_bound(start);
if (range != ranges.begin()) {
--range;
}
if (start + size < range_end) {
m_free.emplace(start + size, range_end - start - size);
while (range != ranges.end() && range->first < end) {
const auto range_start = range->first;
const auto range_end = range_start + range->second;
if (range_end <= start) {
++range;
continue;
}
range = ranges.erase(range);
if (range_start < start) {
ranges.emplace(range_start, start - range_start);
}
if (end < range_end) {
ranges.emplace(end, range_end - end);
}
}
}
void PhysicalMemory::AddFreeRange(uint64_t start, uint64_t size) {
void PhysicalMemory::AddFreeRange(std::map<uint64_t, uint64_t>& ranges, uint64_t start,
uint64_t size) {
auto end = start + size;
auto next = m_free.lower_bound(start);
if (next != m_free.begin()) {
auto next = ranges.lower_bound(start);
if (next != ranges.begin()) {
auto previous = std::prev(next);
if (previous->first + previous->second >= start) {
start = previous->first;
end = std::max(end, previous->first + previous->second);
next = m_free.erase(previous);
next = ranges.erase(previous);
}
}
while (next != m_free.end() && next->first <= end) {
while (next != ranges.end() && next->first <= end) {
end = std::max(end, next->first + next->second);
next = m_free.erase(next);
next = ranges.erase(next);
}
ranges.emplace(start, end - start);
}
bool PhysicalMemory::ReserveAutomatic(uint64_t size, PhysicalRanges* ranges) {
Common::LockGuard lock(m_mutex);
for (const auto& [start, available]: m_automatic_free) {
const auto length = std::min(size, available);
ranges->emplace_back(start, length);
size -= length;
if (size == 0) {
for (const auto& [offset, length]: *ranges) {
RemoveFreeRange(m_automatic_free, offset, length);
}
return true;
}
}
ranges->clear();
return false;
}
void PhysicalMemory::RestoreAutomatic(const PhysicalRanges& ranges) {
Common::LockGuard lock(m_mutex);
for (const auto& [start, size]: ranges) {
AddFreeRange(m_automatic_free, start, size);
}
}
void PhysicalMemory::ReclaimAutomatic(uint64_t start, uint64_t size) {
const auto end = start + size;
auto owner = m_physical.upper_bound(start);
if (owner != m_physical.begin()) {
--owner;
}
bool automatic = false;
for (; owner != m_physical.end() && owner->first < end; ++owner) {
const auto& block = owner->second;
if (block.kind != AllocationKind::Automatic || block.start_addr + block.size <= start) {
continue;
}
const auto first = std::max(start, block.start_addr);
const auto last = std::min(end, block.start_addr + block.size);
AddFreeRange(m_automatic_free, first, last - first);
automatic = true;
}
if (automatic) {
for (const auto& mapping: m_mappings) {
const auto first = std::max(start, mapping.start_addr);
const auto last = std::min(end, mapping.start_addr + mapping.size);
if (first < last) {
RemoveFreeRange(m_automatic_free, first, last - first);
}
}
}
m_free.emplace(start, end - start);
}
bool PhysicalMemory::Release(uint64_t start, size_t len, uint64_t* vaddr, uint64_t* size,
@@ -1225,18 +1299,22 @@ bool PhysicalMemory::Release(uint64_t start, size_t len, uint64_t* vaddr, uint64
}
auto it = std::prev(next);
auto& b = it->second;
if (b.pool_expansion || start < b.start_addr || start >= b.start_addr + b.size ||
len > b.start_addr + b.size - start) {
if (b.kind == AllocationKind::Pooled || start < b.start_addr ||
start >= b.start_addr + b.size || len > b.start_addr + b.size - start) {
return false;
}
if (b.kind == AllocationKind::Automatic) {
RemoveFreeRange(m_automatic_free, start, len);
}
if (start == b.start_addr && len == b.size) {
*vaddr = b.map_vaddr;
*size = b.map_size;
*gpu_mode = b.gpu_mode;
m_physical.erase(it);
AddFreeRange(start, len);
AddFreeRange(m_free, start, len);
return true;
}
if (start > b.start_addr && start + len < b.start_addr + b.size) {
@@ -1261,7 +1339,7 @@ bool PhysicalMemory::Release(uint64_t start, size_t len, uint64_t* vaddr, uint64
}
m_physical.emplace(right.start_addr, right);
AddFreeRange(start, len);
AddFreeRange(m_free, start, len);
return true;
}
if (start == b.start_addr && len < b.size) {
@@ -1278,7 +1356,7 @@ bool PhysicalMemory::Release(uint64_t start, size_t len, uint64_t* vaddr, uint64
remaining.map_size -= len;
}
m_physical.emplace(remaining.start_addr, remaining);
AddFreeRange(start, len);
AddFreeRange(m_free, start, len);
return true;
}
if (start > b.start_addr && start + len == b.start_addr + b.size) {
@@ -1290,7 +1368,7 @@ bool PhysicalMemory::Release(uint64_t start, size_t len, uint64_t* vaddr, uint64
if (b.map_vaddr != 0) {
b.map_size = b.size;
}
AddFreeRange(start, len);
AddFreeRange(m_free, start, len);
return true;
}
@@ -1311,6 +1389,7 @@ bool PhysicalMemory::Map(uint64_t vaddr, uint64_t phys_addr, size_t len, int pro
return false;
}
auto first = std::prev(next);
bool has_automatic = false;
while (current < phys_addr + len) {
auto block = m_physical.upper_bound(current);
if (block == m_physical.begin()) {
@@ -1318,10 +1397,11 @@ bool PhysicalMemory::Map(uint64_t vaddr, uint64_t phys_addr, size_t len, int pro
}
--block;
const auto block_end = block->second.start_addr + block->second.size;
if (block->second.pool_expansion || current < block->second.start_addr ||
if (block->second.kind == AllocationKind::Pooled || current < block->second.start_addr ||
current >= block_end) {
return false;
}
has_automatic |= block->second.kind == AllocationKind::Automatic;
current = std::min<uint64_t>(phys_addr + len, block_end);
if (current < phys_addr + len) {
const auto following = std::next(block);
@@ -1342,6 +1422,9 @@ bool PhysicalMemory::Map(uint64_t vaddr, uint64_t phys_addr, size_t len, int pro
mapping.mode = mode;
mapping.gpu_mode = gpu_mode;
m_mappings.push_back(mapping);
if (has_automatic) {
RemoveFreeRange(m_automatic_free, phys_addr, len);
}
return true;
}
@@ -1362,7 +1445,7 @@ bool PhysicalMemory::CanMapDirect(uint64_t phys_addr, size_t len) {
}
--block;
const auto block_end = block->second.start_addr + block->second.size;
if (block->second.pool_expansion || current < block->second.start_addr ||
if (block->second.kind == AllocationKind::Pooled || current < block->second.start_addr ||
current >= block_end) {
return false;
}
@@ -1380,11 +1463,12 @@ bool PhysicalMemory::CanMapDirect(uint64_t phys_addr, size_t len) {
bool PhysicalMemory::ReleasePoolExpansion(uint64_t phys_addr, size_t len) {
Common::LockGuard lock(m_mutex);
const auto it = m_physical.find(phys_addr);
if (it == m_physical.end() || !it->second.pool_expansion || it->second.size != len) {
if (it == m_physical.end() || it->second.kind != AllocationKind::Pooled ||
it->second.size != len) {
return false;
}
m_physical.erase(it);
AddFreeRange(phys_addr, len);
AddFreeRange(m_free, phys_addr, len);
return true;
}
@@ -1407,7 +1491,7 @@ bool PhysicalMemory::GetAllocatedSpan(uint64_t phys_addr, size_t len,
}
--block;
const auto block_end = block->second.start_addr + block->second.size;
if (block->second.pool_expansion || current < block->second.start_addr ||
if (block->second.kind == AllocationKind::Pooled || current < block->second.start_addr ||
current >= block_end) {
blocks->clear();
return false;
@@ -1463,49 +1547,39 @@ bool PhysicalMemory::Unmap(uint64_t vaddr, uint64_t size, GpuAccessMode* gpu_mod
}
};
size_t index = 0;
for (auto& b: m_mappings) {
if (b.map_vaddr == vaddr && b.map_size == size) {
*gpu_mode = b.gpu_mode;
const auto host_vaddr = b.host_vaddr;
const auto host_size = b.host_size;
for (size_t index = 0; index < m_mappings.size(); ++index) {
auto& b = m_mappings[index];
if (vaddr < b.map_vaddr || vaddr >= b.map_vaddr + b.map_size ||
size > b.map_vaddr + b.map_size - vaddr) {
continue;
}
*gpu_mode = b.gpu_mode;
const auto phys_addr = b.start_addr + vaddr - b.map_vaddr;
const auto host_vaddr = b.host_vaddr;
const auto host_size = b.host_size;
if (vaddr == b.map_vaddr && size == b.map_size) {
m_mappings.erase(m_mappings.begin() + static_cast<std::ptrdiff_t>(index));
set_host_release_if_last(host_vaddr, host_size);
return true;
}
if (vaddr > b.map_vaddr && vaddr + size < b.map_vaddr + b.map_size) {
*gpu_mode = b.gpu_mode;
} else if (vaddr > b.map_vaddr && vaddr + size < b.map_vaddr + b.map_size) {
AllocatedBlock right = b;
right.start_addr += (vaddr + size) - b.map_vaddr;
right.size = b.map_vaddr + b.map_size - (vaddr + size);
right.map_size = right.size;
right.map_vaddr = vaddr + size;
b.size = vaddr - b.map_vaddr;
b.map_size = b.size;
m_mappings.push_back(right);
return true;
}
if (vaddr == b.map_vaddr && size < b.map_size) {
*gpu_mode = b.gpu_mode;
} else if (vaddr == b.map_vaddr) {
b.start_addr += size;
b.size -= size;
b.map_vaddr += size;
b.map_size -= size;
return true;
}
if (vaddr > b.map_vaddr && vaddr + size == b.map_vaddr + b.map_size) {
*gpu_mode = b.gpu_mode;
} else {
b.size = vaddr - b.map_vaddr;
b.map_size = b.size;
return true;
}
index++;
ReclaimAutomatic(phys_addr, size);
return true;
}
return false;
@@ -2726,10 +2800,8 @@ int KYTY_SYSV_ABI KernelDirectMemoryQuery(int64_t offset, int flags, void* info,
return OK;
}
int KYTY_SYSV_ABI KernelAllocateDirectMemory(int64_t search_start, int64_t search_end, size_t len,
size_t alignment, int memory_type,
int64_t* phys_addr_out) {
PRINT_NAME();
int AllocateDirectMemory(int64_t search_start, int64_t search_end, size_t len, size_t alignment,
int memory_type, int64_t* phys_addr_out, bool automatic) {
std::lock_guard<std::recursive_mutex> memory_operation_lock(g_memory_operation_mutex);
@@ -2748,7 +2820,10 @@ int KYTY_SYSV_ABI KernelAllocateDirectMemory(int64_t search_start, int64_t searc
}
uint64_t addr = 0;
if (!g_physical_memory->Alloc(search_start, search_end, len, alignment, &addr, memory_type)) {
const auto kind = automatic ? PhysicalMemory::AllocationKind::Automatic
: PhysicalMemory::AllocationKind::Direct;
if (!g_physical_memory->Alloc(search_start, search_end, len, alignment, &addr, memory_type,
kind)) {
LOGF_COLOR(Log::Color::Red, "\t[Fail]\n");
return KERNEL_ERROR_EAGAIN;
}
@@ -2760,6 +2835,14 @@ int KYTY_SYSV_ABI KernelAllocateDirectMemory(int64_t search_start, int64_t searc
return OK;
}
int KYTY_SYSV_ABI KernelAllocateDirectMemory(int64_t search_start, int64_t search_end, size_t len,
size_t alignment, int memory_type,
int64_t* phys_addr_out) {
PRINT_NAME();
return AllocateDirectMemory(search_start, search_end, len, alignment, memory_type,
phys_addr_out);
}
int KYTY_SYSV_ABI KernelAllocateMainDirectMemory(size_t len, size_t alignment, int memory_type,
int64_t* phys_addr_out) {
PRINT_NAME();
@@ -3082,6 +3165,36 @@ int KYTY_SYSV_ABI KernelMapDirectMemory2(void** addr, size_t len, int type, int
return ret;
}
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;
}
PhysicalMemory::PhysicalRanges ranges;
if (!g_physical_memory->ReserveAutomatic(size, &ranges)) {
return KERNEL_ERROR_EAGAIN;
}
uint64_t mapped = 0;
for (const auto& [offset, length]: ranges) {
void* address = reinterpret_cast<void*>(vaddr + mapped);
const int result = KernelMapDirectMemory2(&address, length, type, prot, 0x10,
static_cast<int64_t>(offset), page_size);
if (result != OK) {
if (mapped != 0) {
EXIT_IF(KernelMunmap(vaddr, mapped) != OK);
}
g_physical_memory->RestoreAutomatic(ranges);
return result;
}
mapped += length;
}
return OK;
}
int KYTY_SYSV_ABI KernelMapNamedDirectMemory(void** addr, size_t len, int prot, int flags,
int64_t direct_memory_start, size_t alignment,
const char* name) {
@@ -3892,7 +4005,7 @@ int KYTY_SYSV_ABI KernelMemoryPoolExpand(int64_t search_start, int64_t search_en
uint64_t phys_addr = 0;
if (!g_physical_memory->Alloc(static_cast<uint64_t>(search_start),
static_cast<uint64_t>(search_end), len, effective_alignment,
&phys_addr, 0, true)) {
&phys_addr, 0, PhysicalMemory::AllocationKind::Pooled)) {
return KERNEL_ERROR_ENOMEM;
}
+5
View File
@@ -24,6 +24,8 @@ struct Lifecycle {
using callback_func_t = void (*)(uintptr_t addr, size_t size);
constexpr uint32_t KERNEL_MAXIMUM_NAME_LENGTH = 32;
constexpr uint64_t kExtendedMemoryBase = 0x080000000000ull;
constexpr uint64_t kExtendedMemorySize = 512ull * 1024 * 1024 * 1024;
struct VirtualQueryInfo {
uintptr_t start;
@@ -107,6 +109,9 @@ static_assert(sizeof(KernelMemoryPoolBlockStats) == 16,
void RegisterCallbacks(callback_func_t alloc_func, callback_func_t free_func);
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);
bool TryWriteBacking(uint64_t vaddr, const void* data, uint64_t size);
bool TryReadBacking(uint64_t vaddr, void* data, uint64_t size);
bool TryReadGpuCleanBacking(uint64_t vaddr, void* data, uint64_t size);
+37 -38
View File
@@ -3,7 +3,6 @@
static constexpr uint64_t HOST_SYSTEM_MANAGED_MIN = 0x0000040000ull;
static constexpr uint64_t HOST_SYSTEM_MANAGED_MAX = 0x07ffffbfffull;
static constexpr uint64_t HOST_SYSTEM_RESERVED_MIN = HOST_SYSTEM_MANAGED_MAX + 1u;
#if defined(__APPLE__) && (defined(__x86_64__) || defined(_M_X64))
static constexpr uint64_t HOST_SYSTEM_RESERVED_MAX = 0x0fbfffffffull;
static constexpr uint64_t HOST_USER_MIN = 0x7000000000ull;
@@ -1077,51 +1076,51 @@ private:
#endif
void ReserveGuestRegions() {
const std::array<std::pair<uint64_t, uint64_t>, 3> regions {{
{HOST_SYSTEM_MANAGED_MIN, HOST_SYSTEM_RESERVED_MAX + 1u},
{HOST_USER_MIN, HOST_USER_MAX + 1u},
{kExtendedMemoryBase, kExtendedMemoryBase + kExtendedMemorySize},
}};
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
constexpr uint64_t MinimumRegionSize = 0x1000000;
const auto process = GetCurrentProcess();
const auto guest_end = HOST_USER_MAX + 1u;
auto next_addr = HOST_SYSTEM_MANAGED_MIN;
while (next_addr < guest_end) {
MEMORY_BASIC_INFORMATION info {};
if (VirtualQuery(reinterpret_cast<const void*>(next_addr), &info, sizeof(info)) == 0) {
EXIT("failed to inspect guest address space at 0x%016" PRIx64 "\n", next_addr);
}
const auto region_base = reinterpret_cast<uint64_t>(info.BaseAddress);
const auto region_end =
std::min<uint64_t>(guest_end, region_base + static_cast<uint64_t>(info.RegionSize));
const auto reserve_start =
AlignUp(std::max<uint64_t>(region_base, HOST_SYSTEM_MANAGED_MIN), m_granularity);
const auto reserve_end = AlignDown(region_end, m_granularity);
if (info.State == MEM_FREE && reserve_end > reserve_start &&
reserve_end - reserve_start > MinimumRegionSize) {
auto* ptr = VirtualAlloc2(
process, reinterpret_cast<void*>(reserve_start), reserve_end - reserve_start,
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0);
if (reinterpret_cast<uint64_t>(ptr) != reserve_start) {
EXIT("failed to reserve guest address space at 0x%016" PRIx64
", size 0x%016" PRIx64 "\n",
reserve_start, reserve_end - reserve_start);
for (const auto& [guest_start, guest_end]: regions) {
auto next_addr = guest_start;
while (next_addr < guest_end) {
MEMORY_BASIC_INFORMATION info {};
if (VirtualQuery(reinterpret_cast<const void*>(next_addr), &info, sizeof(info)) == 0) {
EXIT("failed to inspect guest address space at 0x%016" PRIx64 "\n", next_addr);
}
AddFreeUnlocked(reserve_start, reserve_end - reserve_start);
m_owned.emplace_back(reserve_start, reserve_end - reserve_start);
}
if (region_end <= next_addr) {
EXIT("guest address-space scan did not advance at 0x%016" PRIx64 "\n", next_addr);
const auto region_base = reinterpret_cast<uint64_t>(info.BaseAddress);
const auto region_end = std::min<uint64_t>(
guest_end, region_base + static_cast<uint64_t>(info.RegionSize));
const auto reserve_start =
AlignUp(std::max(region_base, guest_start), m_granularity);
const auto reserve_end = AlignDown(region_end, m_granularity);
if (info.State == MEM_FREE && reserve_end > reserve_start &&
reserve_end - reserve_start > MinimumRegionSize) {
auto* ptr = VirtualAlloc2(
process, reinterpret_cast<void*>(reserve_start), reserve_end - reserve_start,
MEM_RESERVE | MEM_RESERVE_PLACEHOLDER, PAGE_NOACCESS, nullptr, 0);
if (reinterpret_cast<uint64_t>(ptr) != reserve_start) {
EXIT("failed to reserve guest address space at 0x%016" PRIx64
", size 0x%016" PRIx64 "\n",
reserve_start, reserve_end - reserve_start);
}
AddFreeUnlocked(reserve_start, reserve_end - reserve_start);
m_owned.emplace_back(reserve_start, reserve_end - reserve_start);
}
if (region_end <= next_addr) {
EXIT("guest address-space scan did not advance at 0x%016" PRIx64 "\n", next_addr);
}
next_addr = region_end;
}
next_addr = region_end;
}
#else
const std::array<std::pair<uint64_t, uint64_t>, 3> regions {{
{HOST_SYSTEM_MANAGED_MIN, HOST_SYSTEM_MANAGED_MAX + 1u},
{HOST_SYSTEM_RESERVED_MIN, HOST_SYSTEM_RESERVED_MAX + 1u},
{HOST_USER_MIN, HOST_USER_MAX + 1u},
}};
#if KYTY_PLATFORM == KYTY_PLATFORM_LINUX && !defined(__APPLE__)
std::vector<std::pair<uint64_t, uint64_t>> occupied;
FILE* maps = fopen("/proc/self/maps", "r");
+7
View File
@@ -3,6 +3,8 @@
#include <QNetworkAccessManager>
#include <QObject>
#include <QString>
#include <QUrl>
class QByteArray;
class QWidget;
@@ -29,6 +31,11 @@ private:
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;
};
#endif // UPDATE_CHECKER_H
@@ -440,6 +440,14 @@ void ConfigurationEditDialog::add_game_directory() {
start_dir = m_game_dirs_list->item(m_game_dirs_list->count() - 1)->text();
}
#if defined(__APPLE__)
// Use the native macOS picker to browse mounted volumes.
const auto dir = QFileDialog::getExistingDirectory(this, tr("Select game folder"), start_dir);
if (dir.isEmpty()) {
return;
}
AddGameDirectoryItem(dir);
#else
QFileDialog dialog(this, tr("Select game folders"), start_dir);
dialog.setFileMode(QFileDialog::Directory);
dialog.setOption(QFileDialog::ShowDirsOnly, true);
@@ -462,6 +470,7 @@ void ConfigurationEditDialog::add_game_directory() {
for (const auto& dir: dialog.selectedFiles()) {
AddGameDirectoryItem(dir);
}
#endif
update_game_directory_buttons();
}
+22 -4
View File
@@ -84,12 +84,30 @@ void UpdateChecker::FetchUpdateInfo(const char* url, bool fallback, bool manual)
}
reply->deleteLater();
if (!fallback &&
(!info.error.isEmpty() || info.tag != QString::fromLatin1(KYTY_RELEASE_TAG))) {
FetchUpdateInfo(FALLBACK_FEED_URL, true, manual);
return;
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);
+2 -141
View File
@@ -5,6 +5,7 @@
#include "libs/ajm/atrac9_decoder.h"
#include "libs/ajm/decoder.h"
#include "libs/ajm/mp3_decoder.h"
#include "libs/ajm/opus_decoder.h"
#include "libs/audio.h"
#include "libs/errno.h"
#include "libs/libs.h"
@@ -75,16 +76,6 @@ struct AjmGetStatisticsResult {
uint32_t memory[6];
};
struct AjmDecOpusInitializeParameters {
uint32_t channel_num;
uint32_t sample_rate;
uint32_t mapping_family;
};
struct AjmSidebandDecOpusCodecInfo {
uint32_t frames_per_packet;
};
enum class AjmCodec : uint32_t {
DecMp3 = 0,
DecAt9 = 1,
@@ -120,8 +111,6 @@ constexpr uint64_t AJM_FLAG_SIDEBAND_STREAM = 1ull << 47u;
constexpr uint64_t AJM_INSTANCE_FLAG_FORMAT_OFFSET = 7u;
constexpr uint64_t AJM_INSTANCE_FLAG_FORMAT_MASK = 0x7u;
constexpr uint64_t AJM_INSTANCE_FLAG_MAX_CHANNEL_MASK = 0x7fu;
constexpr uint32_t AJM_DEC_OPUS_MAX_CHANNELS_FOR_10CH = 10;
constexpr uint32_t AJM_DEC_OPUS_FRAME_SAMPLES = 960;
static std::atomic_uint32_t g_ajm_next_instance {1};
static std::atomic_uint32_t g_ajm_next_batch {1};
@@ -196,134 +185,6 @@ int KYTY_SYSV_ABI AjmDecAt9ParseConfigData(const void* config_data,
return OK;
}
class AjmOpusDecoder final: public AjmDecoder {
public:
AjmOpusDecoder(uint32_t channels, uint32_t sample_rate, AjmSampleEncoding encoding,
uint64_t flags)
: AjmDecoder(channels, sample_rate, encoding) {
(void)flags;
}
AjmDecodeResult Initialize(const void* codec_parameters,
size_t codec_parameters_size) override {
auto result = MakeResult();
if (codec_parameters == nullptr ||
codec_parameters_size < sizeof(AjmDecOpusInitializeParameters)) {
result.result = AJM_RESULT_INVALID_PARAMETER;
return result;
}
const auto* params = static_cast<const AjmDecOpusInitializeParameters*>(codec_parameters);
if (params->channel_num == 0 || params->channel_num > AJM_DEC_OPUS_MAX_CHANNELS_FOR_10CH ||
params->sample_rate == 0) {
result.result = AJM_RESULT_INVALID_PARAMETER;
return result;
}
SetFormat(params->channel_num, params->sample_rate, m_sample_encoding);
m_mapping_family = params->mapping_family;
m_total_decoded_samples = 0;
m_is_initialized = true;
m_frames_per_packet = 1;
LOGF("AJM Opus initialized: %" PRIu32 " Hz, %" PRIu32 " ch, mapping=%" PRIu32 "\n",
params->sample_rate, params->channel_num, params->mapping_family);
return MakeResult();
}
void Reset() override { m_total_decoded_samples = 0; }
AjmDecodeResult Decode(const void* input, size_t input_size, void* output, size_t output_size,
bool multiple_frames, AjmGaplessState* gapless) override {
(void)multiple_frames;
auto result = MakeResult();
if (input == nullptr || input_size == 0) {
result.result = AJM_RESULT_PARTIAL_INPUT;
return result;
}
if (!m_is_initialized) {
result.result = AJM_RESULT_NOT_INITIALIZED;
return result;
}
auto samples_to_write = AJM_DEC_OPUS_FRAME_SAMPLES;
auto skip_samples = 0u;
if (gapless != nullptr) {
skip_samples = std::min<uint32_t>(samples_to_write, gapless->current.skip_samples);
samples_to_write -= skip_samples;
if (gapless->HasSampleLimit()) {
samples_to_write =
std::min<uint32_t>(samples_to_write, gapless->current.total_samples);
}
}
const auto output_bytes = static_cast<size_t>(samples_to_write) *
static_cast<size_t>(m_channels) *
AjmBytesPerSample(m_sample_encoding);
if (output_bytes != 0 && output == nullptr) {
result.result = AJM_RESULT_NOT_ENOUGH_ROOM;
return result;
}
if (output_bytes > output_size) {
result.result = AJM_RESULT_NOT_ENOUGH_ROOM;
return result;
}
if (output_bytes != 0) {
std::memset(output, 0, output_bytes);
}
if (gapless != nullptr) {
gapless->current.skip_samples =
static_cast<uint16_t>(gapless->current.skip_samples - skip_samples);
if (gapless->HasSampleLimit()) {
gapless->current.total_samples =
(gapless->current.total_samples > samples_to_write
? gapless->current.total_samples - samples_to_write
: 0);
}
const auto skipped_samples = AJM_DEC_OPUS_FRAME_SAMPLES - samples_to_write;
const auto skipped_total = std::min<uint32_t>(
std::numeric_limits<uint16_t>::max(),
static_cast<uint32_t>(gapless->current.skipped_samples) + skipped_samples);
gapless->current.skipped_samples = static_cast<uint16_t>(skipped_total);
}
result.input_consumed = input_size;
result.output_written = output_bytes;
result.frames = 1;
result.frames_per_packet = m_frames_per_packet;
m_total_decoded_samples += samples_to_write;
result.total_decoded_samples = m_total_decoded_samples;
result.format = GetFormat();
return result;
}
void WriteCodecInfo(void* output, size_t output_size,
const AjmDecodeResult& result) const override {
(void)result;
if (output == nullptr || output_size < sizeof(AjmSidebandDecOpusCodecInfo)) {
return;
}
auto* info = static_cast<AjmSidebandDecOpusCodecInfo*>(output);
info->frames_per_packet = m_frames_per_packet;
}
[[nodiscard]] size_t CodecInfoSize() const override {
return sizeof(AjmSidebandDecOpusCodecInfo);
}
private:
bool m_is_initialized = false;
uint32_t m_mapping_family = 0;
uint32_t m_frames_per_packet = 1;
};
struct AjmInstanceState {
uint32_t context = 0;
uint32_t codec = 0;
@@ -376,7 +237,7 @@ static std::unique_ptr<AjmDecoder> AjmCreateDecoder(uint32_t codec, uint64_t fla
case static_cast<uint32_t>(AjmCodec::DecM4aac):
return std::make_unique<AjmAacDecoder>(channels, 48000, encoding, flags);
case static_cast<uint32_t>(AjmCodec::DecOpus):
return std::make_unique<AjmOpusDecoder>(channels, 48000, encoding, flags);
return std::make_unique<AjmOpusDecoder>(channels, encoding);
default: break;
}
+216
View File
@@ -0,0 +1,216 @@
#pragma once
#include "libs/ajm/decoder.h"
#include "libs/ajm/ffmpeg_decoder_common.h"
#include <algorithm>
#include <cstring>
namespace Libs::Audio::Ajm {
struct AjmDecOpusInitializeParameters {
uint32_t channel_num;
uint32_t sample_rate;
uint32_t mapping_family;
};
struct AjmSidebandDecOpusCodecInfo {
uint32_t frames_per_packet;
};
static_assert(sizeof(AjmDecOpusInitializeParameters) == 12);
static_assert(sizeof(AjmSidebandDecOpusCodecInfo) == 4);
class AjmOpusDecoder final: public AjmDecoder {
public:
AjmOpusDecoder(uint32_t max_channels, AjmSampleEncoding encoding)
: AjmDecoder(0, 48000, encoding), m_max_channels(max_channels) {}
~AjmOpusDecoder() override { avcodec_free_context(&m_context); }
AjmDecodeResult Initialize(const void* parameters, size_t parameters_size) override {
avcodec_free_context(&m_context);
Reset();
auto result = MakeResult();
if (parameters == nullptr || parameters_size < sizeof(AjmDecOpusInitializeParameters)) {
result.result = AJM_RESULT_INVALID_PARAMETER | AJM_RESULT_FATAL;
return result;
}
const auto& params = *static_cast<const AjmDecOpusInitializeParameters*>(parameters);
if (params.channel_num == 0 || params.channel_num > 2 || params.sample_rate != 48000 ||
params.mapping_family != 0) {
result.result = AJM_RESULT_INVALID_PARAMETER | AJM_RESULT_FATAL;
return result;
}
if (params.channel_num > m_max_channels) {
result.result = AJM_RESULT_TOO_MANY_CHANNELS | AJM_RESULT_FATAL;
return result;
}
const auto* codec = avcodec_find_decoder(AV_CODEC_ID_OPUS);
m_context = codec != nullptr ? avcodec_alloc_context3(codec) : nullptr;
if (m_context != nullptr) {
av_channel_layout_default(&m_context->ch_layout, static_cast<int>(params.channel_num));
m_context->sample_rate = static_cast<int>(params.sample_rate);
if (avcodec_open2(m_context, codec, nullptr) < 0) {
avcodec_free_context(&m_context);
}
}
if (m_context == nullptr) {
result.result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_FATAL;
return result;
}
SetFormat(params.channel_num, params.sample_rate, m_sample_encoding);
return MakeResult();
}
void Reset() override {
if (m_context != nullptr) {
avcodec_flush_buffers(m_context);
}
m_total_decoded_samples = 0;
m_frames_per_packet = 0;
}
AjmDecodeResult Decode(const void* input, size_t input_size, void* output, size_t output_size,
bool multiple_frames, AjmGaplessState* gapless) override {
auto result = MakeResult();
if (m_context == nullptr) {
result.result = AJM_RESULT_NOT_INITIALIZED;
return result;
}
if (input == nullptr || input_size == 0) {
result.result = AJM_RESULT_PARTIAL_INPUT;
return result;
}
while (gapless == nullptr || !gapless->IsEnd()) {
const auto* data = static_cast<const uint8_t*>(input) + result.input_consumed;
const auto remaining = input_size - result.input_consumed;
if (remaining < 2) {
result.result = AJM_RESULT_PARTIAL_INPUT;
break;
}
const auto packet_size = static_cast<uint32_t>(data[0]) | (uint32_t {data[1]} << 8u);
if (packet_size > remaining - 2) {
result.result = AJM_RESULT_PARTIAL_INPUT;
break;
}
data += 2;
if (packet_size == 0 || ((data[0] & 3u) == 3 && packet_size < 2)) {
result.result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA;
result.internal_result = 0x1043;
break;
}
const uint32_t config = data[0] >> 3u;
const uint32_t frames = (data[0] & 3u) == 0 ? 1
: (data[0] & 3u) == 3 ? data[1] & 0x3fu
: 2;
const uint32_t frame_samples = config >= 16 ? 120u << (config & 3u)
: config >= 12 ? 480u << (config & 1u)
: (config & 3u) == 3 ? 2880u
: 480u << (config & 3u);
const uint32_t samples = frame_samples * frames;
if (frames == 0 || frames > 6 || frame_samples > 960) {
result.result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA;
result.internal_result = static_cast<int32_t>(0xffff8800u);
break;
}
const auto skip = gapless != nullptr
? std::min(samples, uint32_t {gapless->current.skip_samples})
: 0;
const auto write = gapless != nullptr && gapless->HasSampleLimit()
? std::min(samples - skip, gapless->current.total_samples)
: samples - skip;
const auto sample_bytes = m_channels * AjmBytesPerSample(m_sample_encoding);
const auto output_bytes = write * sample_bytes;
if (output_bytes > output_size - result.output_written ||
(output_bytes != 0 && output == nullptr)) {
result.result = AJM_RESULT_NOT_ENOUGH_ROOM;
break;
}
AVFrame* frame = DecodePacket(data, packet_size, &result);
if (frame == nullptr) {
break;
}
if (frame->nb_samples != static_cast<int>(samples) ||
frame->ch_layout.nb_channels != static_cast<int>(m_channels)) {
av_frame_free(&frame);
result.result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA;
result.internal_result = 0x1043;
break;
}
if (output_bytes != 0) {
std::memcpy(static_cast<uint8_t*>(output) + result.output_written,
frame->data[0] + skip * sample_bytes, output_bytes);
}
av_frame_free(&frame);
if (gapless != nullptr) {
gapless->current.skip_samples -= static_cast<uint16_t>(skip);
gapless->current.skipped_samples = static_cast<uint16_t>(
std::min<uint32_t>(std::numeric_limits<uint16_t>::max(),
gapless->current.skipped_samples + samples - write));
if (gapless->HasSampleLimit()) {
gapless->current.total_samples -= write;
}
}
result.input_consumed += packet_size + 2;
result.output_written += output_bytes;
result.frames += frames;
m_frames_per_packet = frames;
m_total_decoded_samples += write;
if (!multiple_frames) {
break;
}
}
result.frames_per_packet = m_frames_per_packet;
result.total_decoded_samples = m_total_decoded_samples;
return result;
}
void WriteCodecInfo(void* output, size_t size, const AjmDecodeResult&) const override {
if (output != nullptr && size >= sizeof(AjmSidebandDecOpusCodecInfo)) {
static_cast<AjmSidebandDecOpusCodecInfo*>(output)->frames_per_packet =
m_frames_per_packet;
}
}
[[nodiscard]] size_t CodecInfoSize() const override {
return sizeof(AjmSidebandDecOpusCodecInfo);
}
private:
AVFrame* DecodePacket(const uint8_t* data, uint32_t size, AjmDecodeResult* result) {
AVPacket* packet = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
if (packet == nullptr || frame == nullptr ||
av_new_packet(packet, static_cast<int>(size)) < 0) {
av_packet_free(&packet);
av_frame_free(&frame);
result->result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_FATAL;
return nullptr;
}
std::memcpy(packet->data, data, size);
int rc = avcodec_send_packet(m_context, packet);
av_packet_free(&packet);
if (rc >= 0) {
rc = avcodec_receive_frame(m_context, frame);
}
if (rc < 0) {
av_frame_free(&frame);
result->result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA;
result->internal_result = 0x1043;
return nullptr;
}
AVFrame* converted = AjmConvertFfmpegFrame(frame, m_sample_encoding, result);
av_frame_free(&frame);
return converted;
}
AVCodecContext* m_context = nullptr;
const uint32_t m_max_channels;
uint32_t m_frames_per_packet = 0;
};
}
+18 -3
View File
@@ -7,6 +7,8 @@
#include "common/threads.h"
#include "kernel/pthread.h"
#include "libs/audio_internal.h"
#include "libs/controller.h"
#include "libs/dualSenseHaptics.h"
#include "libs/errno.h"
#include "libs/libs.h"
@@ -105,7 +107,8 @@ private:
int channels_num = 0;
int volume[12] = {};
SDL_AudioStream* stream = nullptr;
SDL_AudioStream* stream = nullptr;
Controller::DualSenseHaptics::Stream* haptics = nullptr;
};
struct PortIn {
@@ -268,6 +271,8 @@ bool Audio::OpenSdlDevice(PortOut* port) {
void Audio::CloseSdlDevice(PortOut* port) {
EXIT_IF(port == nullptr);
Controller::DualSenseHaptics::Close(port->haptics);
port->haptics = nullptr;
if (port->stream != nullptr) {
SDL_DestroyAudioStream(port->stream);
@@ -435,7 +440,9 @@ Audio::Id Audio::AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, For
port.volume[i] = 32768;
}
if (type != AUDIO_OUT_PORT_TYPE_VIBRATION) {
if (type == AUDIO_OUT_PORT_TYPE_VIBRATION) {
port.haptics = Controller::DualSenseHaptics::Open(freq);
} else {
OpenSdlDevice(&port);
}
@@ -547,7 +554,15 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
for (uint32_t i = 0; i < num; i++) {
auto& port = m_out_ports[params[i].handle.GetId()];
QueueSdlAudio(&port, params[i].data, blocking);
if (port.type == AUDIO_OUT_PORT_TYPE_VIBRATION) {
// Haptics never pace output; keep the stream alive against close and volume changes.
Common::LockGuard lock(m_mutex);
Controller::DualSenseHaptics::Queue(
port.haptics, Controller::GetActiveControllerId(), params[i].data, port.samples_num,
static_cast<uint32_t>(port.channels_num), FormatIsFloat(port.format), port.volume);
} else {
QueueSdlAudio(&port, params[i].data, blocking);
}
}
for (uint32_t i = 0; i < num; i++) {
+10 -11
View File
@@ -950,11 +950,8 @@ public:
if (deliver_seek_frame || sync_mode != 0) {
return true;
}
if (audio_id) {
return candidate.info.time_stamp <= last_audio_ts;
}
auto now = CurrentTimeNoLock();
return now == 0 || candidate.info.time_stamp <= now;
return now == 0 || candidate.info.time_stamp + candidate.timestamp_offset <= now;
});
if (!frame) {
return false;
@@ -999,7 +996,9 @@ public:
out->details.audio.size = current_audio->info.details.audio.size;
std::memcpy(out->details.audio.language_code,
current_audio->info.details.audio.language_code, 4);
last_audio_ts = out->time_stamp;
start_time_ms = current_audio->info.time_stamp + current_audio->timestamp_offset;
clock_start = std::chrono::steady_clock::now();
paused_extra = {};
RecordLoopBoundary(*current_audio);
return true;
}
@@ -1064,7 +1063,6 @@ private:
video_done = true;
audio_done = true;
seek_video_frame_pending = false;
last_audio_ts = 0;
last_output_loop_offset = 0;
pending_loop_warnings = 0;
}
@@ -1397,7 +1395,6 @@ private:
av_frame_free(&frame);
return false;
}
ready.info.time_stamp += timestamp_offset;
ready.timestamp_offset = timestamp_offset;
frames.Push(std::move(ready));
av_frame_free(&frame);
@@ -1541,11 +1538,14 @@ private:
return false;
}
auto* dst = buffer->Get();
std::memset(dst, 0, static_cast<size_t>(size));
auto* c = dst + pitch * h;
// PPSA02433 samples the padded columns; zero chroma would turn them green.
std::memset(dst, s->codecpar->color_range == AVCOL_RANGE_JPEG ? 0 : 16,
static_cast<size_t>(pitch) * h);
std::memset(c, 128, static_cast<size_t>(pitch) * h / 2);
for (int y = 0; y < src->height; y++) {
std::memcpy(dst + y * pitch, nv12->data[0] + y * nv12->linesize[0], src->width);
}
auto* c = dst + pitch * h;
for (int y = 0; y < src->height / 2; y++) {
std::memcpy(c + y * pitch, nv12->data[1] + y * nv12->linesize[1], src->width);
}
@@ -1674,7 +1674,6 @@ private:
int32_t trick_speed = AVPLAYER_TRICK_SPEED_NORMAL;
uint32_t sync_mode = 0;
uint64_t start_time_ms = 0;
uint64_t last_audio_ts = 0;
uint64_t last_output_loop_offset = 0;
uint32_t pending_loop_warnings = 0;
std::chrono::steady_clock::time_point clock_start {};
@@ -1719,7 +1718,7 @@ static AvPlayerInternal* create_player(const AvPlayerMemAllocator& mem,
h->mem = mem;
h->file = file;
h->event = event;
h->auto_start = auto_start;
h->auto_start = auto_start || event.event_callback == nullptr;
h->video_buffers = std::clamp(video_buffers <= 0 ? 2 : video_buffers, 2, 16);
h->post_init.demux_video_buffer_size = 4 * 1024 * 1024;
return h;
+15
View File
@@ -8,6 +8,7 @@
#include "common/stringUtils.h"
#include "common/threads.h"
#include "kernel/pthread.h"
#include "libs/dualSenseHaptics.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "libs/padData.h"
@@ -113,6 +114,7 @@ public:
void ReleaseHostPads();
void GetConnectionInfo(bool* flag, int* count);
void SetVibration(uint8_t large_motor, uint8_t small_motor);
int GetActiveControllerId();
void SetLightBar(uint8_t r, uint8_t g, uint8_t b);
bool SetTriggerEffect(const PadTriggerEffectParam& param);
void ReadState(ControllerState* state, bool* flag, int* count);
@@ -518,6 +520,7 @@ void GameController::ResetInputState() {
void GameController::ReleaseHostPads() {
Common::LockGuard lock(m_mutex);
DualSenseHaptics::Shutdown();
std::vector<SDL_Gamepad*> pads;
for (const auto id: m_connected_ids) {
@@ -551,6 +554,9 @@ void GameController::ReleaseHostPads() {
void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
Common::LockGuard lock(m_mutex);
if (DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor)) {
return;
}
if (m_active_id == HOST_INPUT_CONTROLLER_ID) {
return;
@@ -568,6 +574,11 @@ void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
}
}
int GameController::GetActiveControllerId() {
Common::LockGuard lock(m_mutex);
return m_active_id;
}
void GameController::SetLightBar(uint8_t r, uint8_t g, uint8_t b) {
Common::LockGuard lock(m_mutex);
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(m_active_id));
@@ -692,6 +703,10 @@ void ResetInputState() {
g_controller->ResetInputState();
}
int GetActiveControllerId() {
return g_controller != nullptr ? g_controller->GetActiveControllerId() : -1;
}
int KYTY_SYSV_ABI PadInit() {
PRINT_NAME();
+1
View File
@@ -73,6 +73,7 @@ void SetRightStick(int id, int x, int y);
void SetTouchPad(int id, int finger, bool down, float x, float y);
void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us);
void ResetInputState();
int GetActiveControllerId();
int KYTY_SYSV_ABI PadInit();
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param);
+205
View File
@@ -0,0 +1,205 @@
#include "libs/dualSenseHaptics.h"
#include "common/logging/log.h"
#include "common/threads.h"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cmath>
#include <vector>
namespace Libs::Controller::DualSenseHaptics {
struct Stream {
uint32_t freq = 0;
SDL_AudioStream* sdl = nullptr;
SDL_AudioDeviceID device = 0;
uint64_t next_check = 0;
std::vector<float> buffer;
};
namespace {
constexpr int FRAME_BYTES = 4 * sizeof(float);
Common::Mutex g_mutex;
int g_controller = -1;
int g_playback_streams = 0;
uint8_t g_large_motor = 0, g_small_motor = 0;
uint64_t g_rumble_until = 0, g_haptics_until = 0;
void ApplyRumble() { // Caller holds g_mutex; no calls into Audio or Controller.
SDL_LockJoysticks();
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(g_controller));
pad != nullptr) {
const auto now = SDL_GetTicks();
const bool muted = g_haptics_until > now || g_rumble_until <= now;
(void)SDL_RumbleGamepad(
pad, muted ? 0 : g_large_motor * 0x101U, muted ? 0 : g_small_motor * 0x101U,
g_rumble_until > now ? static_cast<uint32_t>(g_rumble_until - now) : 0);
}
SDL_UnlockJoysticks();
}
void SelectController(int controller) {
if (g_controller != controller) {
g_haptics_until = 0;
ApplyRumble();
g_controller = controller;
g_large_motor = g_small_motor = 0;
g_rumble_until = 0;
}
}
void SDLCALL UpdateRumble(void*, SDL_AudioStream*, int, int) {
Common::LockGuard lock(g_mutex);
if (g_haptics_until != 0 && SDL_GetTicks() >= g_haptics_until) {
g_haptics_until = 0;
ApplyRumble();
}
}
void CloseDevice(Stream* stream) {
if (stream->sdl != nullptr) {
// Destroy outside g_mutex: SDL waits for any running get callback.
SDL_DestroyAudioStream(stream->sdl);
stream->sdl = nullptr;
Common::LockGuard lock(g_mutex);
if (--g_playback_streams == 0 && g_haptics_until != 0) {
g_haptics_until = 0;
ApplyRumble();
}
}
}
SDL_AudioDeviceID FindDevice() {
int count = 0;
auto* devices = SDL_GetAudioPlaybackDevices(&count);
SDL_AudioDeviceID device = 0;
for (int i = 0; i < count && device == 0; i++) {
const char* name = SDL_GetAudioDeviceName(devices[i]);
SDL_AudioSpec spec {};
if (name != nullptr && SDL_strcasestr(name, "DualSense") != nullptr &&
SDL_GetAudioDeviceFormat(devices[i], &spec, nullptr) && spec.channels == 4) {
device = devices[i];
}
}
SDL_free(devices);
return device;
}
} // namespace
Stream* Open(uint32_t freq) {
if (freq == 0 || !SDL_InitSubSystem(SDL_INIT_AUDIO)) {
return nullptr;
}
return new Stream {.freq = freq};
}
void Close(Stream* stream) {
if (stream != nullptr) {
CloseDevice(stream);
delete stream;
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
}
void Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume) {
if (stream == nullptr || data == nullptr || frames == 0 || channels == 0 || volume == nullptr) {
return;
}
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) != SDL_GAMEPAD_TYPE_PS5) {
CloseDevice(stream);
return;
}
const auto now = SDL_GetTicks();
if (now >= stream->next_check) {
stream->next_check = now + 2000;
const auto device = FindDevice();
if (stream->device != device || stream->sdl == nullptr) {
CloseDevice(stream);
stream->device = device;
if (device != 0) {
const SDL_AudioSpec desired {SDL_AUDIO_F32, 4, static_cast<int>(stream->freq)};
auto* sdl = SDL_OpenAudioDeviceStream(device, &desired, UpdateRumble, nullptr);
SDL_AudioSpec actual {};
if (sdl != nullptr &&
SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl), &actual, nullptr) &&
actual.channels == 4 && SDL_ResumeAudioStreamDevice(sdl)) {
stream->sdl = sdl;
{
Common::LockGuard lock(g_mutex);
g_playback_streams++;
}
LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device));
} else {
SDL_DestroyAudioStream(sdl);
LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError());
}
}
}
}
if (stream->sdl == nullptr) {
return;
}
stream->buffer.assign(static_cast<size_t>(frames) * 4, 0.0f);
bool audible = false;
for (uint32_t frame = 0; frame < frames; frame++) {
for (uint32_t ch = 0; ch < 2; ch++) {
const auto src_ch = channels == 1 ? 0 : ch;
const auto index = static_cast<size_t>(frame) * channels + src_ch;
float value = is_float ? static_cast<const float*>(data)[index]
: static_cast<const int16_t*>(data)[index] / 32768.0f;
value *= volume[src_ch] / 32768.0f;
stream->buffer[static_cast<size_t>(frame) * 4 + ch + 2] = value;
audible |= std::fabs(value) > 1.0f / 1024;
}
}
auto queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl));
if (queued > static_cast<int>(stream->freq * FRAME_BYTES * 80 / 1000)) {
SDL_ClearAudioStream(stream->sdl);
queued = 0;
}
const int bytes = static_cast<int>(frames * FRAME_BYTES);
if (audible) {
Common::LockGuard lock(g_mutex);
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) !=
SDL_GAMEPAD_TYPE_PS5) {
return;
}
SelectController(controller);
const auto playback_now = SDL_GetTicks();
const bool starting = g_haptics_until <= playback_now;
const auto duration = (queued + bytes) * 1000ULL / (stream->freq * FRAME_BYTES);
g_haptics_until =
std::max(g_haptics_until, playback_now + std::max<uint64_t>(duration, 250));
if (starting) {
ApplyRumble();
}
}
if (!SDL_PutAudioStreamData(stream->sdl, stream->buffer.data(), bytes)) {
CloseDevice(stream);
}
}
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) {
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) != SDL_GAMEPAD_TYPE_PS5) {
return false;
}
Common::LockGuard lock(g_mutex);
SelectController(controller);
g_large_motor = large_motor;
g_small_motor = small_motor;
g_rumble_until = SDL_GetTicks() + 0xffff;
ApplyRumble();
return true;
}
void Shutdown() {
Common::LockGuard lock(g_mutex);
g_rumble_until = 0;
SelectController(-1);
}
} // namespace Libs::Controller::DualSenseHaptics
+20
View File
@@ -0,0 +1,20 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_
#define EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_
#include <cstdint>
namespace Libs::Controller::DualSenseHaptics {
struct Stream;
Stream* Open(uint32_t freq);
void Close(Stream* stream);
void Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume);
// Returns false for other gamepad types, which retain the normal rumble path.
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor);
void Shutdown();
} // namespace Libs::Controller::DualSenseHaptics
#endif // EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_
+15 -71
View File
@@ -685,8 +685,6 @@ constexpr int PROT_AMPR_READ = 0x40;
constexpr int PROT_AMPR_WRITE = 0x80;
constexpr int PROT_ACP_READ = 0x100;
constexpr int PROT_ACP_WRITE = 0x200;
constexpr uint64_t AMM_VA_START = 0x0000001000000000ull;
constexpr uint64_t AMM_VA_SIZE = 0x0000001000000000ull;
constexpr uint64_t APR_MAX_READ_LENGTH = 0x0000000100000000ull;
constexpr uint64_t APR_MAX_FILE_OFFSET = 0x0000010000000000ull;
constexpr uint64_t APR_MAX_APP_ADDRESS = 0x0000f00000000000ull;
@@ -744,19 +742,6 @@ struct CommandBufferState {
uint64_t gather_scatter_file_offset = 0;
};
struct AmmAutoPoolRange {
uint64_t start = 0;
uint64_t size = 0;
uint64_t used = 0;
};
struct AmmVirtualAddressRanges {
uint64_t va_start = 0;
uint64_t va_end = 0;
uint64_t multimap_va_start = 0;
uint64_t multimap_va_end = 0;
};
struct AmmUsageStatsData {
uint64_t size_in_bytes = 0;
uint16_t num_page_table_pool_entries = 0;
@@ -770,8 +755,6 @@ static_assert(sizeof(AmmUsageStatsData) == 0x18);
static std::mutex g_command_buffer_mutex;
static std::unordered_map<uint64_t, CommandBufferState> g_command_buffers;
static std::unordered_map<uint64_t, uint64_t> g_command_buffer_aliases;
static std::mutex g_amm_auto_pool_mutex;
static std::vector<AmmAutoPoolRange> g_amm_auto_pool;
using CommandBufferIterator = std::unordered_map<uint64_t, CommandBufferState>::iterator;
static bool HasQueuedCommands(const CommandBufferState& state) {
@@ -1227,48 +1210,20 @@ static int NormalizeAmmProtection(int prot) {
return normalized;
}
static bool AllocateAmmAutoDirectMemory(uint64_t size, int memory_type, uint64_t* dmem_offset) {
if (dmem_offset == nullptr || size == 0) {
return false;
}
std::scoped_lock lock(g_amm_auto_pool_mutex);
for (auto& range: g_amm_auto_pool) {
const auto aligned_used = (range.used + (AMM_PAGE_SIZE - 1u)) & ~(AMM_PAGE_SIZE - 1u);
if (aligned_used <= range.size && size <= range.size - aligned_used) {
*dmem_offset = range.start + aligned_used;
range.used = aligned_used + size;
return true;
}
}
int64_t allocated = 0;
if (LibKernel::Memory::KernelAllocateDirectMemory(
0, LibKernel::Memory::KernelGetDirectMemorySize(), size, AMM_PAGE_SIZE, memory_type,
&allocated) != OK) {
return false;
}
*dmem_offset = static_cast<uint64_t>(allocated);
return true;
}
static int ExecuteAmmMapCommand(const CommandBufferState::AmmMapCommand& command) {
if (!ValidateAmmMapArgs(command.va, command.size)) {
return LibKernel::KERNEL_ERROR_EINVAL;
}
uint64_t dmem_offset = command.dmem_offset;
if (command.kind == AmmCommandKind::MapAuto &&
!AllocateAmmAutoDirectMemory(command.size, command.type, &dmem_offset)) {
return LibKernel::KERNEL_ERROR_EAGAIN;
if (command.kind == AmmCommandKind::MapAuto) {
return LibKernel::Memory::MapAutomaticMemory(command.va, command.size, command.type,
NormalizeAmmProtection(command.prot));
}
void* addr = reinterpret_cast<void*>(command.va);
return LibKernel::Memory::KernelMapDirectMemory2(
&addr, command.size, command.type, NormalizeAmmProtection(command.prot), AMM_MAP_FIXED,
static_cast<int64_t>(dmem_offset), AMM_PAGE_SIZE);
static_cast<int64_t>(command.dmem_offset), AMM_PAGE_SIZE);
}
static bool AppendAmmMapRecord(uint64_t command_buffer,
@@ -2360,25 +2315,15 @@ static int KYTY_SYSV_ABI AmmGiveDirectMemory(int64_t search_start, int64_t searc
int64_t* dmem_offset) {
PRINT_NAME();
if (dmem_offset == nullptr || size == 0 ||
(usage != AMM_USAGE_DIRECT && usage != AMM_USAGE_AUTO)) {
constexpr uint64_t block_size = 0x200000;
if (dmem_offset == nullptr || size == 0 || (size & (block_size - 1)) != 0 ||
(align & (block_size - 1)) != 0 || (usage != AMM_USAGE_DIRECT && usage != AMM_USAGE_AUTO)) {
return LibKernel::KERNEL_ERROR_EINVAL;
}
int64_t allocated = 0;
int result = LibKernel::Memory::KernelAllocateDirectMemory(search_start, search_end, size,
align, 0, &allocated);
if (result != OK) {
return result;
}
*dmem_offset = allocated;
if (usage == AMM_USAGE_AUTO) {
std::scoped_lock lock(g_amm_auto_pool_mutex);
g_amm_auto_pool.push_back(AmmAutoPoolRange {static_cast<uint64_t>(allocated), size, 0});
}
return OK;
return LibKernel::Memory::AllocateDirectMemory(search_start, search_end, size,
align != 0 ? align : block_size, 0, dmem_offset,
usage == AMM_USAGE_AUTO);
}
static void KYTY_SYSV_ABI AmmGetVirtualAddressRanges(uint64_t* va_start, uint64_t* va_end,
@@ -2386,19 +2331,18 @@ static void KYTY_SYSV_ABI AmmGetVirtualAddressRanges(uint64_t* va_start, uint64_
uint64_t* multimap_va_end) {
PRINT_NAME();
constexpr auto end = LibKernel::Memory::kExtendedMemoryBase + LibKernel::Memory::kExtendedMemorySize;
if (va_start != nullptr) {
AprShared::WriteGuest(reinterpret_cast<uint64_t>(va_start), AMM_VA_START);
AprShared::WriteGuest(reinterpret_cast<uint64_t>(va_start), LibKernel::Memory::kExtendedMemoryBase);
}
if (va_end != nullptr) {
AprShared::WriteGuest(reinterpret_cast<uint64_t>(va_end), AMM_VA_START + AMM_VA_SIZE);
AprShared::WriteGuest(reinterpret_cast<uint64_t>(va_end), end);
}
if (multimap_va_start != nullptr) {
AprShared::WriteGuest(reinterpret_cast<uint64_t>(multimap_va_start),
AMM_VA_START + AMM_VA_SIZE / 2u);
AprShared::WriteGuest(reinterpret_cast<uint64_t>(multimap_va_start), end);
}
if (multimap_va_end != nullptr) {
AprShared::WriteGuest(reinterpret_cast<uint64_t>(multimap_va_end),
AMM_VA_START + AMM_VA_SIZE);
AprShared::WriteGuest(reinterpret_cast<uint64_t>(multimap_va_end), end);
}
}
+22 -12
View File
@@ -188,7 +188,7 @@ struct AudioOut2PortStateEntry {
uint32_t samples_num = 512;
AudioInternal::Format audio_format = AudioInternal::Format::Unknown;
int audio_handle = 0;
const void* pcm_data = nullptr;
std::vector<uint8_t> pcm_data;
};
struct AudioOut2SpeakerArrayState {
@@ -341,11 +341,17 @@ static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle p
return nullptr;
}
static size_t audioout2_pcm_size(const AudioOut2PortStateEntry& state) {
const auto bytes_per_sample = (state.data_format & 0x7fu) == 1 ? sizeof(int16_t) : sizeof(float);
return static_cast<size_t>(state.samples_num) *
audioout2_data_format_channels(state.data_format) * bytes_per_sample;
}
static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) {
Common::LockGuard lock(g_audioout2_port_mutex);
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
state.pcm_data != nullptr && AudioInternal::AudioOutHasDevice(state.audio_handle)) {
!state.pcm_data.empty() && AudioInternal::AudioOutHasDevice(state.audio_handle)) {
return true;
}
}
@@ -353,24 +359,23 @@ static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) {
}
static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) {
// The backend consumes the buffers synchronously, but can block while pacing SDL's queue.
// Keep both PCM storage and port handles alive until it returns.
std::vector<AudioInternal::OutputParam> params;
params.reserve(AudioInternal::OUT_PORTS_MAX);
g_audioout2_port_mutex.Lock();
Common::LockGuard lock(g_audioout2_port_mutex);
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
state.pcm_data != nullptr && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data});
!state.pcm_data.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data.data()});
}
}
g_audioout2_port_mutex.Unlock();
if (params.empty()) {
return;
if (!params.empty()) {
(void)AudioInternal::AudioOutOutputs(params.data(), static_cast<uint32_t>(params.size()),
blocking);
}
(void)AudioInternal::AudioOutOutputs(params.data(), static_cast<uint32_t>(params.size()),
blocking);
}
static void audioout2_close_audio_handle(int audio_handle) {
@@ -679,7 +684,12 @@ int KYTY_SYSV_ABI AudioOut2PortSetAttributes(AudioOut2PortHandle port,
if (has_pcm) {
g_audioout2_port_mutex.Lock();
if (auto* state = audioout2_find_port_locked(port); state != nullptr) {
state->pcm_data = pcm_data;
if (pcm_data != nullptr && state->audio_format != AudioInternal::Format::Unknown) {
const auto* bytes = static_cast<const uint8_t*>(pcm_data);
state->pcm_data.assign(bytes, bytes + audioout2_pcm_size(*state));
} else {
state->pcm_data.clear();
}
}
g_audioout2_port_mutex.Unlock();
}
+7
View File
@@ -1226,6 +1226,7 @@ static KYTY_SYSV_ABI KernelModule KernelLoadStartModule(const char* module_file_
auto* program = rt->FindProgramByFileName(module_path);
if (program != nullptr) {
++program->load_count;
if (res != nullptr) {
*res = OK;
}
@@ -1240,6 +1241,7 @@ static KYTY_SYSV_ABI KernelModule KernelLoadStartModule(const char* module_file_
rt->RelocateProgram(program);
program->load_count = 1;
int result = rt->StartModule(program, args, argp, nullptr);
LOGF("\tmodule_start() result = %d\n", result);
@@ -1269,6 +1271,11 @@ static int KYTY_SYSV_ABI KernelStopUnloadModule(KernelModule handle, size_t args
if (program == nullptr) {
LOGF("\tinvalid module handle = %" PRId32 "\n", handle);
return KERNEL_ERROR_ESRCH;
}
if (program->load_count > 1) {
--program->load_count;
return OK;
}
+27 -3
View File
@@ -7,9 +7,11 @@
#include "libs/network.h"
#include "loader/symbolDatabase.h"
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <cstring>
#include <limits>
#include <map>
#include <mutex>
#include <string>
@@ -163,6 +165,16 @@ int KYTY_SYSV_ABI NetSetsockopt(int s, int level, int optname, const void* optva
return FinishSocketCall(Net::Setsockopt(s, level, optname, optval, optlen));
}
int KYTY_SYSV_ABI NetSend(int s, const void* buf, size_t len, int flags) {
const auto size = std::min<size_t>(len, std::numeric_limits<int>::max());
return FinishSocketCall(static_cast<int>(Net::Send(s, buf, size, flags | 0x20000)));
}
int KYTY_SYSV_ABI NetRecv(int s, void* buf, size_t len, int flags) {
const auto size = std::min<size_t>(len, std::numeric_limits<int>::max());
return FinishSocketCall(static_cast<int>(Net::Recv(s, buf, size, flags)));
}
uint32_t KYTY_SYSV_ABI NetHtonl(uint32_t host32) {
return ((host32 & 0x000000ffu) << 24u) | ((host32 & 0x0000ff00u) << 8u) |
((host32 & 0x00ff0000u) >> 8u) | ((host32 & 0xff000000u) >> 24u);
@@ -207,6 +219,8 @@ LIB_DEFINE(InitNet_1_Net) {
LIB_FUNC("Q4qBuN-c0ZM", LibNet::NetSocket);
LIB_FUNC("45ggEzakPJQ", LibNet::NetSocketClose);
LIB_FUNC("2mKX2Spso7I", LibNet::NetSetsockopt);
LIB_FUNC("beRjXBn-z+o", LibNet::NetSend);
LIB_FUNC("9wO9XrMsNhc", LibNet::NetRecv);
LIB_FUNC("9T2pDF2Ryqg", LibNet::NetHtonl);
LIB_FUNC("iWQWrwiSt8A", LibNet::NetHtons);
LIB_FUNC("pQGpHYopAIY", LibNet::NetNtohl);
@@ -277,7 +291,7 @@ static char* CopyUriPart(char*& dst, const UriPart& part) {
}
static int ParseEmptyUri(SceHttpUriElement* out, void* pool, size_t* require, size_t prepare) {
constexpr size_t needed = 3;
constexpr size_t needed = 4;
if (require != nullptr) {
*require = needed;
@@ -296,10 +310,12 @@ static int ParseEmptyUri(SceHttpUriElement* out, void* pool, size_t* require, si
auto* dst = static_cast<char*>(pool);
out->scheme = dst++;
out->hostname = dst++;
out->path = dst;
out->path = dst++;
out->query = dst;
out->scheme[0] = '\0';
out->hostname[0] = '\0';
out->path[0] = '\0';
out->query[0] = '\0';
}
return 0;
@@ -460,6 +476,9 @@ static int KYTY_SYSV_ABI HttpUriParse(SceHttpUriElement* out, const char* src_ur
needed += part.len + 1;
}
}
if (query.begin == nullptr) {
needed += 1;
}
if (require != nullptr) {
*require = needed;
@@ -482,7 +501,12 @@ static int KYTY_SYSV_ABI HttpUriParse(SceHttpUriElement* out, const char* src_ur
out->password = CopyUriPart(dst, password);
out->hostname = CopyUriPart(dst, hostname);
out->path = CopyUriPart(dst, path);
out->query = CopyUriPart(dst, query);
if (query.begin == nullptr) {
out->query = dst++;
out->query[0] = '\0';
} else {
out->query = CopyUriPart(dst, query);
}
out->fragment = CopyUriPart(dst, fragment);
}
+2
View File
@@ -15,6 +15,8 @@ LIB_DEFINE(InitVideoOut_1) {
LIB_FUNC("Up36PTk687E", VideoOut::VideoOutOpen);
LIB_FUNC("uquVH4-Du78", VideoOut::VideoOutClose);
LIB_FUNC("WkYtyOg30do", VideoOut::VideoOutSetFlipMaster);
LIB_FUNC("MBlv8YWoyOk", VideoOut::VideoOutAddBufferAttributeOption);
LIB_FUNC("PjS5uASwcV8", VideoOut::VideoOutSetBufferAttribute2);
LIB_FUNC("rKBUtgRrtbk", VideoOut::VideoOutRegisterBuffers2);
LIB_FUNC("HuViW4HnrOw", VideoOut::VideoOutSubmitChangeBufferAttribute2);
+1
View File
@@ -1003,6 +1003,7 @@ static int ConvertHostSocketError(int error) {
case ENOBUFS: posix_error = Posix::POSIX_ENOBUFS; break;
case ENOMEM: posix_error = Posix::POSIX_ENOMEM; break;
case ENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
case EPIPE: posix_error = Posix::POSIX_EPIPE; break;
case EPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
default: break;
}
+601 -168
View File
File diff suppressed because it is too large. Load diff
-47
View File
@@ -7,53 +7,6 @@ namespace Loader::Jit {
#pragma pack(1)
struct JmpWithIndex {
void SetIndex(uint32_t index) { *reinterpret_cast<uint32_t*>(&code[1]) = index; }
void SetFunc(void* handler) {
auto func_addr = reinterpret_cast<int64_t>(handler);
auto rip_addr = reinterpret_cast<int64_t>(&code[10]);
auto offset64 = func_addr - rip_addr;
auto offset32 = static_cast<uint32_t>(static_cast<uint64_t>(offset64) & 0xffffffffu);
*reinterpret_cast<uint32_t*>(&code[6]) = offset32;
}
static uint64_t GetSize() { return 16; }
// 68 00 00 00 00 push <index>
// E9 E0 FF FF FF jmp <handler>
uint8_t code[16] = {0x68, 0x00, 0x00, 0x00, 0x00, 0xE9, 0x00, 0x00,
0x00, 0x00, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90};
};
struct CallPlt {
explicit CallPlt(uint32_t table_size) {
for (uint32_t index = 0; index < table_size; index++) {
auto* c = new (&code[32] + JmpWithIndex::GetSize() * index) JmpWithIndex;
c->SetIndex(index);
c->SetFunc(this);
}
}
void SetPltGot(uint64_t vaddr) { *reinterpret_cast<uint64_t*>(&code[2]) = vaddr; }
uint64_t GetAddr(uint32_t index) {
return reinterpret_cast<uint64_t>(&code[32] + JmpWithIndex::GetSize() * index);
}
static uint64_t GetSize(uint32_t table_size) {
return 32 + JmpWithIndex::GetSize() * table_size;
}
// 0: 49 bb 88 77 66 55 44 movabs r11,0x1122334455667788
// 7: 33 22 11
// a: 41 ff 73 08 push QWORD PTR [r11+0x8]
// e: 41 ff 63 10 jmp QWORD PTR [r11+0x10]
uint8_t code[32] = {0x49, 0xBB, 0x88, 0x77, 0x66, 0x55, 0x44, 0x33, 0x22, 0x11, 0x41, 0xFF,
0x73, 0x08, 0x41, 0xFF, 0x63, 0x10, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90};
};
struct JmpRax {
template <class Handler>
void SetFunc(Handler func) {
+122 -375
View File
@@ -126,19 +126,13 @@ struct StubbedImportRecord {
std::string program;
};
// The structure will be passed via the stack
// since the size of an object is larger than 16 bytes
struct RelocateHandlerStack {
uint64_t stack[3];
};
static std::vector<StubbedImportRecord> g_stubbed_imports;
static std::atomic_uint32_t g_unresolved_stub_call_log_count {0};
static std::vector<uint64_t> g_unresolved_stub_thunk_pages;
static uint64_t g_unresolved_stub_thunk_offset = 0;
static constexpr uint64_t UNRESOLVED_STUB_PAGE_SIZE = 4096;
static KYTY_SYSV_ABI uint64_t ResolveImportStubWithId(uint64_t record_id);
static KYTY_SYSV_ABI uint64_t UnresolvedImportStub(uint64_t record_id);
static bool PatchGuestMemory64(uint64_t vaddr, uint64_t value) {
auto* ptr = reinterpret_cast<uint64_t*>(vaddr);
@@ -148,7 +142,7 @@ static bool PatchGuestMemory64(uint64_t vaddr, uint64_t value) {
}
static uint64_t AllocateUnresolvedImportThunk(uint64_t record_id) {
constexpr uint64_t thunk_size = 165;
constexpr uint64_t thunk_size = 34;
if (g_unresolved_stub_thunk_pages.empty() ||
g_unresolved_stub_thunk_offset + thunk_size > UNRESOLVED_STUB_PAGE_SIZE) {
@@ -164,105 +158,18 @@ static uint64_t AllocateUnresolvedImportThunk(uint64_t record_id) {
g_unresolved_stub_thunk_offset);
g_unresolved_stub_thunk_offset += thunk_size;
const auto target = reinterpret_cast<uint64_t>(ResolveImportStubWithId);
uint8_t bytes[thunk_size] {};
size_t i = 0;
const auto emit = [&](uint8_t b) { bytes[i++] = b; };
const auto emit64 = [&](uint64_t v) {
std::memcpy(bytes + i, &v, sizeof(v));
i += sizeof(v);
uint8_t bytes[thunk_size] = {
0x48, 0x83, 0xec, 0x08, // sub rsp, 8
0x48, 0xbf, 0, 0, 0, 0, 0, 0, 0, 0, // mov rdi, record_id
0x48, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, // mov rax, UnresolvedImportStub
0xff, 0xd0, // call rax
0x48, 0x83, 0xc4, 0x08, // add rsp, 8
0x0f, 0x57, 0xc0, // xorps xmm0, xmm0
0xc3, // ret
};
const auto emit32 = [&](uint32_t v) {
std::memcpy(bytes + i, &v, sizeof(v));
i += sizeof(v);
};
const auto save_xmm = [&](uint8_t reg, uint8_t offset) {
emit(0xf3);
emit(0x0f);
emit(0x7f);
if (offset == 0) {
emit(static_cast<uint8_t>(0x04u | (reg << 3u)));
emit(0x24);
} else {
emit(static_cast<uint8_t>(0x44u | (reg << 3u)));
emit(0x24);
emit(offset);
}
};
const auto load_xmm = [&](uint8_t reg, uint8_t offset) {
emit(0xf3);
emit(0x0f);
emit(0x6f);
if (offset == 0) {
emit(static_cast<uint8_t>(0x04u | (reg << 3u)));
emit(0x24);
} else {
emit(static_cast<uint8_t>(0x44u | (reg << 3u)));
emit(0x24);
emit(offset);
}
};
emit(0x50); // push rax; preserve AL for variadic SysV calls
emit(0x57); // push rdi
emit(0x56); // push rsi
emit(0x52); // push rdx
emit(0x51); // push rcx
emit(0x41);
emit(0x50); // push r8
emit(0x41);
emit(0x51); // push r9
emit(0x48);
emit(0x81);
emit(0xec);
emit32(0x80); // sub rsp, 0x80
for (uint8_t reg = 0; reg < 8; reg++) {
save_xmm(reg, static_cast<uint8_t>(reg * 0x10u));
}
emit(0x48);
emit(0xbf);
emit64(record_id); // mov rdi, record_id
emit(0x48);
emit(0xb8);
emit64(target); // mov rax, ResolveImportStubWithId
emit(0xff);
emit(0xd0); // call rax
emit(0x49);
emit(0x89);
emit(0xc3); // mov r11, rax
for (uint8_t reg = 0; reg < 8; reg++) {
load_xmm(reg, static_cast<uint8_t>(reg * 0x10u));
}
emit(0x48);
emit(0x81);
emit(0xc4);
emit32(0x80); // add rsp, 0x80
emit(0x41);
emit(0x59); // pop r9
emit(0x41);
emit(0x58); // pop r8
emit(0x59); // pop rcx
emit(0x5a); // pop rdx
emit(0x5e); // pop rsi
emit(0x5f); // pop rdi
emit(0x58); // pop rax
emit(0x4d);
emit(0x85);
emit(0xdb); // test r11, r11
emit(0x74);
emit(0x03); // jz +3
emit(0x41);
emit(0xff);
emit(0xe3); // jmp r11
// Match the integer fallback for floating-point return values.
emit(0x0f);
emit(0x57);
emit(0xc0); // xorps xmm0, xmm0
emit(0x31);
emit(0xc0); // xor eax, eax
emit(0xc3); // ret
EXIT_NOT_IMPLEMENTED(i != thunk_size);
const auto target = reinterpret_cast<uint64_t>(UnresolvedImportStub);
std::memcpy(bytes + 6, &record_id, sizeof(record_id));
std::memcpy(bytes + 16, &target, sizeof(target));
std::memcpy(code, bytes, sizeof(bytes));
Common::VirtualMemory::FlushInstructionCache(reinterpret_cast<uint64_t>(code), thunk_size);
return reinterpret_cast<uint64_t>(code);
@@ -270,15 +177,8 @@ static uint64_t AllocateUnresolvedImportThunk(uint64_t record_id) {
static uint64_t RegisterStubbedImport(uint32_t index, const Program* program,
const RelocationInfo& ri) {
const auto program_name = program != nullptr ? Common::PathToString(program->file_name) : "";
for (auto& record: g_stubbed_imports) {
for (const auto& record: g_stubbed_imports) {
if (record.patch_vaddr == ri.vaddr) {
record.index = index;
record.name = ri.name;
record.type = ri.type;
record.bind = ri.bind;
record.program = program_name;
return record.thunk_vaddr;
}
}
@@ -289,7 +189,7 @@ static uint64_t RegisterStubbedImport(uint32_t index, const Program* program,
record.name = ri.name;
record.type = ri.type;
record.bind = ri.bind;
record.program = program_name;
record.program = Common::PathToString(program->file_name);
g_stubbed_imports.push_back(record);
const auto record_id = g_stubbed_imports.size() - 1;
const auto thunk = AllocateUnresolvedImportThunk(record_id);
@@ -297,31 +197,7 @@ static uint64_t RegisterStubbedImport(uint32_t index, const Program* program,
return thunk;
}
static KYTY_SYSV_ABI uint64_t ResolveImportStubWithId(uint64_t record_id) {
if (record_id < g_stubbed_imports.size()) {
auto& record = g_stubbed_imports[record_id];
auto nid = record.name;
auto pos = nid.find('[');
if (pos != std::string::npos) {
nid.resize(pos);
}
SymbolRecord resolved {};
if (!nid.empty() &&
Common::Singleton<RuntimeLinker>::Instance()->ResolveLoadedSymbolByNid(nid, record.type,
&resolved) &&
resolved.vaddr != 0 && resolved.vaddr != record.thunk_vaddr) {
LOGF("Late-resolved import: %s -> %s [0x%016" PRIx64 "]\n", record.name.c_str(),
resolved.name.c_str(), resolved.vaddr);
if (record.patch_vaddr != 0) {
PatchGuestMemory64(record.patch_vaddr, resolved.vaddr);
}
return resolved.vaddr;
}
}
static KYTY_SYSV_ABI uint64_t UnresolvedImportStub(uint64_t record_id) {
const auto log_index = g_unresolved_stub_call_log_count.fetch_add(1);
if (log_index < 1024) {
if (record_id < g_stubbed_imports.size()) {
@@ -1007,135 +883,56 @@ static RelocationInfo GetRelocationInfo(Elf64_Rela* r, Program* program) {
return ret;
}
static void RelocateRecord(uint32_t index, Elf64_Rela* r, Program* program, bool jmprela_table,
bool imports_only, std::vector<std::string>* unresolved) {
static bool RelocateRecord(uint32_t index, Elf64_Rela* r, Program* program, bool jmprela_table) {
KYTY_PROFILER_FUNCTION();
auto ri = GetRelocationInfo(r, program);
if (imports_only &&
(ri.bind_self || (ri.bind != BindType::Global && ri.bind != BindType::Weak))) {
return;
}
[[maybe_unused]] bool patched = false;
bool stubbed_import = false;
bool stubbed_func = false;
// KYTY_PROFILER_BLOCK("patch");
if (ri.resolved) {
patched = PatchGuestMemory64(ri.vaddr, ri.value);
} else {
uint64_t value = 0;
bool weak = (ri.bind == BindType::Weak || !program->fail_if_global_not_resolved);
if (ri.type == SymbolType::Object && weak) {
const auto ri = GetRelocationInfo(r, program);
auto value = ri.value;
bool stubbed = false;
if (!ri.resolved) {
const bool weak = ri.bind == BindType::Weak || !program->fail_if_global_not_resolved;
if (!weak) {
LOGF("Stubbed: %s\n",
fmt::format("[{:016x}] <- {:016x}, {}, {}, {}, {}", ri.vaddr, ri.value, ri.name,
magic_enum::enum_name(ri.type), magic_enum::enum_name(ri.bind),
ri.dbg_name).c_str());
}
if (ri.type == SymbolType::Object) {
value = g_invalid_memory;
} else if (ri.type == SymbolType::Func && jmprela_table && weak) {
value = RegisterStubbedImport(index, program, ri);
stubbed_import = true;
stubbed_func = true;
} else if (ri.type == SymbolType::Func && !jmprela_table && weak) {
value = RegisterStubbedImport(index, program, ri);
stubbed_func = true;
} else if (ri.type == SymbolType::NoType && weak) {
value = RuntimeLinker::ReadFromElf(program, ri.vaddr) + ri.base_vaddr;
}
if (value != 0) {
patched = PatchGuestMemory64(ri.vaddr, value);
} else {
auto dbg_str = fmt::format("[{:016x}] <- {:016x}, {}, {}, {}, {}", ri.vaddr, ri.value,
ri.name.c_str(), magic_enum::enum_name(ri.type),
magic_enum::enum_name(ri.bind), ri.dbg_name.c_str());
if (unresolved != nullptr) {
unresolved->push_back(dbg_str);
} else {
EXIT("Can't resolve: %s\n", dbg_str.c_str());
}
if (ri.type == SymbolType::Object) {
value = g_invalid_memory;
} else if (ri.type == SymbolType::Func || ri.type == SymbolType::NoType) {
value = RegisterStubbedImport(index, program, ri);
stubbed_func = true;
if (jmprela_table) {
stubbed_import = true;
}
}
if (value != 0) {
patched = PatchGuestMemory64(ri.vaddr, value);
}
value = RegisterStubbedImport(index, program, ri);
stubbed = true;
}
}
// KYTY_PROFILER_END_BLOCK;
if (patched && stubbed_import) {
const auto thunk = RegisterStubbedImport(index, program, ri);
LOGF("Relocate: unresolved PLT import patched to stub [%u] [%016" PRIx64 "] <- %016" PRIx64
const bool patched = PatchGuestMemory64(ri.vaddr, value);
if (patched && stubbed) {
LOGF("Relocate: unresolved %s import patched to stub [%u] [%016" PRIx64 "] <- %016" PRIx64
", %s, %s, %s, %s\n",
index, ri.vaddr, thunk, ri.name.c_str(), magic_enum::enum_name(ri.type),
magic_enum::enum_name(ri.bind), Common::PathToString(program->file_name).c_str());
} else if (patched && stubbed_func) {
const auto thunk = RegisterStubbedImport(index, program, ri);
LOGF("Relocate: unresolved non-PLT function patched to stub [%u] [%016" PRIx64
"] <- %016" PRIx64 ", %s, %s, %s, %s\n",
index, ri.vaddr, thunk, ri.name.c_str(), magic_enum::enum_name(ri.type),
magic_enum::enum_name(ri.bind), Common::PathToString(program->file_name).c_str());
jmprela_table ? "PLT" : "non-PLT", index, ri.vaddr, value, ri.name.c_str(),
magic_enum::enum_name(ri.type), magic_enum::enum_name(ri.bind),
Common::PathToString(program->file_name).c_str());
}
if (program->dbg_print_reloc) {
if (patched && !ri.bind_self &&
(ri.bind == BindType::Global || ri.bind == BindType::Weak ||
ri.type == SymbolType::TlsModule)) {
auto dbg_str = fmt::format("[{:016x}] <- {:016x}, {}, {}, {}, {}", ri.vaddr, ri.value,
ri.name.c_str(), magic_enum::enum_name(ri.type),
magic_enum::enum_name(ri.bind), ri.dbg_name.c_str());
LOGF("Relocate: %s\n", dbg_str.c_str());
}
if (program->dbg_print_reloc && patched && !ri.bind_self &&
(ri.bind == BindType::Global || ri.bind == BindType::Weak ||
ri.type == SymbolType::TlsModule)) {
LOGF("Relocate: %s\n",
fmt::format("[{:016x}] <- {:016x}, {}, {}, {}, {}", ri.vaddr, ri.value, ri.name,
magic_enum::enum_name(ri.type), magic_enum::enum_name(ri.bind),
ri.dbg_name).c_str());
}
return ri.resolved;
}
static void RelocateRecords(Elf64_Rela* records, uint64_t size, Program* program,
bool jmprela_table, bool imports_only,
std::vector<std::string>* unresolved) {
KYTY_PROFILER_FUNCTION();
uint32_t index = 0;
for (auto* r = records;
reinterpret_cast<uint8_t*>(r) < reinterpret_cast<uint8_t*>(records) + size; r++, index++) {
RelocateRecord(index, r, program, jmprela_table, imports_only, unresolved);
static void ForEachRelocation(Program* program, auto&& func) {
const auto& info = *program->dynamic_info;
for (uint32_t i = 0; i < info.rela_table_total_size / sizeof(Elf64_Rela); ++i) {
func(info.rela_table + i, i, false);
}
}
__attribute__((naked)) static KYTY_SYSV_ABI void RelocateHandlerReturnStub() {
asm volatile("addq $8, %rsp\n\t"
"retq\n");
}
static KYTY_SYSV_ABI uint64_t RelocateHandler(RelocateHandlerStack s) {
auto* stack = s.stack;
auto* program = reinterpret_cast<Program*>(stack[-1]);
auto rel_index = stack[0];
std::string name = "<unknown function>";
if (program != nullptr && program->dynamic_info != nullptr &&
program->dynamic_info->jmprela_table != nullptr) {
auto ri = GetRelocationInfo(program->dynamic_info->jmprela_table + rel_index, program);
name = ri.name.c_str();
for (uint32_t i = 0; i < info.jmprela_table_size / sizeof(Elf64_Rela); ++i) {
func(info.jmprela_table + i, i, true);
}
// Restore return address (for stack trace)
stack[-1] = reinterpret_cast<uint64_t>(RelocateHandlerReturnStub);
LOGF("=== Stubbed function, returning OK ===\n[%d]\t%s\n", Common::Thread::GetThreadIdUnique(),
name.c_str());
return 0;
}
static KYTY_MS_ABI uint8_t* TlsMainGetAddr() {
@@ -1314,8 +1111,6 @@ void RuntimeLinker::RelocateAll() {
for (auto* p: m_programs) {
Relocate(p);
}
m_relocated = true;
}
void RuntimeLinker::RelocateProgram(Program* program) {
@@ -1335,17 +1130,52 @@ void RuntimeLinker::UnloadProgram(Program* program) {
Common::LockGuard lock(m_mutex);
if (auto it = std::find(m_programs.begin(), m_programs.end(), program);
it != m_programs.end()) {
DeleteProgram(*it);
m_programs.erase(it);
} else {
EXIT("program not found");
}
auto it = std::find(m_programs.begin(), m_programs.end(), program);
EXIT_IF(it == m_programs.end());
m_programs.erase(it);
if (m_relocated) {
RelocateAll();
for (auto* importer: m_programs) {
const auto& info = *importer->dynamic_info;
const auto rela_count =
static_cast<uint32_t>(info.rela_table_total_size / sizeof(Elf64_Rela));
ForEachRelocation(importer, [&](Elf64_Rela* r, uint32_t index, bool jmprela) {
const auto bit_index = (jmprela ? rela_count : 0) + index;
auto& bits = importer->rela_bits[bit_index >> 3u];
const auto mask = 1u << (bit_index & 7u);
const auto type = r->GetType();
if ((bits & mask) == 0 || (type != R_X86_64_64 && type != R_X86_64_GLOB_DAT &&
type != R_X86_64_JUMP_SLOT)) {
return;
}
const auto& symbol = info.symbol_table[r->GetSymbol()];
if (symbol.GetBind() != STB_GLOBAL && symbol.GetBind() != STB_WEAK) {
return;
}
const auto value =
*reinterpret_cast<const uint64_t*>(importer->base_vaddr + r->r_offset);
const auto address = value - (type == R_X86_64_64 ? r->r_addend : 0);
if (address < program->base_vaddr ||
address >= program->base_vaddr + program->mapped_size) {
return;
}
SymbolType symbol_type;
switch (symbol.GetType()) {
case STT_NOTYPE: symbol_type = SymbolType::NoType; break;
case STT_FUNC: symbol_type = SymbolType::Func; break;
case STT_OBJECT: symbol_type = SymbolType::Object; break;
default: return;
}
const std::string name = info.str_table + symbol.st_name;
if (program->export_symbols->FindByNid(name.substr(0, name.find('#')), symbol_type,
address) != nullptr) {
bits &= ~mask;
if (RelocateRecord(index, r, importer, jmprela)) {
bits |= mask;
}
}
});
}
DeleteProgram(program);
}
RuntimeLinker::RuntimeLinker(): m_symbols(std::make_unique<SymbolDatabase>()) {
@@ -1499,7 +1329,6 @@ void RuntimeLinker::Clear() {
g_tls_cached_main_tcb = nullptr;
g_desired_base_addr = SYSTEM_RESERVED + CODE_BASE_OFFSET;
m_symbols.reset();
m_relocated = false;
}
void RuntimeLinker::Resolve(const std::string& name, SymbolType type, Program* program,
@@ -1559,10 +1388,20 @@ void RuntimeLinker::Resolve(const std::string& name, SymbolType type, Program* p
}
if (rec == nullptr) {
if (auto* p = FindProgram(*m, *l); p != nullptr && p->export_symbols != nullptr) {
for (auto* p: m_programs) {
const auto& libs = p->dynamic_info->export_libs;
const auto& modules = p->dynamic_info->export_modules;
if (p->export_symbols == nullptr ||
std::find(libs.begin(), libs.end(), *l) == libs.end() ||
std::find(modules.begin(), modules.end(), *m) == modules.end()) {
continue;
}
rec = p->export_symbols->Find(sr);
if (bind_self != nullptr) {
*bind_self = (p == program);
if (rec != nullptr) {
if (bind_self != nullptr) {
*bind_self = (p == program);
}
break;
}
}
}
@@ -1575,7 +1414,6 @@ void RuntimeLinker::Resolve(const std::string& name, SymbolType type, Program* p
}
if (rec != nullptr) {
//*out_vaddr = rec->vaddr;
*out_info = *rec;
} else {
out_info->vaddr = 0;
@@ -1598,33 +1436,6 @@ void RuntimeLinker::Resolve(const std::string& name, SymbolType type, Program* p
}
}
bool RuntimeLinker::ResolveLoadedSymbolByNid(const std::string& nid, SymbolType type,
SymbolRecord* out_info) {
KYTY_PROFILER_FUNCTION();
Common::LockGuard lock(m_mutex);
EXIT_IF(out_info == nullptr);
for (auto* p: m_programs) {
if (p != nullptr && p->export_symbols != nullptr) {
if (const auto* rec = p->export_symbols->FindByNid(nid, type); rec != nullptr) {
*out_info = *rec;
return true;
}
}
}
if (m_symbols != nullptr) {
if (const auto* rec = m_symbols->FindByNid(nid, type); rec != nullptr) {
*out_info = *rec;
return true;
}
}
return false;
}
uint64_t RuntimeLinker::ReadFromElf(Program* program, uint64_t vaddr) {
EXIT_IF(program == nullptr);
EXIT_IF(program->base_vaddr == 0 || program->base_size == 0);
@@ -2208,11 +2019,6 @@ void RuntimeLinker::DeleteProgram(Program* p) {
EXIT_IF(
!Libs::LibKernel::Memory::FreeGuestMemory(program->base_vaddr, program->mapped_size));
}
if (program->custom_call_plt_vaddr != 0 || program->custom_call_plt_num != 0) {
const auto size = Jit::CallPlt::GetSize(program->custom_call_plt_num);
EXIT_IF(!Libs::LibKernel::Memory::FreeGuestMemory(program->custom_call_plt_vaddr, size));
}
}
void RuntimeLinker::ParseProgramDynamicInfo(Program* program) {
@@ -2255,10 +2061,6 @@ void RuntimeLinker::ParseProgramDynamicInfo(Program* program) {
GetDynValue(elf, &program->dynamic_info->fini_array_size, DT_FINI_ARRAYSZ);
GetDynValue(elf, &program->dynamic_info->preinit_array_size, DT_PREINIT_ARRAYSZ);
EXIT_NOT_IMPLEMENTED(elf->HasDynValue(DT_OS_PLTGOT) && elf->HasDynValue(DT_PLTGOT));
GetDynPtr(elf, &program->dynamic_info->pltgot_vaddr, DT_OS_PLTGOT);
GetDynPtr(elf, &program->dynamic_info->pltgot_vaddr, DT_PLTGOT);
Elf64_Sxword jmprel_type = 0;
EXIT_NOT_IMPLEMENTED(elf->HasDynValue(DT_OS_PLTREL) && elf->HasDynValue(DT_PLTREL));
GetDynValue(elf, &jmprel_type, DT_OS_PLTREL);
@@ -2281,6 +2083,11 @@ void RuntimeLinker::ParseProgramDynamicInfo(Program* program) {
GetDynValue(elf, &program->dynamic_info->rela_table_total_size, DT_RELASZ);
GetDynValue(elf, &program->dynamic_info->rela_table_entry_size, DT_OS_RELAENT);
GetDynValue(elf, &program->dynamic_info->rela_table_entry_size, DT_RELAENT);
EXIT_NOT_IMPLEMENTED(program->dynamic_info->rela_table_total_size % sizeof(Elf64_Rela) != 0 ||
program->dynamic_info->jmprela_table_size % sizeof(Elf64_Rela) != 0);
const auto rela_count = program->dynamic_info->rela_table_total_size / sizeof(Elf64_Rela);
const auto jmprela_count = program->dynamic_info->jmprela_table_size / sizeof(Elf64_Rela);
program->rela_bits.resize((rela_count + jmprela_count + 7u) / 8u);
GetDynValue(elf, &program->dynamic_info->relative_count, DT_RELACOUNT);
@@ -2327,43 +2134,6 @@ void RuntimeLinker::ParseProgramDynamicInfo(Program* program) {
DT_OS_EXPORT_LIB_1);
}
static void InstallRelocateHandler(Program* program) {
KYTY_PROFILER_FUNCTION();
uint64_t pltgot_vaddr = program->dynamic_info->pltgot_vaddr + program->base_vaddr;
uint64_t pltgot_size = static_cast<uint64_t>(3) * 8;
void** pltgot = reinterpret_cast<void**>(pltgot_vaddr);
Common::VirtualMemory::Mode old_mode {};
EXIT_IF(!Libs::LibKernel::Memory::ProtectGuestMemory(
pltgot_vaddr, pltgot_size, Common::VirtualMemory::Mode::Write, &old_mode));
pltgot[1] = program;
pltgot[2] = reinterpret_cast<void*>(RelocateHandler);
EXIT_IF(!Libs::LibKernel::Memory::ProtectGuestMemory(pltgot_vaddr, pltgot_size, old_mode));
if (Common::VirtualMemory::IsExecute(old_mode)) {
Common::VirtualMemory::FlushInstructionCache(pltgot_vaddr, pltgot_size);
}
// TODO(): check if this table already generated by compiler (sometimes it is missing)
if (program->custom_call_plt_vaddr == 0) {
program->custom_call_plt_num =
program->dynamic_info->jmprela_table_size / sizeof(Elf64_Rela);
auto size = Jit::CallPlt::GetSize(program->custom_call_plt_num);
program->custom_call_plt_vaddr = Libs::LibKernel::Memory::AllocateRuntimeMemory(
SYSTEM_RESERVED, size, Common::VirtualMemory::Mode::Write, "custom_call_plt");
EXIT_NOT_IMPLEMENTED(program->custom_call_plt_vaddr == 0);
auto* code = new (reinterpret_cast<void*>(program->custom_call_plt_vaddr))
Jit::CallPlt(program->custom_call_plt_num);
code->SetPltGot(pltgot_vaddr);
EXIT_IF(!Libs::LibKernel::Memory::ProtectGuestMemory(program->custom_call_plt_vaddr, size,
Common::VirtualMemory::Mode::Execute));
Common::VirtualMemory::FlushInstructionCache(program->custom_call_plt_vaddr, size);
}
}
void RuntimeLinker::Relocate(Program* program) {
KYTY_PROFILER_FUNCTION();
@@ -2383,46 +2153,23 @@ void RuntimeLinker::Relocate(Program* program) {
EXIT_NOT_IMPLEMENTED(program->dynamic_info->jmprela_table == nullptr);
EXIT_NOT_IMPLEMENTED(program->dynamic_info->rela_table == nullptr);
EXIT_NOT_IMPLEMENTED(program->dynamic_info->symbol_table == nullptr);
EXIT_NOT_IMPLEMENTED(program->dynamic_info->pltgot_vaddr == 0);
InstallRelocateHandler(program);
const auto rela_count = static_cast<uint32_t>(
program->dynamic_info->rela_table_total_size / sizeof(Elf64_Rela));
std::vector<std::string> unresolved;
const bool imports_only = program->relocated;
RelocateRecords(program->dynamic_info->rela_table, program->dynamic_info->rela_table_total_size,
program, false, imports_only, &unresolved);
RelocateRecords(program->dynamic_info->jmprela_table, program->dynamic_info->jmprela_table_size,
program, true, imports_only, &unresolved);
program->relocated = true;
ForEachRelocation(program, [&](Elf64_Rela* r, uint32_t index, bool jmprela) {
const auto bit_index = (jmprela ? rela_count : 0) + index;
if ((program->rela_bits[bit_index >> 3u] & (1u << (bit_index & 7u))) == 0 &&
RelocateRecord(index, r, program, jmprela)) {
program->rela_bits[bit_index >> 3u] |= 1u << (bit_index & 7u);
}
});
if (program->tls.image_vaddr != 0 && program->tls.init_size != 0 &&
program->tls.init_image.empty()) {
const auto* src = reinterpret_cast<const uint8_t*>(program->tls.image_vaddr);
program->tls.init_image.assign(src, src + program->tls.init_size);
}
if (!unresolved.empty()) {
LOGF("--- Stubbed unresolved imports: %zu ---\n", unresolved.size());
for (const auto& symbol: unresolved) {
LOGF("Stubbed: %s\n", symbol.c_str());
}
}
}
Program* RuntimeLinker::FindProgram(const ModuleId& m, const LibraryId& l) {
Common::LockGuard lock(m_mutex);
for (auto* p: m_programs) {
const auto& export_libs = p->dynamic_info->export_libs;
const auto& export_modules = p->dynamic_info->export_modules;
if (std::find(export_libs.begin(), export_libs.end(), l) != export_libs.end() &&
std::find(export_modules.begin(), export_modules.end(), m) != export_modules.end()) {
return p;
}
}
return nullptr;
}
const ModuleId* RuntimeLinker::FindModule(const Program& program, const std::string& id) {
+2 -8
View File
@@ -86,7 +86,6 @@ struct DynamicInfo {
uint64_t init_array_size = 0;
uint64_t fini_array_size = 0;
uint64_t preinit_array_size = 0;
uint64_t pltgot_vaddr = 0;
Elf64_Rela* jmprela_table = nullptr;
uint64_t jmprela_table_size = 0;
@@ -116,6 +115,7 @@ struct Program {
int32_t unique_id = -1;
RuntimeLinker* rt = nullptr;
uint32_t load_count = 0; // Successful sceKernelLoadStartModule calls.
std::filesystem::path file_name;
std::unique_ptr<Elf64> elf;
std::unique_ptr<DynamicInfo> dynamic_info;
@@ -132,10 +132,8 @@ struct Program {
ThreadLocalStorage tls;
bool fail_if_global_not_resolved = true;
bool dbg_print_reloc = false;
bool relocated = false;
std::vector<uint8_t> rela_bits;
uint64_t proc_param_vaddr = 0;
uint64_t custom_call_plt_vaddr = 0;
uint32_t custom_call_plt_num = 0;
};
class RuntimeLinker {
@@ -171,7 +169,6 @@ public:
void Resolve(const std::string& name, SymbolType type, Program* program, SymbolRecord* out_info,
bool* bind_self);
bool ResolveLoadedSymbolByNid(const std::string& nid, SymbolType type, SymbolRecord* out_info);
SymbolDatabase* Symbols() { return m_symbols.get(); }
@@ -194,14 +191,11 @@ private:
static void SetupTlsHandler(Program* program);
void PreloadAdjacentPrograms();
Program* FindProgram(const ModuleId& m, const LibraryId& l);
static const ModuleId* FindModule(const Program& program, const std::string& id);
static const LibraryId* FindLibrary(const Program& program, const std::string& id);
std::vector<Program*> m_programs;
std::unique_ptr<SymbolDatabase> m_symbols;
bool m_relocated = false;
Common::Mutex m_mutex;
application_heap_malloc_func_t m_application_heap_malloc = nullptr;
+4 -2
View File
@@ -65,12 +65,14 @@ const SymbolRecord* SymbolDatabase::Find(const SymbolResolve& s) const {
return &m_symbols[index];
}
const SymbolRecord* SymbolDatabase::FindByNid(const std::string& nid, SymbolType type) const {
const SymbolRecord* SymbolDatabase::FindByNid(const std::string& nid, SymbolType type,
uint64_t vaddr) const {
auto prefix = nid + "[";
auto suffix = fmt::format("[{}]", magic_enum::enum_name(type));
for (const auto& symbol: m_symbols) {
if (symbol.name.starts_with(prefix) && symbol.name.ends_with(suffix)) {
if ((vaddr == 0 || symbol.vaddr == vaddr) && symbol.name.starts_with(prefix) &&
symbol.name.ends_with(suffix)) {
return &symbol;
}
}
+2 -1
View File
@@ -43,7 +43,8 @@ public:
void Add(const SymbolResolve& s, uint64_t vaddr, const std::string& dbg_name);
[[nodiscard]] const SymbolRecord* Find(const SymbolResolve& s) const;
[[nodiscard]] const SymbolRecord* FindByNid(const std::string& nid, SymbolType type) const;
[[nodiscard]] const SymbolRecord* FindByNid(const std::string& nid, SymbolType type,
uint64_t vaddr = 0) const;
[[nodiscard]] const SymbolRecord* FindByName(const std::string& name, SymbolType type) const;
void DbgDump(const std::string& folder, const std::string& file_name);
+38 -6
View File
@@ -133,6 +133,26 @@ static bool ParseConsoleLanguage(const std::string& value, uint32_t& out) {
return true;
}
static bool ParseUint32(const std::string& value, uint32_t& out) {
uint32_t number = 0;
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), number);
if (error != std::errc {} || end != value.data() + value.size()) {
return false;
}
out = number;
return true;
}
static bool ParseInt32(const std::string& value, int32_t& out) {
int32_t number = 0;
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), number);
if (error != std::errc {} || end != value.data() + value.size()) {
return false;
}
out = number;
return true;
}
static bool ParseUserId(const std::string& value, int32_t& out) {
int32_t user_id = 0;
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), user_id);
@@ -246,9 +266,17 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
}
options.game_patch = path;
} else if (arg == "--screen-width") {
options.config.screen_width = static_cast<uint32_t>(Common::ToInt32(value));
if (!ParseUint32(value, options.config.screen_width) ||
options.config.screen_width == 0) {
::printf("invalid screen width: %s\n", value.c_str());
return false;
}
} else if (arg == "--screen-height") {
options.config.screen_height = static_cast<uint32_t>(Common::ToInt32(value));
if (!ParseUint32(value, options.config.screen_height) ||
options.config.screen_height == 0) {
::printf("invalid screen height: %s\n", value.c_str());
return false;
}
} else if (arg == "--user-name") {
if (value.empty() || value.size() > Config::MAX_USER_NAME_LENGTH) {
::printf("invalid user name: must contain 1-%zu bytes\n",
@@ -269,11 +297,15 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
return false;
}
} else if (arg == "--gpu") {
options.config.gpu_index = Common::ToInt32(value);
if (!ParseInt32(value, options.config.gpu_index)) {
::printf("invalid gpu index: %s\n", value.c_str());
return false;
}
} else if (arg == "--vblank-frequency") {
const int32_t vblank_frequency = Common::ToInt32(value);
options.config.vblank_frequency =
static_cast<uint32_t>(vblank_frequency < 0 ? 0 : vblank_frequency);
if (!ParseUint32(value, options.config.vblank_frequency)) {
::printf("invalid vblank frequency: %s\n", value.c_str());
return false;
}
} else if (arg == "--console-language") {
if (!ParseConsoleLanguage(value, options.config.console_language)) {
::printf("invalid console language: %s\n", value.c_str());
+65 -9
View File
@@ -8,6 +8,7 @@
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <thread>
#include <vector>
@@ -16,14 +17,16 @@ namespace {
namespace AudioOut2 = Libs::Audio::AudioOut2;
std::mutex g_device_mutex;
std::condition_variable g_device_cv;
std::vector<int> g_live_devices;
std::vector<int> g_device_backed_handles;
std::vector<bool> g_output_blocking;
int g_next_device = 1;
int g_open_waiters = 0;
bool g_block_opens = false;
std::mutex g_device_mutex;
std::condition_variable g_device_cv;
std::vector<int> g_live_devices;
std::vector<int> g_device_backed_handles;
std::vector<bool> g_output_blocking;
std::vector<std::vector<uint8_t>> g_output_pcm;
size_t g_capture_bytes = 0;
int g_next_device = 1;
int g_open_waiters = 0;
bool g_block_opens = false;
void Check(bool value, const char* text) {
if (!value) {
@@ -146,6 +149,17 @@ std::vector<bool> OutputCalls() {
return g_output_blocking;
}
void CaptureOutputPcm(size_t bytes) {
std::lock_guard lock(g_device_mutex);
g_capture_bytes = bytes;
g_output_pcm.clear();
}
std::vector<std::vector<uint8_t>> OutputPcm() {
std::lock_guard lock(g_device_mutex);
return g_output_pcm;
}
void TestSlotReuse() {
const auto context = CreateContext();
const auto param = MakeParam();
@@ -327,6 +341,41 @@ void TestHandleWithoutPcmDoesNotBypassQueue() {
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestPcmCopiedBeforeScratchBufferReuse() {
const auto context = CreateContext();
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle first = 0;
AudioOut2::AudioOut2PortHandle second = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &first) == OK,
"first port create failed");
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &second) == OK,
"second port create failed");
std::vector<float> scratch(512 * 2);
std::vector<float> first_pcm(scratch.size(), 0.25f);
std::vector<float> second_pcm(scratch.size(), -0.5f);
std::copy(first_pcm.begin(), first_pcm.end(), scratch.begin());
SetPcm(first, scratch.data());
std::copy(second_pcm.begin(), second_pcm.end(), scratch.begin());
SetPcm(second, scratch.data());
std::fill(scratch.begin(), scratch.end(), 0.0f);
const auto pcm_bytes = scratch.size() * sizeof(float);
CaptureOutputPcm(pcm_bytes);
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "shared-buffer push failed");
const auto output = OutputPcm();
Check(output.size() == 2, "shared-buffer push did not output both ports");
Check(std::memcmp(output[0].data(), first_pcm.data(), pcm_bytes) == 0,
"first port lost PCM when scratch buffer was reused");
Check(std::memcmp(output[1].data(), second_pcm.data(), pcm_bytes) == 0,
"second port lost PCM when scratch buffer was reused");
CaptureOutputPcm(0);
AudioOut2::AudioOut2PortDestroy(first);
AudioOut2::AudioOut2PortDestroy(second);
AudioOut2::AudioOut2ContextDestroy(context);
}
} // namespace
namespace Libs::Audio::AudioInternal {
@@ -363,9 +412,15 @@ bool AudioOutHasDevice(int handle) {
g_device_backed_handles.end();
}
uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool blocking) {
uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) {
std::lock_guard lock(g_device_mutex);
g_output_blocking.push_back(blocking);
if (g_capture_bytes != 0) {
for (uint32_t i = 0; i < num; i++) {
const auto* bytes = static_cast<const uint8_t*>(params[i].data);
g_output_pcm.emplace_back(bytes, bytes + g_capture_bytes);
}
}
return 0;
}
@@ -389,6 +444,7 @@ int main() {
TestFloat12ChannelPortOutputsPcm();
TestAsynchronousDevicePushKeepsQueueBounded();
TestHandleWithoutPcmDoesNotBypassQueue();
TestPcmCopiedBeforeScratchBufferReuse();
std::printf("AudioOut2PortTests: all cases passed\n");
return 0;
}
Loaded 100 of 113 files, more files were not shown because too many files have changed in this diff. Show more