Compare commits

...
Author SHA1 Message Date
Aspenandnmzik 4b0de1b672 Zarchive Loading (#724)
* Simplify archive loading and harden filesystem integration

* Restore directory entry name length assertion

* Keep graphics-dependent filesystem tests out of CI

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-29 00:06:44 +04:00
nmzik 8e61798bfd graphics: honor CPU ownership in image dirty-region queries 2026-09-28 22:17:43 +04:00
nmzik 6ee1bf9db0 graphics: trust image-local readback validity 2026-09-28 22:17:43 +04:00
nmzik 0471c9991c audio: implement AudioOut2 user capability queries 2026-09-28 22:17:43 +04:00
nmzik b10bd53dd3 audio: prevent ATRAC9 vibration configuration heap corruption 2026-09-28 22:17:43 +04:00
nmzik 4cf456fb77 graphics: retain buffer names across draw bindings 2026-09-28 22:17:43 +04:00
Devran Cosmo Uenalandnmzik 22ff4693c5 audio: play pad speaker ports on the DualSense speaker (#868)
* audio: play pad speaker ports on the DualSense speaker

Route the right channel to the speaker with the audio-control values the Linux hid-playstation
driver uses without headphones, and fall back to the main output while no DualSense audio device
is active. Also stop the previous DualSense's rumble when another DualSense becomes active.

* audio: harden DualSense speaker routing and fallback

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-28 20:00:58 +04:00
nmzik a107d1e333 filesystem: keep unmapped guest paths out of the host namespace 2026-09-28 19:17:03 +04:00
05f92b6422 libCes CP932 fix for Windows/macOS, fix SaveDataDialog lockups (#878)
* libs: fix CP932 iconv conversion on Windows and macOS

SDL3's SDL_SYSTEM_ICONV build option defaults off on Windows and macOS,
so SDL falls back to its own bundled iconv there, which only implements
ASCII/Latin-1/UTF/UCS. SDL_iconv_open("UTF-8", "CP932") always failed as
a result, crashing games that convert Shift-JIS strings via
sceCesMbcsStrToUtf8Str (e.g. save-data dialogs).

Add a small CesIconv shim selected at compile time: Windows converts one
character at a time via the Win32 codepage-932 APIs, replicating
SDL_iconv's chunked semantics and error codes; macOS calls real POSIX
iconv directly, since libiconv is always present there and does support
CP932, unlike SDL's fallback. Linux is untouched, since SDL already
links real system iconv by default there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* libs: fix SaveDataDialog infinite loop on save

SaveDataDialogOpen() jumped straight from SAVE_STATUS_INITIALIZED (1) to
SAVE_STATUS_FINISHED (3), skipping SAVE_STATUS_RUNNING (2) entirely. At
least one title's save-write dialog stage only accepts a FINISHED status
after observing a RUNNING poll first; without it, the title assumed the
Open() request never took effect and re-issued it forever, hanging the
save flow (this only became visible once the CP932 crash above stopped
masking it).

Open() now starts the dialog as RUNNING. UpdateStatus() advances the
state machine, reporting RUNNING once before flipping to FINISHED for
subsequent polls; GetStatus() is untouched, staying a pure peek with no
side effects, matching the real SDK's pump-vs-peek split between the two
calls.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Add iconv linking to macOS

* Fix missing macOS iconv link for tests

* Fix CP932 edge cases and save dialog status consistency

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-28 18:42:22 +04:00
nmzik a892097ff0 tests: fix macOS extended-memory alias regression check (#889)
tests: use a macOS-owned address for extended memory alias checks
2026-09-28 18:33:46 +04:00
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
122 changed files with 9491 additions and 3588 deletions

No files matched your search

+9 -9
View File
@@ -113,14 +113,14 @@ jobs:
shell: cmd
run: |
cmake --build _Build/windows --target launcher ^
audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
audio_out2_port_tests pad_haptics_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
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests --parallel
- name: Test
shell: cmd
run: |
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)$"
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
- name: Install
shell: cmd
@@ -201,16 +201,16 @@ jobs:
shell: bash
run: |
cmake --build _Build/macos \
--target launcher audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
--target launcher audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
resource_tracking_tests scalar_provenance_tests \
resource_materialization_tests shader_vertex_metadata_tests \
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
--parallel
- name: Test
shell: bash
run: |
ctest --test-dir _Build/macos --output-on-failure \
-R '^(audio_out2_port|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$'
-R '^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
- name: Install
shell: bash
@@ -384,16 +384,16 @@ jobs:
run: |
cmake --build _Build/linux \
--target launcher page_manager_tests memory_tracker_tests \
audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
resource_tracking_tests scalar_provenance_tests \
resource_materialization_tests shader_vertex_metadata_tests \
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
--parallel
- name: Test
shell: bash
run: |
ctest --test-dir _Build/linux --output-on-failure \
-R '^(audio_out2_port|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$'
-R '^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
- name: Install
shell: bash
+47
View File
@@ -28,7 +28,54 @@ FetchContent_Declare(
FetchContent_MakeAvailable(xbyak zydis)
# ZArchive provides random-access reads for compressed game dumps. Build only
# the reader/writer library and its zstd dependency; the command-line tool is
# not part of KytyPS5's runtime distribution.
set(ZSTD_BUILD_PROGRAMS OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_SHARED OFF CACHE BOOL "" FORCE)
set(ZSTD_BUILD_STATIC ON CACHE BOOL "" FORCE)
set(ZSTD_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(ZSTD_LEGACY_SUPPORT OFF CACHE BOOL "" FORCE)
set(ZSTD_MULTITHREAD_SUPPORT OFF CACHE BOOL "" FORCE)
FetchContent_Declare(
zstd
GIT_REPOSITORY https://github.com/facebook/zstd.git
GIT_TAG f8745da6ff1ad1e7bab384bd1f9d742439278e99
GIT_SHALLOW FALSE
SOURCE_SUBDIR build/cmake
)
FetchContent_Declare(
zarchive_source
GIT_REPOSITORY https://github.com/Exzap/ZArchive.git
GIT_TAG d2c717730092c7bf8cbb033b12fd4001b7c4d932
GIT_SHALLOW FALSE
PATCH_COMMAND "${GIT_EXECUTABLE}" apply --ignore-whitespace
"${CMAKE_CURRENT_SOURCE_DIR}/patches/zarchive-reader.patch"
SOURCE_SUBDIR kyty-library-only
)
FetchContent_MakeAvailable(zstd zarchive_source)
add_library(zarchive STATIC
"${zarchive_source_SOURCE_DIR}/src/zarchivereader.cpp"
"${zarchive_source_SOURCE_DIR}/src/zarchivewriter.cpp"
"${zarchive_source_SOURCE_DIR}/src/sha_256.c"
)
add_library(ZArchive::zarchive ALIAS zarchive)
target_include_directories(zarchive SYSTEM PUBLIC "${zarchive_source_SOURCE_DIR}/include")
target_include_directories(zarchive PRIVATE "${zstd_SOURCE_DIR}/lib")
target_link_libraries(zarchive PRIVATE libzstd_static)
target_compile_features(zarchive PRIVATE cxx_std_20)
if(MSVC)
target_compile_options(zarchive PRIVATE /W0)
else()
target_compile_options(zarchive PRIVATE -w)
endif()
set(SDL_SHARED OFF CACHE BOOL "" FORCE)
if(APPLE)
# SDL's built-in iconv fallback does not support CP932.
set(SDL_SYSTEM_ICONV ON CACHE BOOL "Use system iconv for CP932 conversion" FORCE)
endif()
add_subdirectory(SDL3)
add_library(Vulkan-Headers INTERFACE)
+100
View File
@@ -0,0 +1,100 @@
--- a/include/zarchive/zarchivecommon.h
+++ b/include/zarchive/zarchivecommon.h
@@ -256,7 +256,7 @@
bool IsWithinValidRange(uint64_t fileSize) const
{
- return (offset + size) <= fileSize;
+ return offset <= fileSize && size <= fileSize - offset;
}
};
--- a/src/zarchivereader.cpp
+++ b/src/zarchivereader.cpp
@@ -14,6 +14,7 @@
static bool _ifstream_readBytes(std::ifstream& file, uint64_t offset, void* buffer, uint32_t size)
{
+ file.clear();
file.seekg(offset, std::ios_base::beg);
file.read((char*)buffer, size);
return file.gcount() == size;
@@ -80,9 +81,27 @@
// verify file tree
if (fileTree[0].IsFile())
return nullptr; // first entry must be root directory
- auto rootName = GetName(nameTable, fileTree[0].GetNameOffset());
- if (!rootName.empty())
+ if (fileTree[0].GetNameOffset() != 0x7FFFFFFF)
return nullptr; // root node must not have a name
+ const uint64_t dataCapacity = (uint64_t)offsetRecords.size() * _ZARCHIVE::ENTRIES_PER_OFFSETRECORD * _ZARCHIVE::COMPRESSED_BLOCK_SIZE;
+ for (size_t i = 0; i < fileTree.size(); ++i)
+ {
+ auto& entry = fileTree[i];
+ if (i != 0)
+ {
+ auto name = GetName(nameTable, entry.GetNameOffset());
+ if (name.empty() || name == "." || name == ".." || name.find_first_of("/\\") != std::string_view::npos || name.find('\0') != std::string_view::npos)
+ return nullptr;
+ }
+ if (entry.IsFile())
+ {
+ if (entry.GetFileOffset() > dataCapacity || entry.GetFileSize() > dataCapacity - entry.GetFileOffset())
+ return nullptr;
+ }
+ else if (entry.directoryRecord.nodeStartIndex > fileTree.size() ||
+ entry.directoryRecord.count > fileTree.size() - entry.directoryRecord.nodeStartIndex)
+ return nullptr;
+ }
// read meta data
// todo
@@ -238,7 +257,7 @@
uint32_t stepSize = std::min<uint32_t>(remainingBytes, _ZARCHIVE::COMPRESSED_BLOCK_SIZE - blockOffset);
CacheBlock* block = GetCachedBlock(blockIdx);
if (!block)
- return 0;
+ return bytesToRead - remainingBytes;
std::memcpy(bufferU8, block->data + blockOffset, stepSize);
rawReadOffset += stepSize;
remainingBytes -= stepSize;
@@ -325,14 +344,18 @@
// determine offset and size of compressed block
auto& record = m_offsetRecords[recordIndex];
uint64_t offset = record.baseOffset;
+ if (offset > m_compressedDataSize)
+ return false;
for (uint32_t i = 0; i < recordSubIndex; i++)
{
- offset += (uint64_t)record.size[i];
- offset++;
+ const uint64_t size = (uint64_t)record.size[i] + 1;
+ if (size > m_compressedDataSize - offset)
+ return false;
+ offset += size;
}
uint32_t compressedSize = (uint32_t)record.size[recordSubIndex] + 1;
// load file data
- if ((offset + compressedSize) > m_compressedDataSize)
+ if (compressedSize > m_compressedDataSize - offset)
return false;
offset += m_compressedDataOffset;
if (compressedSize == _ZARCHIVE::COMPRESSED_BLOCK_SIZE)
@@ -350,7 +373,7 @@
// returns empty view on failure
std::string_view ZArchiveReader::GetName(const std::vector<uint8_t>& nameTable, uint32_t nameOffset)
{
- if (nameOffset == 0x7FFFFFFF || nameOffset > nameTable.size())
+ if (nameOffset == 0x7FFFFFFF || nameOffset >= nameTable.size())
return "";
// parse header
uint16_t nameLength = nameTable[nameOffset] & 0x7F;
@@ -359,7 +382,7 @@
// extended 2-byte length
if (nameOffset + 1 >= nameTable.size())
return "";
- nameLength |= ((uint16_t)nameTable[nameOffset] << 7);
+ nameLength |= ((uint16_t)nameTable[nameOffset + 1] << 7);
nameOffset += 2;
}
else
+45 -1
View File
@@ -1,4 +1,4 @@
cmake_minimum_required(VERSION 3.16...4.3)
cmake_minimum_required(VERSION 3.18...4.3)
if(CMAKE_VERSION VERSION_GREATER_EQUAL 4.0)
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "Minimum policy version for bundled dependencies")
@@ -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)
@@ -406,6 +407,16 @@ add_executable(page_manager_tests EXCLUDE_FROM_ALL
target_link_libraries(page_manager_tests common)
target_include_directories(page_manager_tests PRIVATE ${inc_headers})
add_executable(archive_file_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/ArchiveFileTests.cpp"
)
target_link_libraries(archive_file_tests common ZArchive::zarchive)
target_include_directories(archive_file_tests PRIVATE ${inc_headers})
add_executable(avplayer_file_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/AvPlayerFileTests.cpp")
target_link_libraries(avplayer_file_tests common FFmpeg::ffmpeg ZArchive::zarchive)
target_include_directories(avplayer_file_tests PRIVATE ${inc_headers})
add_executable(bit_array_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/BitArrayTests.cpp"
)
@@ -477,12 +488,32 @@ add_executable(ngs2_sampler_tests EXCLUDE_FROM_ALL
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_executable(ces_tests EXCLUDE_FROM_ALL "${KYTY_TESTS_DIR}/CesTests.cpp")
target_link_libraries(ces_tests common fmt::fmt SDL3::SDL3-static)
target_include_directories(ces_tests PRIVATE ${inc_headers})
add_executable(save_data_dialog_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/SaveDataDialogTests.cpp"
"${KYTY_SOURCE_DIR}/libs/dialog.cpp"
)
target_link_libraries(save_data_dialog_tests common fmt::fmt)
target_include_directories(save_data_dialog_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"
@@ -493,6 +524,7 @@ target_include_directories(event_queue_lifetime_tests PRIVATE ${inc_headers})
add_kyty_full_emulator_test(kernel_file_system_tests
"${KYTY_TESTS_DIR}/KernelFileSystemTests.cpp")
target_link_libraries(kernel_file_system_tests ZArchive::zarchive)
add_executable(sync_on_address_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/SyncOnAddressTests.cpp"
@@ -559,6 +591,8 @@ if(BUILD_TESTING)
add_test(NAME image_page_table COMMAND $<TARGET_FILE:image_page_table_tests>)
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 archive_file COMMAND $<TARGET_FILE:archive_file_tests>)
add_test(NAME avplayer_file COMMAND $<TARGET_FILE:avplayer_file_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>)
@@ -569,7 +603,11 @@ if(BUILD_TESTING)
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 ces COMMAND $<TARGET_FILE:ces_tests>)
add_test(NAME save_data_dialog COMMAND $<TARGET_FILE:save_data_dialog_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)
@@ -624,6 +662,8 @@ if(BUILD_TESTING)
image_page_table_tests
memory_tracker_tests
page_manager_tests
archive_file_tests
avplayer_file_tests
bit_array_tests
lru_cache_tests
shader_vertex_metadata_tests
@@ -636,7 +676,11 @@ if(BUILD_TESTING)
virtual_memory_allocation_tests
audio_out2_port_tests
ngs2_sampler_tests
pad_haptics_tests
save_data_memory_tests
ces_tests
save_data_dialog_tests
http_uri_parse_tests
)
endif()
+17 -8
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>
@@ -232,9 +236,10 @@ cmake --build _Build/linux --target launcher --parallel
cmake --install _Build/linux --prefix _Build/linux/install
```
The configure step downloads the FFmpeg prebuilts and the `xbyak`/`zydis` sources, so it needs
network access; a fully sandboxed `nix build` would require vendoring those inputs. A Vulkan 1.3
driver must be available at runtime (on NixOS, `hardware.graphics.enable = true`).
The configure step downloads the FFmpeg prebuilts and the `xbyak`, `zydis`, `zstd`, and ZArchive
sources, so it needs network access; a fully sandboxed `nix build` would require vendoring those
inputs. A Vulkan 1.3 driver must be available at runtime (on NixOS,
`hardware.graphics.enable = true`).
### Building on macOS
@@ -328,17 +333,21 @@ open _Build/macos/install/KytyPS5.app # or double-click in Finder
```
On first launch, add one or more game folders in the global settings. The launcher searches those
folders recursively for game directories containing `eboot.bin`. Select a detected game and run it
from the game list.
folders recursively for game directories containing `eboot.bin` and ZArchive (`.zar`) game dumps
whose archive root contains `eboot.bin`. Select a detected game and run it from the game list.
ZArchive dumps are mounted read-only and streamed directly; they do not need to be extracted first.
The emulator can also be started directly with a legally obtained game directory or ELF file:
The emulator can also be started directly with a legally obtained game directory, ELF file, or
ZArchive dump:
```powershell
.\_Build\windows\install\kyty_emulator.exe --game "D:\Games\ExampleGame"
.\_Build\windows\install\kyty_emulator.exe --game "D:\Games\ExampleGame.zar"
```
```bash
./_Build/linux/install/kyty_emulator --game "/games/ExampleGame"
./_Build/linux/install/kyty_emulator --game "/games/ExampleGame.zar"
```
On macOS, the adjacent flat or app-bundled `libMoltenVK.dylib` is found automatically; no
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

After

Width:  |  Height:  |  Size: 1.7 MiB

+1
View File
@@ -26,6 +26,7 @@ target_link_libraries(common
cpuinfo
kyty_git_version
spdlog::spdlog
ZArchive::zarchive
)
get_property(inc_headers TARGET common PROPERTY INCLUDE_DIRECTORIES)
+397
View File
@@ -0,0 +1,397 @@
#include "common/archive.h"
#include "common/stringUtils.h"
#include <algorithm>
#include <condition_variable>
#include <cstring>
#include <mutex>
#include <optional>
#include <string_view>
#include <thread>
#include <unordered_map>
#include <utility>
#include <zarchive/zarchivereader.h>
// POSIX hosts and MinGW's pthread-backed libstdc++ have cancellable condition waits.
// MSVC/ClangCL and MinGW's native Win32 threading backend do not use pthread waits.
#if !defined(_WIN32) || defined(_GLIBCXX_GCC_GTHR_POSIX_H)
#include <pthread.h>
#define KYTY_ARCHIVE_PTHREAD_CANCELLATION
#endif
namespace Common {
// Backends expose immutable metadata and offset-based reads. Cursor management, path routing,
// reader lifetime and the host-stack worker are shared by every archive format.
class ArchiveReader {
public:
struct Entry {
uint64_t id;
uint64_t size;
bool is_file;
};
virtual ~ArchiveReader() = default;
virtual std::optional<Entry> Find(std::string_view member) = 0;
virtual std::vector<File::DirEntry> List(std::string_view member) = 0;
virtual uint64_t Read(uint64_t id, uint64_t offset, uint32_t size, void* data) = 0;
};
namespace {
class ZArchiveReaderBackend final: public ArchiveReader {
public:
explicit ZArchiveReaderBackend(std::unique_ptr<ZArchiveReader> reader)
: m_reader(std::move(reader)) {}
std::optional<Entry> Find(std::string_view member) override {
const auto node = m_reader->LookUp(member);
if (node == ZARCHIVE_INVALID_NODE) {
return {};
}
return Entry {node, m_reader->GetFileSize(node), m_reader->IsFile(node)};
}
std::vector<File::DirEntry> List(std::string_view member) override {
const auto node = m_reader->LookUp(member);
if (!m_reader->IsDirectory(node)) {
return {};
}
const auto count = m_reader->GetDirEntryCount(node);
if (count > (1u << 20u)) {
return {};
}
std::vector<File::DirEntry> result;
result.reserve(count);
for (uint32_t index = 0; index < count; ++index) {
ZArchiveReader::DirEntry entry {};
if (m_reader->GetDirEntry(node, index, entry)) {
result.push_back({std::string(entry.name), entry.isFile});
}
}
return result;
}
uint64_t Read(uint64_t id, uint64_t offset, uint32_t size, void* data) override {
// ZArchiveReader already serializes access to its stream and decompression cache.
return m_reader->ReadFromFile(static_cast<ZArchiveNodeHandle>(id), offset, size, data);
}
private:
std::unique_ptr<ZArchiveReader> m_reader;
};
std::shared_ptr<ArchiveReader> OpenZArchive(const std::filesystem::path& path) {
auto reader = std::unique_ptr<ZArchiveReader>(ZArchiveReader::OpenFromFile(path));
return reader ? std::make_shared<ZArchiveReaderBackend>(std::move(reader)) : nullptr;
}
struct ArchiveFormat {
std::string_view extension;
std::shared_ptr<ArchiveReader> (*open)(const std::filesystem::path&);
};
constexpr ArchiveFormat Formats[] = {{".zar", OpenZArchive}};
using NativeView = std::basic_string_view<std::filesystem::path::value_type>;
const ArchiveFormat* FindFormat(NativeView path) {
for (const auto& format: Formats) {
if (path.size() < format.extension.size()) {
continue;
}
const auto extension = path.substr(path.size() - format.extension.size());
if (std::equal(
extension.begin(), extension.end(), format.extension.begin(),
[](auto a, char b) { return (a >= 'A' && a <= 'Z' ? a + ('a' - 'A') : a) == b; })) {
return &format;
}
}
return nullptr;
}
size_t FindMarker(NativeView path) {
// Native files use this check on every operation. The usual path has no marker and allocates
// nothing.
for (auto marker = path.find('!'); marker != NativeView::npos;
marker = path.find('!', marker + 1)) {
if ((marker + 1 == path.size() || path[marker + 1] == '/' || path[marker + 1] == '\\') &&
FindFormat(path.substr(0, marker)) != nullptr) {
return marker;
}
}
return NativeView::npos;
}
struct ParsedPath {
std::filesystem::path archive;
std::string member;
};
std::optional<ParsedPath> ParsePath(const std::filesystem::path& path) {
const auto& native = path.native();
const auto marker = FindMarker(native);
if (marker == NativeView::npos) {
return {};
}
ParsedPath parsed {.archive = std::filesystem::path(native.substr(0, marker))};
auto member = PathToGenericString(std::filesystem::path(native.substr(marker + 1)));
std::replace(member.begin(), member.end(), '\\', '/');
std::string_view remaining = member;
while (!remaining.empty()) {
const auto separator = remaining.find('/');
const auto component = remaining.substr(0, separator);
remaining.remove_prefix(separator == std::string_view::npos ? remaining.size() : separator + 1);
if (component.empty() || component == ".") {
continue;
}
if (component == "..") {
if (parsed.member.empty()) {
return {};
}
const auto separator = parsed.member.rfind('/');
parsed.member.resize(separator == std::string::npos ? 0 : separator);
} else {
if (!parsed.member.empty()) {
parsed.member += '/';
}
parsed.member += component;
}
}
return parsed;
}
std::mutex g_readers_mutex;
std::unordered_map<std::filesystem::path, std::weak_ptr<ArchiveReader>> g_readers;
// Guest stacks can be only 64 KiB. Decompress on an ordinary host stack, then copy into guest
// memory on the caller so GPU-tracked writes use its fault handler. A single worker bounds host
// threads and avoids duplicating the archive's own block cache. Requests live on waiting callers.
class ArchiveIoThread {
public:
static ArchiveIoThread& Instance() {
// Guest exit can run static destructors while other guest threads are still reading.
// Keep the worker alive until process termination, as with the guest threads themselves.
static auto* instance = new ArchiveIoThread;
return *instance;
}
uint64_t Read(ArchiveReader& reader, uint64_t id, uint64_t offset, uint32_t size, void* data) {
Request request {reader, id, offset, size, data};
std::unique_lock lock(m_mutex);
if (m_last != nullptr) {
m_last->next = &request;
} else {
m_first = &request;
}
m_last = &request;
m_wake.notify_one();
request.ready.wait(lock, [&] { return request.done; });
return request.result;
}
KYTY_CLASS_NO_COPY(ArchiveIoThread);
private:
struct Request {
ArchiveReader& reader;
uint64_t id;
uint64_t offset;
uint32_t size;
void* data;
Request* next = nullptr;
uint64_t result = 0;
bool done = false;
std::condition_variable ready {};
};
ArchiveIoThread() {
std::thread([this] { Loop(); }).detach();
}
void Loop() {
for (;;) {
Request* request;
{
std::unique_lock lock(m_mutex);
m_wake.wait(lock, [this] { return m_first != nullptr; });
request = m_first;
m_first = request->next;
if (m_first == nullptr) {
m_last = nullptr;
}
}
const auto result =
request->reader.Read(request->id, request->offset, request->size, request->data);
{
std::lock_guard lock(m_mutex);
request->result = result;
request->done = true;
// Notify before releasing the mutex: the waiting caller owns the condition
// variable.
request->ready.notify_one();
}
}
}
std::mutex m_mutex;
std::condition_variable m_wake;
Request* m_first = nullptr;
Request* m_last = nullptr;
};
} // namespace
struct ArchiveFile::Private {
std::shared_ptr<ArchiveReader> reader;
ArchiveReader::Entry entry;
uint64_t position = 0;
};
ArchiveFile::ArchiveFile(std::unique_ptr<Private> p): m_p(std::move(p)) {}
ArchiveFile::~ArchiveFile() = default;
uint64_t ArchiveFile::Size() const {
return m_p->entry.size;
}
uint64_t ArchiveFile::Tell() const {
return m_p->position;
}
bool ArchiveFile::Seek(uint64_t offset) {
m_p->position = offset;
return true;
}
void ArchiveFile::Read(void* data, uint32_t size, uint32_t* bytes_read) {
#if defined(KYTY_ARCHIVE_PTHREAD_CANCELLATION)
// A pending cancellation must not destroy the waiting request or its TLS buffer while the
// worker still uses them. Restore the caller's state only after the copy is complete.
int cancellation_state = PTHREAD_CANCEL_ENABLE;
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cancellation_state);
#endif
constexpr uint32_t chunk_size = 1u << 20u;
thread_local std::vector<uint8_t> chunk;
uint32_t read = 0;
if (data != nullptr && size != 0 && m_p->position < Size()) {
const auto requested =
static_cast<uint32_t>(std::min<uint64_t>(size, Size() - m_p->position));
if (chunk.size() < std::min(chunk_size, requested)) {
chunk.resize(std::min(chunk_size, requested));
}
while (read < requested) {
const auto step = std::min(chunk_size, requested - read);
const auto got = ArchiveIoThread::Instance().Read(*m_p->reader, m_p->entry.id,
m_p->position, step, chunk.data());
if (got == 0 || got > step) {
break;
}
std::memcpy(static_cast<uint8_t*>(data) + read, chunk.data(), got);
read += static_cast<uint32_t>(got);
m_p->position += got;
if (got < step) {
break;
}
}
}
if (bytes_read != nullptr) {
*bytes_read = read;
}
#if defined(KYTY_ARCHIVE_PTHREAD_CANCELLATION)
pthread_setcancelstate(cancellation_state, nullptr);
#endif
}
std::filesystem::path MakeArchivePath(const std::filesystem::path& archive,
const std::filesystem::path& member) {
auto root = archive;
root += "!";
return member.empty() ? root : root / member.relative_path();
}
bool IsSupportedArchive(const std::filesystem::path& path) {
return FindFormat(path.native()) != nullptr;
}
bool IsArchivePath(const std::filesystem::path& path) {
return FindMarker(path.native()) != NativeView::npos;
}
std::filesystem::path GetArchiveHostPath(const std::filesystem::path& path) {
const auto marker = FindMarker(path.native());
return marker == NativeView::npos ? std::filesystem::path {}
: std::filesystem::path(path.native().substr(0, marker));
}
std::shared_ptr<ArchiveReader> OpenArchive(const std::filesystem::path& path) {
const auto archive = IsArchivePath(path) ? GetArchiveHostPath(path) : path;
const auto* format = FindFormat(archive.native());
if (format == nullptr) {
return {};
}
std::error_code error;
auto key = std::filesystem::absolute(archive, error);
if (error) {
return {};
}
{
std::lock_guard lock(g_readers_mutex);
if (auto it = g_readers.find(key); it != g_readers.end()) {
if (auto reader = it->second.lock()) {
return reader;
}
}
}
// Do not hold the cache mutex while opening an unrelated archive from disk.
auto reader = format->open(archive);
if (reader == nullptr) {
return {};
}
std::lock_guard lock(g_readers_mutex);
std::erase_if(g_readers, [](const auto& entry) { return entry.second.expired(); });
auto& cached = g_readers[key];
if (auto existing = cached.lock()) {
return existing;
}
cached = reader;
return reader;
}
std::optional<File::Info> GetArchiveInfo(const std::filesystem::path& path) {
const auto parsed = ParsePath(path);
if (parsed) {
if (auto reader = OpenArchive(parsed->archive)) {
if (auto entry = reader->Find(parsed->member)) {
return File::Info {entry->is_file, entry->size};
}
}
}
return {};
}
std::vector<File::DirEntry> GetArchiveDirEntries(const std::filesystem::path& path) {
const auto parsed = ParsePath(path);
if (parsed) {
if (auto reader = OpenArchive(parsed->archive)) {
return reader->List(parsed->member);
}
}
return {};
}
std::unique_ptr<ArchiveFile> OpenArchiveFile(const std::filesystem::path& path) {
auto parsed = ParsePath(path);
if (!parsed) {
return {};
}
auto reader = OpenArchive(parsed->archive);
const auto entry = reader ? reader->Find(parsed->member) : std::nullopt;
if (!entry || !entry->is_file) {
return {};
}
auto p = std::make_unique<ArchiveFile::Private>();
p->reader = std::move(reader);
p->entry = *entry;
return std::unique_ptr<ArchiveFile>(new ArchiveFile(std::move(p)));
}
} // namespace Common
+50
View File
@@ -0,0 +1,50 @@
#ifndef KYTY_COMMON_ARCHIVE_H_
#define KYTY_COMMON_ARCHIVE_H_
#include "common/file.h"
#include <cstdint>
#include <filesystem>
#include <memory>
#include <vector>
namespace Common {
class ArchiveReader;
class ArchiveFile {
public:
~ArchiveFile();
[[nodiscard]] uint64_t Size() const;
[[nodiscard]] uint64_t Tell() const;
bool Seek(uint64_t offset);
void Read(void* data, uint32_t size, uint32_t* bytes_read);
KYTY_CLASS_NO_COPY(ArchiveFile);
private:
struct Private;
explicit ArchiveFile(std::unique_ptr<Private> p);
std::unique_ptr<Private> m_p;
friend std::unique_ptr<ArchiveFile> OpenArchiveFile(const std::filesystem::path& path);
};
// Virtual paths use "archive.ext!/member" and retain native host-path encoding.
[[nodiscard]] std::filesystem::path MakeArchivePath(const std::filesystem::path& archive,
const std::filesystem::path& member = {});
[[nodiscard]] bool IsSupportedArchive(const std::filesystem::path& path);
[[nodiscard]] bool IsArchivePath(const std::filesystem::path& path);
[[nodiscard]] std::optional<File::Info> GetArchiveInfo(const std::filesystem::path& path);
[[nodiscard]] std::vector<File::DirEntry> GetArchiveDirEntries(const std::filesystem::path& path);
[[nodiscard]] std::filesystem::path GetArchiveHostPath(const std::filesystem::path& path);
[[nodiscard]] std::unique_ptr<ArchiveFile> OpenArchiveFile(const std::filesystem::path& path);
// Retain this handle while a mount or metadata scan is active to reuse the index and block cache.
// Accepts either a host archive filename or a virtual path inside the archive.
[[nodiscard]] std::shared_ptr<ArchiveReader> OpenArchive(const std::filesystem::path& path);
} // namespace Common
#endif /* KYTY_COMMON_ARCHIVE_H_ */
+140 -17
View File
@@ -1,5 +1,6 @@
#include "common/file.h"
#include "common/archive.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/platform/sysFileIO.h"
@@ -61,7 +62,8 @@ SysFileTimeStruct DateTimeToFileTimeUtc(const DateTime& date_time) {
} // namespace
struct File::FilePrivate {
sys_file_t* f;
sys_file_t* f;
std::unique_ptr<ArchiveFile> archive;
};
File::File(): m_p(std::make_unique<FilePrivate>()) {
@@ -85,11 +87,15 @@ File::File(const std::filesystem::path& name, Mode mode): m_p(std::make_unique<F
}
bool File::IsInvalid() const {
return m_p->f == nullptr;
return m_p->f == nullptr && m_p->archive == nullptr;
}
bool File::Create(const std::filesystem::path& name) {
EXIT_IF(m_p->f != nullptr);
EXIT_IF(!IsInvalid());
if (IsArchivePath(name)) {
return false;
}
m_file_name = name;
@@ -104,9 +110,16 @@ bool File::Create(const std::filesystem::path& name) {
}
bool File::Open(const std::filesystem::path& name, Mode mode) {
EXIT_IF(m_p->f != nullptr);
EXIT_IF(!IsInvalid());
m_file_name = name;
if (IsArchivePath(name)) {
if (mode != Mode::Read) {
return false;
}
m_p->archive = OpenArchiveFile(name);
return m_p->archive != nullptr;
}
switch (mode) {
case Mode::Read: m_p->f = SysFileOpenR(name); break;
@@ -124,7 +137,7 @@ bool File::Open(const std::filesystem::path& name, Mode mode) {
}
bool File::OpenInMem(void* buf, uint32_t buf_size) {
EXIT_IF(m_p->f != nullptr);
EXIT_IF(!IsInvalid());
m_p->f = SysFileOpen(static_cast<uint8_t*>(buf), buf_size);
@@ -137,7 +150,7 @@ bool File::OpenInMem(void* buf, uint32_t buf_size) {
}
bool File::CreateInMem() {
EXIT_IF(m_p->f != nullptr);
EXIT_IF(!IsInvalid());
m_p->f = SysFileCreate();
@@ -150,6 +163,7 @@ bool File::CreateInMem() {
}
void File::Close() {
m_p->archive.reset();
if (m_p->f != nullptr) {
SysFileClose(m_p->f);
m_p->f = nullptr;
@@ -157,7 +171,11 @@ void File::Close() {
}
uint64_t File::Size() const {
EXIT_IF(m_p->f == nullptr);
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
return m_p->archive->Size();
}
if (m_p->f == nullptr) {
return 0;
@@ -167,41 +185,76 @@ uint64_t File::Size() const {
}
uint64_t File::Remaining() const {
EXIT_IF(m_p->f == nullptr);
EXIT_IF(IsInvalid());
return Size() - Tell();
const auto size = Size();
const auto position = Tell();
return position < size ? size - position : 0;
}
uint64_t File::Size(const std::filesystem::path& name) {
if (IsArchivePath(name)) {
const auto info = GetArchiveInfo(name);
return info ? info->size : 0;
}
return SysFileSize(name);
}
std::optional<File::Info> File::GetInfo(const std::filesystem::path& name) {
if (IsArchivePath(name)) {
return GetArchiveInfo(name);
}
Info info {};
return SysFileGetInfo(name, &info.is_file, &info.size) ? std::optional(info) : std::nullopt;
}
bool File::Seek(uint64_t offset) {
EXIT_IF(m_p->f == nullptr);
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
return m_p->archive->Seek(offset);
}
return SysFileSeek(*m_p->f, offset);
}
bool File::Truncate(uint64_t size) {
EXIT_IF(m_p->f == nullptr);
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
return false;
}
return SysFileTruncate(*m_p->f, size);
}
bool File::Unlink() {
EXIT_IF(m_p->f == nullptr);
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
return false;
}
return SysFileUnlink(*m_p->f, m_file_name);
}
uint64_t File::Tell() const {
EXIT_IF(m_p->f == nullptr);
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
return m_p->archive->Tell();
}
return SysFileTell(*m_p->f);
}
void File::Read(void* data, uint32_t size, uint32_t* bytes_read) {
EXIT_IF(m_p->f == nullptr);
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
m_p->archive->Read(data, size, bytes_read);
return;
}
if (m_p->f != nullptr) {
SysFileRead(data, size, *m_p->f, bytes_read);
@@ -209,7 +262,14 @@ void File::Read(void* data, uint32_t size, uint32_t* bytes_read) {
}
void File::Write(const void* data, uint32_t size, uint32_t* bytes_written) {
EXIT_IF(m_p->f == nullptr);
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
if (bytes_written != nullptr) {
*bytes_written = 0;
}
return;
}
SysFileWrite(data, size, *m_p->f, bytes_written);
}
@@ -257,24 +317,41 @@ static std::filesystem::path WithoutTrailingSeparator(
bool File::IsDirectoryExisting(const std::filesystem::path& path) {
if (IsArchivePath(path)) {
const auto info = GetArchiveInfo(path);
return info && !info->is_file;
}
return SysFileIsDirectoryExisting(WithoutTrailingSeparator(path));
}
bool File::IsFileExisting(const std::filesystem::path& name) {
if (IsArchivePath(name)) {
const auto info = GetArchiveInfo(name);
return info && info->is_file;
}
return SysFileIsFileExisting(name);
}
bool File::CreateDirectory(const std::filesystem::path& path) {
if (IsArchivePath(path)) {
return false;
}
return SysFileCreateDirectory(
WithoutTrailingSeparator(path));
}
bool File::DeleteDirectory(const std::filesystem::path& path) {
if (IsArchivePath(path)) {
return false;
}
return SysFileDeleteDirectory(
WithoutTrailingSeparator(path));
}
bool File::CreateDirectories(const std::filesystem::path& path) {
if (IsArchivePath(path)) {
return false;
}
const auto normalized_path =
WithoutTrailingSeparator(path);
@@ -297,6 +374,9 @@ bool File::CreateDirectories(const std::filesystem::path& path) {
}
bool File::DeleteDirectories(const std::filesystem::path& path) {
if (IsArchivePath(path)) {
return false;
}
const auto normalized_path =
WithoutTrailingSeparator(path);
@@ -322,11 +402,18 @@ bool File::DeleteDirectories(const std::filesystem::path& path) {
bool File::DeleteFile(
const std::filesystem::path& name) // @suppress("Member declaration not found")
{
if (IsArchivePath(name)) {
return false;
}
return SysFileDeleteFile(name);
}
bool File::Flush() {
EXIT_IF(m_p->f == nullptr);
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
return true;
}
return SysFileFlush(*m_p->f);
}
@@ -348,6 +435,9 @@ std::vector<std::byte> File::ReadWholeBuffer() {
}
DateTime File::GetLastAccessTimeUTC(const std::filesystem::path& name) {
if (IsArchivePath(name)) {
return GetLastAccessTimeUTC(GetArchiveHostPath(name));
}
SysTimeStruct t {};
SysFileToSystemTimeUtc(SysFileGetLastAccessTimeUtc(name), t);
@@ -359,6 +449,9 @@ DateTime File::GetLastAccessTimeUTC(const std::filesystem::path& name) {
}
DateTime File::GetLastWriteTimeUTC(const std::filesystem::path& name) {
if (IsArchivePath(name)) {
return GetLastWriteTimeUTC(GetArchiveHostPath(name));
}
SysTimeStruct t {};
SysFileToSystemTimeUtc(SysFileGetLastWriteTimeUtc(name), t);
@@ -373,6 +466,10 @@ void File::GetLastAccessAndWriteTimeUTC(const std::filesystem::path& name, DateT
DateTime* write) {
EXIT_IF(access == nullptr);
EXIT_IF(write == nullptr);
if (IsArchivePath(name)) {
GetLastAccessAndWriteTimeUTC(GetArchiveHostPath(name), access, write);
return;
}
SysTimeStruct at {};
SysTimeStruct wt {};
@@ -399,7 +496,12 @@ void File::GetLastAccessAndWriteTimeUTC(DateTime* access, DateTime* write) {
EXIT_IF(access == nullptr);
EXIT_IF(write == nullptr);
EXIT_IF(m_p->f == nullptr);
EXIT_IF(IsInvalid());
if (m_p->archive != nullptr) {
GetLastAccessAndWriteTimeUTC(m_file_name, access, write);
return;
}
if (m_p->f == nullptr) {
*access = DateTime();
@@ -429,23 +531,35 @@ void File::GetLastAccessAndWriteTimeUTC(DateTime* access, DateTime* write) {
}
bool File::SetLastAccessTimeUTC(const std::filesystem::path& name, const DateTime& dt) {
if (IsArchivePath(name)) {
return false;
}
auto f = DateTimeToFileTimeUtc(dt);
return SysFileSetLastAccessTimeUtc(name, f);
}
bool File::SetLastWriteTimeUTC(const std::filesystem::path& name, const DateTime& dt) {
if (IsArchivePath(name)) {
return false;
}
auto f = DateTimeToFileTimeUtc(dt);
return SysFileSetLastWriteTimeUtc(name, f);
}
bool File::SetLastAccessAndWriteTimeUTC(const std::filesystem::path& name, const DateTime& access,
const DateTime& write) {
if (IsArchivePath(name)) {
return false;
}
auto af = DateTimeToFileTimeUtc(access);
auto wf = DateTimeToFileTimeUtc(write);
return SysFileSetLastAccessAndWriteTimeUtc(name, af, wf);
}
std::vector<File::DirEntry> File::GetDirEntries(const std::filesystem::path& path) {
if (IsArchivePath(path)) {
return GetArchiveDirEntries(path);
}
std::vector<sys_dir_entry_t> files;
SysFileGetDents(path, files);
@@ -463,12 +577,18 @@ std::vector<File::DirEntry> File::GetDirEntries(const std::filesystem::path& pat
bool File::CopyFile(const std::filesystem::path& src,
const std::filesystem::path& dst) // @suppress("Member declaration not found")
{
if (IsArchivePath(src) || IsArchivePath(dst)) {
return false;
}
return SysFileCopyFile(src, dst);
}
bool File::RenameFile(const std::filesystem::path& src,
const std::filesystem::path& dst) // @suppress("Member declaration not found")
{
if (IsArchivePath(src) || IsArchivePath(dst)) {
return false;
}
if (IsFileExisting(dst)) {
DeleteFile(dst); // @suppress("Invalid arguments")
}
@@ -477,6 +597,9 @@ bool File::RenameFile(const std::filesystem::path& src,
}
void File::RemoveReadonly(const std::filesystem::path& name) {
if (IsArchivePath(name)) {
return;
}
SysFileRemoveReadonly(name);
}
+6
View File
@@ -7,6 +7,7 @@
#include <cstddef>
#include <filesystem>
#include <memory>
#include <optional>
#include <string>
#include <vector>
@@ -34,6 +35,10 @@ public:
std::string name;
bool is_file;
};
struct Info {
bool is_file;
uint64_t size;
};
File();
explicit File(const std::filesystem::path& name);
@@ -72,6 +77,7 @@ public:
std::vector<std::byte> ReadWholeBuffer();
static uint64_t Size(const std::filesystem::path& name);
static std::optional<Info> GetInfo(const std::filesystem::path& name);
static bool IsDirectoryExisting(const std::filesystem::path& path);
static bool IsFileExisting(const std::filesystem::path& name);
+1
View File
@@ -45,6 +45,7 @@ bool SysFileTruncate(sys_file_t& f, uint64_t size); // NOLINT(google-runt
bool SysFileUnlink(sys_file_t& f,
const std::filesystem::path& name); // NOLINT(google-runtime-references)
uint64_t SysFileSize(const std::filesystem::path& file_name);
bool SysFileGetInfo(const std::filesystem::path& name, bool* is_file, uint64_t* size);
bool SysFileIsError(sys_file_t& f); // NOLINT(google-runtime-references)
bool SysFileIsDirectoryExisting(const std::filesystem::path& path);
bool SysFileIsFileExisting(const std::filesystem::path& name);
+21 -24
View File
@@ -54,7 +54,7 @@ struct sys_file_t {
#endif
static std::filesystem::path get_internal_name(const std::filesystem::path& name) {
return name.is_absolute() ? name : (std::filesystem::path(".") / name);
return name.empty() || name.is_absolute() ? name : (std::filesystem::path(".") / name);
}
// Pass access-pattern hints to the host.
@@ -297,10 +297,21 @@ uint64_t SysFileSize(sys_file_t& f) {
return 0;
}
bool SysFileGetInfo(const std::filesystem::path& name, bool* is_file, uint64_t* size) {
const auto path = get_internal_name(name);
struct stat info {};
if (stat(path.c_str(), &info) != 0) {
return false;
}
*is_file = !S_ISDIR(info.st_mode);
*size = *is_file ? static_cast<uint64_t>(info.st_size) : 0;
return true;
}
uint64_t SysFileSize(const std::filesystem::path& file_name) {
sys_file_t* f = SysFileOpenR(file_name);
uint64_t size = SysFileSize(*f);
SysFileClose(f);
bool is_file;
uint64_t size = 0;
SysFileGetInfo(file_name, &is_file, &size);
return size;
}
@@ -353,29 +364,15 @@ bool SysFileIsError(sys_file_t& f) {
}
bool SysFileIsDirectoryExisting(const std::filesystem::path& path) {
auto real_name = get_internal_name(path);
auto real_name_str = real_name.string();
struct stat s {};
if (0 != stat(real_name_str.c_str(), &s)) {
return false;
}
return S_ISDIR(s.st_mode); // NOLINT
bool is_file;
uint64_t size;
return SysFileGetInfo(path, &is_file, &size) && !is_file;
}
bool SysFileIsFileExisting(const std::filesystem::path& name) {
auto real_name = get_internal_name(name);
auto real_name_str = real_name.string();
struct stat s {};
if (0 != stat(real_name_str.c_str(), &s)) {
return false;
}
return !S_ISDIR(s.st_mode); // NOLINT
bool is_file;
uint64_t size;
return SysFileGetInfo(name, &is_file, &size) && is_file;
}
bool SysFileCreateDirectory(const std::filesystem::path& path) {
+25 -19
View File
@@ -266,19 +266,27 @@ uint64_t SysFileSize(sys_file_t& f) {
return 0;
}
uint64_t SysFileSize(const std::filesystem::path& file_name) {
LARGE_INTEGER s;
WIN32_FILE_ATTRIBUTE_DATA a;
auto wide = file_name.wstring();
if (GetFileAttributesExW(wide.c_str(), GetFileExInfoStandard, &a) == 0) {
return 0;
bool SysFileGetInfo(const std::filesystem::path& name, bool* is_file, uint64_t* size) {
const bool directory_path = !name.empty() && !name.has_filename() && name != name.root_path();
WIN32_FILE_ATTRIBUTE_DATA info {};
if (GetFileAttributesExW(directory_path ? name.parent_path().c_str() : name.c_str(),
GetFileExInfoStandard, &info) == 0) {
return false;
}
const bool file = (info.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == 0;
if (directory_path && file) {
return false;
}
*is_file = file;
*size = *is_file ? ((static_cast<uint64_t>(info.nFileSizeHigh) << 32u) | info.nFileSizeLow) : 0;
return true;
}
s.HighPart = static_cast<LONG>(a.nFileSizeHigh);
s.LowPart = a.nFileSizeLow;
return s.QuadPart;
uint64_t SysFileSize(const std::filesystem::path& file_name) {
bool is_file;
uint64_t size = 0;
SysFileGetInfo(file_name, &is_file, &size);
return size;
}
bool SysFileTruncate(sys_file_t& f, uint64_t size) {
@@ -346,17 +354,15 @@ bool SysFileIsError(sys_file_t& f) {
}
bool SysFileIsDirectoryExisting(const std::filesystem::path& path) {
auto wide = path.wstring();
DWORD a = GetFileAttributesW(wide.c_str());
return a != INVALID_FILE_ATTRIBUTES &&
((a & static_cast<DWORD>(FILE_ATTRIBUTE_DIRECTORY)) != 0u);
bool is_file;
uint64_t size;
return SysFileGetInfo(path, &is_file, &size) && !is_file;
}
bool SysFileIsFileExisting(const std::filesystem::path& name) {
auto wide = name.wstring();
DWORD a = GetFileAttributesW(wide.c_str());
return a != INVALID_FILE_ATTRIBUTES &&
((a & static_cast<DWORD>(FILE_ATTRIBUTE_DIRECTORY)) == 0u);
bool is_file;
uint64_t size;
return SysFileGetInfo(name, &is_file, &size) && is_file;
}
bool SysFileCreateDirectory(const std::filesystem::path& path) {
+2 -1
View File
@@ -1502,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);
+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 {
+52 -46
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;
@@ -193,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) {
@@ -219,14 +222,6 @@ TextureCache::BindingType TextureCache::UploadBinding(const Image& image) {
return image.usage.storage ? BindingType::Storage : BindingType::Texture;
}
bool TextureCache::SafeToDownload(const Image& image) {
if (!image.SafeToDownload()) {
return false;
}
const auto range = image.info.data;
return !m_buffer_cache.HasGpuDirtyBytes(range.address, range.size);
}
ImageId TextureCache::InsertImage(const ImageInfo& info) {
const auto id = m_slot_images.insert(m_graphics, m_scheduler, info);
if (!info.data.Empty()) {
@@ -639,9 +634,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)) {
@@ -675,7 +673,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();
@@ -689,8 +688,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 &&
@@ -701,7 +700,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 &&
@@ -810,12 +809,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.
@@ -1118,9 +1120,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;
@@ -1128,18 +1131,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;
@@ -1147,7 +1150,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.
@@ -1159,15 +1162,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;
@@ -1312,7 +1315,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};
@@ -1349,7 +1352,7 @@ ImageId TextureCache::FindImageFromRange(uint64_t address, uint64_t size, bool e
if (ensure_valid && owner->depth_id) {
owner = m_slot_images.try_get(owner->depth_id);
}
if (owner == nullptr || (ensure_valid && !SafeToDownload(*owner))) {
if (owner == nullptr || (ensure_valid && !owner->SafeToDownload())) {
continue;
}
matches.push_back(id);
@@ -1740,7 +1743,7 @@ bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uin
std::scoped_lock lock {m_texture_cache.m_lock};
auto& image = m_texture_cache.m_slot_images[selected];
// The GPU thread owns image retirement; CPU invalidation can dirty this image after lookup.
if (!m_texture_cache.SafeToDownload(image)) {
if (!image.SafeToDownload()) {
return false;
}
if (!buffer.IsInBounds(image.info.data.address, 1)) {
@@ -1803,7 +1806,7 @@ bool TextureCache::DownloadImageMemory(ImageId id) {
return false;
}
auto transfer = BuildDownload(image);
if (!transfer.valid || !SafeToDownload(image)) {
if (!transfer.valid || !image.SafeToDownload()) {
return false;
}
const auto range = image.info.data;
@@ -1864,7 +1867,10 @@ bool TextureCache::IsRegionGpuModified(uint64_t address, uint64_t size) {
std::scoped_lock lock {m_lock};
for (const auto id: FindImagesInRegion(address, size, false)) {
const auto& image = m_slot_images[id];
if (!image.depth_id && image.IsGpuModified()) {
// PPSA17168: S_LOAD_DWORD reads shader data at an address overlapping an old
// render target whose memory the CPU has reused. The cached image still retains
// its earlier GPU-modified flag.
if (!image.depth_id && image.IsGpuModified() && !image.IsDefinitelyCpuDirty()) {
return true;
}
}
@@ -1990,7 +1996,7 @@ void TextureCache::RunGarbageCollector() {
continue;
}
if (owner->IsGpuModified()) {
const bool safe = SafeToDownload(*owner);
const bool safe = owner->SafeToDownload();
if (safe && owner->info.IsTiled()) {
continue;
}
+3 -4
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;
@@ -128,7 +128,6 @@ private:
[[nodiscard]] static bool SameBacking(const ImageInfo& cached, const ImageInfo& requested,
bool exact_format);
[[nodiscard]] static BindingType UploadBinding(const Image& image);
[[nodiscard]] bool SafeToDownload(const Image& image);
// Caller holds m_lock; it also serializes the per-image query epoch.
[[nodiscard]] ImageIds FindImagesInRegion(uint64_t address, uint64_t size,
@@ -139,7 +138,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) {
@@ -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;
}
@@ -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 {
@@ -91,19 +91,6 @@ vk::DescriptorImageInfo MakeImageInfo(const TextureBinding& texture, uint32_t el
return {nullptr, view, texture.layout};
}
static const char* ShaderStageResourceName(ShaderType stage) {
switch (stage) {
case ShaderType::Vertex: return "Vertex";
case ShaderType::Mesh: return "Mesh";
case ShaderType::Local: return "Local";
case ShaderType::TessellationControl: return "Hull";
case ShaderType::TessellationEvaluation: return "Domain";
case ShaderType::Pixel: return "Pixel";
case ShaderType::Compute: return "Compute";
default: return "Unknown";
}
}
static Prospero::ImageType TextureType(const ShaderTextureResource& descriptor) {
const auto type = descriptor.Type();
return type == Prospero::ImageType::kCube ? Prospero::ImageType::kColor2DArray : type;
@@ -126,8 +113,7 @@ static bool IsMultisampledTexture(Prospero::ImageType type) {
static vk::DescriptorBufferInfo
NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource& source,
const ShaderRecompiler::IR::BufferResource& resource, ShaderType stage,
uint32_t slot, uint32_t& buffer_offset) {
const ShaderRecompiler::IR::BufferResource& resource, uint32_t& buffer_offset) {
buffer_offset = 0;
const auto& [address, size, id] = source;
@@ -152,16 +138,6 @@ NativeStorageBuffer(RenderContext& context, const PreparedBindings::BufferSource
if (resource.written) {
context.GetTextureCache().InvalidateMemoryFromGPU(address, size);
}
const char* access = "Read";
if (resource.written && resource.read) {
access = "ReadWrite";
} else if (resource.written) {
access = "Write";
}
SetVulkanObjectNameF(
graphics.device, result.buffer,
"Kyty.{}.StorageBuffer[slot={} guest=0x{:016x} size=0x{:x} access={} formatted={}]",
ShaderStageResourceName(stage), slot, address, size, access, resource.formatted);
return result;
}
@@ -196,8 +172,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;
}
@@ -514,19 +490,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,
@@ -560,7 +567,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;
@@ -602,12 +609,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,
@@ -679,6 +687,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);
@@ -705,14 +714,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,
@@ -798,9 +808,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) {
@@ -818,10 +833,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);
@@ -830,10 +845,11 @@ 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,
program.info.buffers[resource],
buffer_offset));
pack_memory_offset(i, buffer_offset);
}
@@ -1045,8 +1061,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 -23
View File
@@ -575,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 {
@@ -713,9 +718,6 @@ static PreparedVertexBuffers AcquireVertexBuffers(CommandBuffer& b
Libs::LibKernel::Memory::ClampRangeSize(range.base_address, range.RequestedSize());
range.acquired_end = range.base_address + size;
range.binding = cache.ObtainBuffer(range.base_address, size, false);
SetVulkanObjectNameF(
buffer.GetContext().GetGraphics().device, range.binding.first->Handle(),
"Kyty.VertexBufferRange[guest=0x{:016x} size=0x{:x}]", range.base_address, size);
}
// Rebuild slot bindings, offsetting non-empty slots into their acquired merged range.
@@ -747,10 +749,6 @@ static PreparedVertexBuffers AcquireVertexBuffers(CommandBuffer& b
prepared.buffers[i] = range->binding.first->Handle();
prepared.offsets[i] = range->binding.second + vertex.addr - range->base_address;
prepared.sizes[i] = std::min(size, range->acquired_end - vertex.addr);
SetVulkanObjectNameF(
buffer.GetContext().GetGraphics().device, prepared.buffers[i],
"Kyty.VertexBuffer[slot={} guest=0x{:016x} size=0x{:x} stride={} records={}]", i,
vertex.addr, size, vertex.stride, vertex.num_records);
}
return prepared;
@@ -847,7 +845,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();
@@ -861,7 +859,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)
@@ -880,8 +878,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) {
@@ -890,6 +892,7 @@ bool RenderExecutor::PrepareDrawRenderState(CommandBuffer& buffer, const DrawCal
}
}
}
mrt_mask &= color_output_mask;
if (draw.IsIndexed()) {
LogDrawPhase(draw.Name(), "ResolveRenderColorTarget");
}
@@ -927,17 +930,11 @@ static PreparedIndexBuffer PrepareIndexBuffer(CommandBuffer& buffe
auto& stream = buffer.GetContext().GetBufferCache().GetUtilityBuffer(MemoryUsage::Stream);
prepared.offset = stream.Copy(source.host_data, source.size, 16);
prepared.buffer = stream.Handle();
SetVulkanObjectNameF(buffer.GetContext().GetGraphics().device, prepared.buffer,
"Kyty.IndexBuffer[guest=transient size=0x{:x} type={}]", source.size,
static_cast<uint32_t>(source.type));
} else {
auto [buffer_ptr, offset] =
buffer.GetContext().GetBufferCache().ObtainBuffer(source.address, source.size, false);
prepared.buffer = buffer_ptr->Handle();
prepared.offset = offset;
SetVulkanObjectNameF(buffer.GetContext().GetGraphics().device, prepared.buffer,
"Kyty.IndexBuffer[guest=0x{:016x} size=0x{:x} type={}]",
source.address, source.size, static_cast<uint32_t>(source.type));
}
return prepared;
}
+1 -1
View File
@@ -172,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);
}
}
@@ -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) {
@@ -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()) ||
@@ -307,6 +307,11 @@ uint32_t EmitConvertF32F64(EmitterState& state, uint32_t arg0) {
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) {
return EmitNative<spv::OpCompositeExtract, IR::Type::U32>(state, arg0, arg1.U32());
}
@@ -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);
}
@@ -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.
@@ -29,7 +31,9 @@ uint32_t EmitConvertSigned32ToFloat(EmitterState& state, uint32_t arg0) {
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)
@@ -112,18 +116,34 @@ 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)
@@ -182,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);
@@ -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),
@@ -684,8 +684,8 @@ void DefineModule(EmitterState& state) {
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeSignedZeroInfNanPreserve,
32u);
if (state.requirements.float64) {
EXIT_NOT_IMPLEMENTED(state.program.stage != ShaderType::Compute);
EXIT_NOT_IMPLEMENTED(state.input_info.compute->float_mode != 0xc0);
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);
@@ -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;
}
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);
@@ -156,6 +156,8 @@ enum class Opcode {
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,
@@ -89,6 +89,8 @@ constexpr OpcodeMap VOP1_OPCODE_LIST[] = {
{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},
@@ -151,6 +153,8 @@ constexpr OpcodeMap VOP3_ENCODED_VOP1_OPCODE_LIST[] = {
{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},
@@ -461,6 +465,7 @@ 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:
@@ -537,6 +542,7 @@ constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
{Opcode::V_NOT_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(),
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},
@@ -720,6 +726,7 @@ 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;
@@ -1379,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) {
@@ -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)));
}
@@ -317,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),
@@ -699,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)));
}
@@ -746,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 {
@@ -867,7 +876,9 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
include_vector(inst.src2);
include_vector(inst.src3);
switch (inst.opcode) {
case Decoder::Opcode::V_CVT_F64_I32: include_vector(inst.dst, 2u); break;
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]];
@@ -1272,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)) {
@@ -1297,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
@@ -299,6 +299,8 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
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;
@@ -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++) {
+5 -2
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;
};
@@ -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;
@@ -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)
@@ -65,7 +66,9 @@ 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)
@@ -274,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)
@@ -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++) {
@@ -590,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;
@@ -602,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;
@@ -857,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 {
@@ -916,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;
}
@@ -936,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;
@@ -1100,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;
}
@@ -1127,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;
@@ -1153,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.
+43 -14
View File
@@ -1,5 +1,6 @@
#include "kernel/fileSystem.h"
#include "common/archive.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/dateTime.h"
@@ -41,6 +42,7 @@ public:
struct MountPair {
std::filesystem::path dir;
std::string point;
std::shared_ptr<Common::ArchiveReader> archive;
};
MountPoints() { EXIT_NOT_IMPLEMENTED(!Common::Thread::IsMainThread()); }
@@ -260,6 +262,7 @@ void MountPoints::Mount(const std::filesystem::path& folder, const std::string&
MountPair p;
p.dir = folder;
p.point = point_str;
p.archive = Common::OpenArchive(folder);
m_mount_pairs.push_back(std::move(p));
}
@@ -346,11 +349,13 @@ std::filesystem::path MountPoints::ResolvePath(const std::string& mounted_name)
}
const auto native_rel_path = Common::PathFromUtf8(rel_path);
if (Common::IsArchivePath(p.dir)) {
return p.dir / native_rel_path;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
if (HasWindowsForbiddenFilenameCharacter(rel_path)) {
::printf("FileSystem: Windows-incompatible guest filename: %s\n",
mounted_name.c_str());
::printf("FileSystem: Windows-incompatible guest filename: %s\n", mounted_name.c_str());
}
return p.dir / native_rel_path;
#else
@@ -358,7 +363,8 @@ std::filesystem::path MountPoints::ResolvePath(const std::string& mounted_name)
#endif
}
return mounted_name;
// Unmounted guest paths must not fall through to the host filesystem.
return {};
}
void Initialize() {
@@ -461,14 +467,24 @@ int KYTY_SYSV_ABI KernelOpen(const char* path, int flags, uint16_t mode) {
}
file->real_name = g_mount_points->ResolvePath(file->name);
if (file->real_name.empty()) {
g_files->DeleteDescriptor(descriptor);
return KERNEL_ERROR_ENOENT;
}
if (Common::IsArchivePath(file->real_name) &&
(rw_mode != Common::File::Mode::Read || trunc || creat)) {
g_files->DeleteDescriptor(descriptor);
return KERNEL_ERROR_EROFS;
}
if (trunc && rw_mode == Common::File::Mode::Read) {
g_files->DeleteDescriptor(descriptor);
return KERNEL_ERROR_EACCES;
}
bool dir_exist = Common::File::IsDirectoryExisting(file->real_name);
bool file_exist = Common::File::IsFileExisting(file->real_name);
const auto info = Common::File::GetInfo(file->real_name);
const bool dir_exist = info && !info->is_file;
const bool file_exist = info && info->is_file;
// A missing path opened without O_CREAT is ENOENT
if (!creat && !dir_exist && !file_exist) {
@@ -666,7 +682,7 @@ int64_t KYTY_SYSV_ABI KernelWrite(int d, const void* buf, size_t nbytes) {
auto* file = g_files->GetFile(d);
if (file == nullptr) {
if (file == nullptr || !file->opened || !file->writable) {
return KERNEL_ERROR_EBADF;
}
@@ -881,7 +897,7 @@ int64_t KYTY_SYSV_ABI KernelPwrite(int d, const void* buf, size_t nbytes, int64_
auto* file = g_files->GetFile(d);
if (file == nullptr) {
if (file == nullptr || !file->opened || !file->writable) {
return KERNEL_ERROR_EBADF;
}
@@ -1058,15 +1074,13 @@ int KYTY_SYSV_ABI KernelStat(const char* path, FileStat* sb) {
auto real_file_name = g_mount_points->ResolvePath(path);
bool is_dir = Common::File::IsDirectoryExisting(real_file_name);
bool is_file = Common::File::IsFileExisting(real_file_name);
if (!is_dir && !is_file) {
const auto info = Common::File::GetInfo(real_file_name);
if (!info) {
LOGF("\t file not found\n");
return KERNEL_ERROR_ENOENT;
}
EXIT_NOT_IMPLEMENTED(is_dir && is_file);
const bool is_dir = !info->is_file;
FileStat stat {};
stat.st_mode = 0000777u | (is_dir ? 0040000u : 0100000u);
@@ -1080,7 +1094,7 @@ int KYTY_SYSV_ABI KernelStat(const char* path, FileStat* sb) {
stat.st_blksize = 512;
stat.st_blocks = stat.st_size / 512;
} else {
stat.st_size = static_cast<int64_t>(Common::File::Size(real_file_name));
stat.st_size = static_cast<int64_t>(info->size);
stat.st_blksize = 512;
stat.st_blocks = (stat.st_size + 511) / 512;
@@ -1217,6 +1231,9 @@ int KYTY_SYSV_ABI KernelUnlink(const char* path) {
}
auto real_file_name = g_mount_points->ResolvePath(path);
if (Common::IsArchivePath(real_file_name)) {
return KERNEL_ERROR_EROFS;
}
bool is_dir = Common::File::IsDirectoryExisting(real_file_name);
bool is_file = Common::File::IsFileExisting(real_file_name);
@@ -1254,8 +1271,11 @@ int KYTY_SYSV_ABI KernelRename(const char* from, const char* to) {
auto to_path = std::string(to);
auto real_from = g_mount_points->ResolvePath(from_path);
auto real_to = g_mount_points->ResolvePath(to_path);
if (Common::IsArchivePath(real_from) || Common::IsArchivePath(real_to)) {
return KERNEL_ERROR_EROFS;
}
if (!Common::File::IsFileExisting(real_from)) {
if (real_to.empty() || !Common::File::IsFileExisting(real_from)) {
return KERNEL_ERROR_ENOENT;
}
@@ -1335,6 +1355,12 @@ int KYTY_SYSV_ABI KernelMkdir(const char* path, uint16_t mode) {
path, mode);
auto real_name = g_mount_points->ResolvePath(std::string(path));
if (real_name.empty()) {
return KERNEL_ERROR_ENOENT;
}
if (Common::IsArchivePath(real_name)) {
return KERNEL_ERROR_EROFS;
}
if (Common::File::IsDirectoryExisting(real_name)) {
return KERNEL_ERROR_EEXIST;
@@ -1361,6 +1387,9 @@ int KYTY_SYSV_ABI KernelRmdir(const char* path) {
LOGF("\t path = %s\n", path);
auto real_name = g_mount_points->ResolvePath(std::string(path));
if (Common::IsArchivePath(real_name)) {
return KERNEL_ERROR_EROFS;
}
if (!Common::File::IsDirectoryExisting(real_name)) {
return KERNEL_ERROR_ENOENT;
+1
View File
@@ -54,6 +54,7 @@ struct Lifecycle {
void Mount(const std::filesystem::path& folder, const std::string& point);
void Umount(const std::string& folder_or_point);
// Returns an empty path when the guest path has no mounted filesystem.
std::filesystem::path GetRealFilename(const std::string& mounted_file_name);
int KYTY_SYSV_ABI KernelOpen(const char* path, int flags, uint16_t mode);
+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");
+1 -1
View File
@@ -20,7 +20,7 @@ add_executable(launcher WIN32
)
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static kyty_git_version)
target_link_libraries(launcher Qt6::Concurrent Qt6::Network Qt6::Widgets Vulkan::Headers SDL3::SDL3-static kyty_git_version common)
if(WIN32)
target_link_libraries(launcher setupapi)
endif()
+3 -1
View File
@@ -1,6 +1,7 @@
#ifndef CONFIGURATION_ITEM_H
#define CONFIGURATION_ITEM_H
#include <QIcon>
#include <QObject>
#include <QTreeWidgetItem>
@@ -18,7 +19,7 @@ public:
explicit ConfigurationItem(std::unique_ptr<Configuration> info, QTreeWidget* parent);
~ConfigurationItem() override;
void Update();
void Update(bool reload_icon = false);
bool operator<(const QTreeWidgetItem& other) const override;
Configuration& GetInfo() { return *m_info; }
@@ -35,6 +36,7 @@ private:
void UpdateStatusIndicator();
std::unique_ptr<Configuration> m_info;
QIcon m_icon;
bool m_running = false;
QComboBox* m_status_combo = nullptr;
QLabel* m_status_indicator = nullptr;
+28
View File
@@ -0,0 +1,28 @@
#ifndef GAME_CONTENT_H
#define GAME_CONTENT_H
#include <QByteArray>
#include <QString>
#include <QStringList>
#include <cstdint>
#include <filesystem>
namespace GameContent {
inline constexpr uint64_t MaxMetadataSize = uint64_t {1} << 20u;
inline constexpr uint64_t MaxImageSize = uint64_t {32} << 20u;
inline constexpr uint64_t MaxTrophyPackageSize = uint64_t {128} << 20u;
[[nodiscard]] std::filesystem::path ToPath(const QString& path);
[[nodiscard]] QString FromPath(const std::filesystem::path& path);
[[nodiscard]] bool IsArchive(const QString& base);
[[nodiscard]] std::filesystem::path Resolve(const QString& base, const QString& relative);
[[nodiscard]] bool FileExists(const QString& base, const QString& relative);
[[nodiscard]] QByteArray ReadPath(const std::filesystem::path& path, uint64_t max_size);
[[nodiscard]] QByteArray ReadFile(const QString& base, const QString& relative, uint64_t max_size);
[[nodiscard]] QStringList ListFiles(const QString& base, const QString& relative);
} // namespace GameContent
#endif // GAME_CONTENT_H
+8 -5
View File
@@ -1,6 +1,8 @@
#ifndef GAME_LIST_TREE_WIDGET_H
#define GAME_LIST_TREE_WIDGET_H
#include "gameContent.h"
#include <QPaintEvent>
#include <QPainter>
#include <QPalette>
@@ -17,14 +19,15 @@ public:
viewport()->setAutoFillBackground(false);
}
void SetBackgroundImage(const QString& path) {
if (m_path == path) {
void SetBackgroundImage(const QString& key) {
if (m_path == key) {
return;
}
m_path = path;
m_path = key;
m_source = QPixmap();
if (!path.isEmpty()) {
m_source.load(path);
if (!key.isEmpty()) {
m_source.loadFromData(GameContent::ReadFile(key, QStringLiteral("sce_sys/pic0.png"),
GameContent::MaxImageSize));
}
UpdateScaledBackground();
viewport()->update();
@@ -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();
}
+20 -6
View File
@@ -1,6 +1,7 @@
#include "configurationItem.h"
#include "configuration.h"
#include "gameContent.h"
#include <QApplication>
#include <QComboBox>
@@ -14,6 +15,7 @@
#include <QLabel>
#include <QLineEdit>
#include <QLocale>
#include <QPixmap>
#include <QSize>
#include <QStringList>
#include <QStyle>
@@ -154,7 +156,7 @@ ConfigurationItem::ConfigurationItem(std::unique_ptr<Configuration> info, QTreeW
m_comment_edit->setFrame(false);
parent->setItemWidget(this, CommentsColumn, m_comment_edit);
Update();
Update(true);
SetRunning(false);
setData(SizeColumn, Qt::UserRole, qint64(-1));
@@ -170,7 +172,14 @@ ConfigurationItem::ConfigurationItem(std::unique_ptr<Configuration> info, QTreeW
watcher->deleteLater();
});
watcher->setFuture(QtConcurrent::run([path = m_info->basedir]() -> qint64 {
if (path.isEmpty() || !QDir(path).exists()) {
if (path.isEmpty()) {
return -1;
}
const QFileInfo info(path);
if (GameContent::IsArchive(path)) {
return info.size();
}
if (!QDir(path).exists()) {
return -1;
}
qint64 bytes = 0;
@@ -188,7 +197,7 @@ ConfigurationItem::ConfigurationItem(std::unique_ptr<Configuration> info, QTreeW
ConfigurationItem::~ConfigurationItem() = default;
void ConfigurationItem::Update() {
void ConfigurationItem::Update(bool reload_icon) {
const auto display_text = GetDisplayText(*m_info);
const auto path = GetPathText(*m_info);
@@ -209,6 +218,12 @@ void ConfigurationItem::Update() {
m_comment_edit->setText(m_info->game_comment);
}
if (reload_icon) {
const auto icon_data = GameContent::ReadFile(
m_info->basedir, QStringLiteral("sce_sys/icon0.png"), GameContent::MaxImageSize);
QPixmap icon;
m_icon = !icon_data.isEmpty() && icon.loadFromData(icon_data) ? QIcon(icon) : QIcon {};
}
UpdateIcon();
UpdateStatusIndicator();
}
@@ -267,9 +282,8 @@ void ConfigurationItem::SetCompatibilityEditable(bool editable) {
}
void ConfigurationItem::UpdateIcon() {
const QString icon_file = QDir(m_info->basedir).filePath(QStringLiteral("sce_sys/icon0.png"));
if (QFileInfo::exists(icon_file)) {
setIcon(NameColumn, QIcon(icon_file));
if (!m_icon.isNull()) {
setIcon(NameColumn, m_icon);
return;
}
+106 -84
View File
@@ -1,9 +1,11 @@
#include "configurationListWidget.h"
#include "common/archive.h"
#include "compatibilityDatabase.h"
#include "configuration.h"
#include "configurationEditDialog.h"
#include "configurationItem.h"
#include "gameContent.h"
#include "gameListTreeWidget.h"
#include "inputMappingDialog.h"
#include "mainDialog.h"
@@ -127,15 +129,6 @@ static QStringList SettingsStringList(const QVariant& value) {
return text.isEmpty() ? QStringList() : QStringList({text});
}
static QString GetPic0Path(const Configuration& info) {
if (info.basedir.isEmpty()) {
return {};
}
const auto path = QDir(info.basedir).filePath(QStringLiteral("sce_sys/pic0.png"));
return QFileInfo::exists(path) ? QFileInfo(path).absoluteFilePath() : QString();
}
static void ConfigureGameList(Ui::ConfigurationListWidget* ui) {
ui->cfgs_list->setAlternatingRowColors(false);
ui->cfgs_list->setAllColumnsShowFocus(true);
@@ -490,17 +483,16 @@ static QString GetFirmwareVersion(const QJsonObject& root) {
return version;
}
static GameMetadata GetGameMetadata(const QString& param_file, const QString& fallback) {
static GameMetadata GetGameMetadata(const QByteArray& param_data, const QString& fallback) {
GameMetadata ret;
ret.title_name = fallback;
QFile param(param_file);
if (!param.open(QIODevice::ReadOnly)) {
if (param_data.isEmpty()) {
return ret;
}
QJsonParseError parse_error;
const auto doc = QJsonDocument::fromJson(param.readAll(), &parse_error);
const auto doc = QJsonDocument::fromJson(param_data, &parse_error);
if (parse_error.error != QJsonParseError::NoError || !doc.isObject()) {
return ret;
}
@@ -522,18 +514,18 @@ static GameMetadata GetGameMetadata(const QString& param_file, const QString& fa
}
static void SetGameFiles(Configuration& info, const QString& game_dir, const QString& game_path,
const GameMetadata& metadata) {
QDir game(game_dir);
const GameMetadata& metadata, bool archive) {
const QFileInfo game(game_dir);
info.game_path = game_path;
info.basedir = game.absolutePath();
info.basedir = archive ? game.absoluteFilePath() : QDir(game_dir).absolutePath();
info.name = metadata.title_name;
info.title_id = metadata.title_id;
info.gameVersion = metadata.gameVersion;
info.firmwareVer = metadata.firmwareVer;
if (info.name.isEmpty()) {
info.name = game.dirName();
info.name = archive ? game.completeBaseName() : QDir(game_dir).dirName();
}
}
@@ -609,6 +601,94 @@ void ConfigurationListWidget::ScanGameDirectory() {
const QString eboot_name = QStringLiteral("eboot.bin");
QSet<QString> found_games;
const auto add_game = [this, &found_games, &eboot_name,
&running_items](const QString& base, const QString& game_path,
const QString& legacy_game_path, const QString& fallback,
bool archive) {
const QString game_key = PathKey(game_path);
if (game_key.isEmpty() || found_games.contains(game_key)) {
return;
}
// Keep the index and decompression cache alive through validation, metadata and icon
// reads, after rejecting duplicate candidates from overlapping game folders.
const auto reader = archive ? Common::OpenArchive(GameContent::ToPath(base)) : nullptr;
if (archive && (reader == nullptr || !GameContent::FileExists(base, eboot_name))) {
return;
}
found_games.insert(game_key);
const auto metadata =
GetGameMetadata(GameContent::ReadFile(base, QStringLiteral("sce_sys/param.json"),
GameContent::MaxMetadataSize),
fallback);
auto info = std::make_unique<Configuration>();
info->custom_settings =
FindCustomInfo(&m_custom_infos, game_path, legacy_game_path) != nullptr;
SetGameFiles(*info, base, game_path, metadata, archive);
const auto* compatibility = m_compatibility->Find(info->title_id);
if (compatibility != nullptr) {
info->game_status = compatibility->status;
info->game_comment = compatibility->comment;
}
if (auto* item = running_items.value(game_key); item != nullptr) {
// Refresh metadata without losing the running row's identity.
const QSignalBlocker status_blocker(item->GetStatusCombo());
item->GetInfo().CopyGameInfoFrom(*info);
item->Update(true);
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
!item->GetInfo().title_id.trimmed().isEmpty());
return;
}
auto* item = new ConfigurationItem(std::move(info), m_ui->cfgs_list);
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
!item->GetInfo().title_id.trimmed().isEmpty());
connect(item->GetStatusCombo(), &QComboBox::currentIndexChanged, item,
[this, item](int /*index*/) {
if (!m_compatibility->IsLocal()) {
return;
}
const auto& title_id = item->GetInfo().title_id;
if (title_id.trimmed().isEmpty()) {
return;
}
item->GetInfo().game_status = static_cast<Configuration::GameStatus>(
item->GetStatusCombo()->currentData().toInt());
item->Update();
m_compatibility->SetStatus(title_id, item->GetInfo().game_status);
m_ui->cfgs_list->setCurrentItem(item);
SelectItem(item);
});
connect(item->GetCommentEdit(), &QLineEdit::editingFinished, item, [this, item]() {
if (!m_compatibility->IsLocal()) {
return;
}
const auto& title_id = item->GetInfo().title_id;
if (title_id.trimmed().isEmpty()) {
return;
}
item->GetInfo().game_comment = item->GetCommentEdit()->text();
item->Update();
m_compatibility->SetComment(title_id, item->GetInfo().game_comment);
m_ui->cfgs_list->setCurrentItem(item);
SelectItem(item);
});
};
const auto scan_archives = [&add_game](const QDir& directory, const QDir& root) {
const auto entries = directory.entryInfoList(QDir::Files | QDir::NoSymLinks, QDir::Name);
for (const auto& archive: entries) {
const auto base = archive.absoluteFilePath();
if (!Common::IsSupportedArchive(GameContent::ToPath(base))) {
continue;
}
add_game(base, QDir::cleanPath(base), root.relativeFilePath(base),
archive.completeBaseName(), true);
}
};
for (const auto& root_path: m_game_dirs) {
QDir root(root_path);
@@ -616,6 +696,7 @@ void ConfigurationListWidget::ScanGameDirectory() {
continue;
}
scan_archives(root, root);
QList<QDir> pending_dirs;
const auto root_subdirs =
root.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks);
@@ -625,74 +706,13 @@ void ConfigurationListWidget::ScanGameDirectory() {
while (!pending_dirs.isEmpty()) {
QDir game_dir = pending_dirs.takeFirst();
scan_archives(game_dir, root);
if (game_dir.exists(eboot_name)) {
const QString game_path = NormalizeGameDirectory(game_dir.absolutePath());
const QString legacy_game_path = root.relativeFilePath(game_dir.absolutePath());
const QString game_key = PathKey(game_path);
if (game_key.isEmpty() || found_games.contains(game_key)) {
continue;
}
found_games.insert(game_key);
auto metadata = GetGameMetadata(
game_dir.filePath(QStringLiteral("sce_sys/param.json")), game_dir.dirName());
auto info = std::make_unique<Configuration>();
info->custom_settings =
FindCustomInfo(&m_custom_infos, game_path, legacy_game_path) != nullptr;
SetGameFiles(*info, game_dir.absolutePath(), game_path, metadata);
const auto* compatibility = m_compatibility->Find(info->title_id);
if (compatibility != nullptr) {
info->game_status = compatibility->status;
info->game_comment = compatibility->comment;
}
if (auto* item = running_items.value(game_key); item != nullptr) {
// Refresh metadata without losing the running row's identity.
const QSignalBlocker status_blocker(item->GetStatusCombo());
item->GetInfo().CopyGameInfoFrom(*info);
item->Update();
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
!item->GetInfo().title_id.trimmed().isEmpty());
continue;
}
auto* item = new ConfigurationItem(std::move(info), m_ui->cfgs_list);
item->SetCompatibilityEditable(m_compatibility->IsLocal() &&
!item->GetInfo().title_id.trimmed().isEmpty());
connect(item->GetStatusCombo(), &QComboBox::currentIndexChanged, item,
[this, item](int /*index*/) {
if (!m_compatibility->IsLocal()) {
return;
}
const auto& title_id = item->GetInfo().title_id;
if (title_id.trimmed().isEmpty()) {
return;
}
item->GetInfo().game_status = static_cast<Configuration::GameStatus>(
item->GetStatusCombo()->currentData().toInt());
item->Update();
m_compatibility->SetStatus(title_id, item->GetInfo().game_status);
m_ui->cfgs_list->setCurrentItem(item);
SelectItem(item);
});
connect(item->GetCommentEdit(), &QLineEdit::editingFinished, item, [this, item]() {
if (!m_compatibility->IsLocal()) {
return;
}
const auto& title_id = item->GetInfo().title_id;
if (title_id.trimmed().isEmpty()) {
return;
}
item->GetInfo().game_comment = item->GetCommentEdit()->text();
item->Update();
m_compatibility->SetComment(title_id, item->GetInfo().game_comment);
m_ui->cfgs_list->setCurrentItem(item);
SelectItem(item);
});
add_game(game_dir.absolutePath(), game_path, legacy_game_path, game_dir.dirName(),
false);
continue;
}
@@ -805,7 +825,8 @@ void ConfigurationListWidget::open_game_folder() {
return;
}
const QDir game_dir(item->GetInfo().basedir);
const auto base = item->GetInfo().basedir;
const QDir game_dir(GameContent::IsArchive(base) ? QFileInfo(base).absolutePath() : base);
if (!game_dir.exists()) {
QMessageBox::warning(this, tr("Open game folder"), tr("Game folder does not exist."));
return;
@@ -896,7 +917,7 @@ void ConfigurationListWidget::SelectItem(QTreeWidgetItem* witem) {
m_ui->edit_button->setEnabled(!item->IsRunning());
m_selected_item = item;
m_ui->cfgs_list->SetBackgroundImage(GetPic0Path(item->GetInfo()));
m_ui->cfgs_list->SetBackgroundImage(item->GetInfo().basedir);
emit Select();
}
@@ -959,7 +980,8 @@ void ConfigurationListWidget::show_context_menu(const QPoint& pos) {
if (item != nullptr) {
action_run->setDisabled(item->IsRunning());
action_open_folder->setDisabled(!QDir(item->GetInfo().basedir).exists());
const auto& base = item->GetInfo().basedir;
action_open_folder->setDisabled(!QDir(base).exists() && !GameContent::IsArchive(base));
action_view_trophies->setDisabled(!has_trophy_data);
action_remove_save_data->setDisabled(item->IsRunning() || save_data_dirs.isEmpty());
action_edit->setDisabled(item->IsRunning());
+80
View File
@@ -0,0 +1,80 @@
#include "gameContent.h"
#include "common/archive.h"
#include "common/file.h"
#include "common/stringUtils.h"
#include <QFileInfo>
#include <limits>
namespace GameContent {
std::filesystem::path ToPath(const QString& path) {
#if defined(_WIN32)
return std::filesystem::path(path.toStdWString());
#else
return std::filesystem::path(path.toStdString());
#endif
}
QString FromPath(const std::filesystem::path& path) {
#if defined(_WIN32)
return QString::fromStdWString(path.wstring());
#else
return QString::fromStdString(Common::PathToString(path));
#endif
}
bool IsArchive(const QString& base) {
const QFileInfo info(base);
return info.isFile() && Common::IsSupportedArchive(ToPath(base));
}
std::filesystem::path Resolve(const QString& base, const QString& relative) {
const auto root = ToPath(base);
return IsArchive(base) ? Common::MakeArchivePath(root, ToPath(relative))
: root / ToPath(relative);
}
bool FileExists(const QString& base, const QString& relative) {
return Common::File::IsFileExisting(Resolve(base, relative));
}
QByteArray ReadFile(const QString& base, const QString& relative, uint64_t max_size) {
return ReadPath(Resolve(base, relative), max_size);
}
QByteArray ReadPath(const std::filesystem::path& path, uint64_t max_size) {
Common::File file(path, Common::File::Mode::Read);
if (file.IsInvalid()) {
return {};
}
const auto size = file.Size();
if (size > max_size || size > static_cast<uint64_t>(std::numeric_limits<int>::max())) {
return {};
}
QByteArray data(static_cast<int>(size), Qt::Uninitialized);
uint32_t bytes_read = 0;
file.Read(data.data(), static_cast<uint32_t>(size), &bytes_read);
return bytes_read == size ? data : QByteArray {};
}
QStringList ListFiles(const QString& base, const QString& relative) {
const auto directory = Resolve(base, relative);
const bool archive = Common::IsArchivePath(directory);
QStringList result;
for (const auto& entry: Common::File::GetDirEntries(directory)) {
if (entry.is_file) {
const auto path = FromPath(directory / Common::PathFromUtf8(entry.name));
if (archive || !QFileInfo(path).isSymLink()) {
result.append(path);
}
}
}
return result;
}
} // namespace GameContent
+3 -2
View File
@@ -3,6 +3,7 @@
#include "configuration.h"
#include "configurationItem.h"
#include "configurationListWidget.h"
#include "gameContent.h"
#include "patchesDialog.h"
#include "updateChecker.h"
@@ -260,7 +261,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
}
QString game = info.basedir;
if (!info.elf.isEmpty()) {
if (!info.elf.isEmpty() && !GameContent::IsArchive(info.basedir)) {
game = QDir(info.basedir).filePath(info.elf);
}
args << "--game" << game;
@@ -507,7 +508,7 @@ void MainDialogPrivate::Update() {
if (run_enabled) {
const auto& info = item->GetInfo();
auto dir = info.basedir;
run_enabled = !dir.isEmpty() && QDir(dir).exists();
run_enabled = !dir.isEmpty() && (QDir(dir).exists() || GameContent::IsArchive(dir));
}
m_ui->widget->SetRunEnabled(run_enabled);
+54 -23
View File
@@ -1,13 +1,14 @@
#include "trophyViewerDialog.h"
#include "common/archive.h"
#include "configuration.h"
#include "gameContent.h"
#include <QAbstractItemView>
#include <QBrush>
#include <QByteArray>
#include <QDialogButtonBox>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QFont>
#include <QHeaderView>
@@ -39,12 +40,14 @@
namespace {
constexpr quint32 UCP_MAGIC = 0xb228c60a;
constexpr quint32 UCP_VERSION = 1;
constexpr int UCP_HEADER_LEN = 0x40;
constexpr int UCP_TOC_SKIP = 0x20;
constexpr int UCP_ENTRY_LEN = 0x40;
constexpr int UCP_NAME_LEN = 0x20;
constexpr quint32 UCP_MAGIC = 0xb228c60a;
constexpr quint32 UCP_VERSION = 1;
constexpr int UCP_HEADER_LEN = 0x40;
constexpr int UCP_TOC_SKIP = 0x20;
constexpr int UCP_ENTRY_LEN = 0x40;
constexpr int UCP_NAME_LEN = 0x20;
constexpr quint32 UCP_MAX_FILES = 4096;
constexpr quint64 UCP_MAX_EXTRACTED_SIZE = quint64 {64} << 20u;
struct UcpEntry {
QString name;
@@ -98,14 +101,22 @@ static QString ReadFixedString(const QByteArray& data, qsizetype offset, qsizety
return QString::fromLatin1(data.constData() + offset, len);
}
static bool IsUsedUcpEntry(const QString& name) {
const auto lower = name.toCaseFolded();
return lower == QStringLiteral("tropconf.json") || lower == QStringLiteral("tropmeta.json") ||
(lower.startsWith(QStringLiteral("tropmeta_")) &&
lower.endsWith(QStringLiteral(".json"))) ||
(lower.startsWith(QStringLiteral("trop")) && lower.endsWith(QStringLiteral(".png")));
}
static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files, QString& error) {
QFile file(file_name);
if (!file.open(QIODevice::ReadOnly)) {
const QByteArray data =
GameContent::ReadPath(GameContent::ToPath(file_name), GameContent::MaxTrophyPackageSize);
if (data.isEmpty()) {
error = QObject::tr("Could not open %1").arg(QDir::toNativeSeparators(file_name));
return false;
}
const QByteArray data = file.readAll();
if (data.size() < UCP_HEADER_LEN) {
error = QObject::tr("%1 is too small to be a trophy package.")
.arg(QFileInfo(file_name).fileName());
@@ -127,14 +138,18 @@ static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files,
}
const auto declared_size = qFromBigEndian<quint64>(data.constData() + 0x08);
if (declared_size > static_cast<quint64>(data.size())) {
if (declared_size < UCP_HEADER_LEN || declared_size > static_cast<quint64>(data.size())) {
error = QObject::tr("%1 is truncated.").arg(QFileInfo(file_name).fileName());
return false;
}
const auto file_count = qFromBigEndian<quint32>(data.constData() + 0x10);
if (file_count > UCP_MAX_FILES) {
error = QObject::tr("%1 contains too many files.").arg(QFileInfo(file_name).fileName());
return false;
}
const auto toc_offset = static_cast<quint64>(qFromBigEndian<quint32>(data.constData() + 0x14));
const auto data_size = static_cast<quint64>(data.size());
const auto data_size = declared_size;
const quint64 table_size = UCP_TOC_SKIP + static_cast<quint64>(file_count) * UCP_ENTRY_LEN;
if (toc_offset > data_size || table_size > data_size - toc_offset ||
@@ -144,6 +159,7 @@ static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files,
return false;
}
quint64 extracted_size = 0;
for (quint32 i = 0; i < file_count; i++) {
const auto entry_offset = static_cast<qsizetype>(toc_offset + UCP_TOC_SKIP +
static_cast<quint64>(i) * UCP_ENTRY_LEN);
@@ -161,9 +177,22 @@ static bool ReadUcp(const QString& file_name, QMap<QString, QByteArray>& files,
.arg(QFileInfo(file_name).fileName(), entry.name);
return false;
}
if (!IsUsedUcpEntry(entry.name)) {
continue;
}
const auto entry_limit = entry.name.endsWith(QStringLiteral(".png"), Qt::CaseInsensitive)
? GameContent::MaxImageSize
: GameContent::MaxMetadataSize;
if (entry.size > entry_limit || entry.size > UCP_MAX_EXTRACTED_SIZE - extracted_size) {
error = QObject::tr("%1 contains an oversized entry for %2.")
.arg(QFileInfo(file_name).fileName(), entry.name);
return false;
}
files.insert(entry.name.toCaseFolded(), data.mid(static_cast<qsizetype>(entry.offset),
static_cast<qsizetype>(entry.size)));
extracted_size += entry.size;
}
return true;
@@ -392,30 +421,31 @@ static QStringList FindTrophyFiles(const Configuration* info) {
return {};
}
const QDir trophy_dir(QDir(info->basedir).filePath(QStringLiteral("sce_sys/trophy2")));
if (!trophy_dir.exists()) {
return {};
QStringList files;
for (const auto& file:
GameContent::ListFiles(info->basedir, QStringLiteral("sce_sys/trophy2"))) {
const auto name = QFileInfo(file).fileName();
if (name.startsWith(QStringLiteral("trophy"), Qt::CaseInsensitive) &&
name.endsWith(QStringLiteral(".ucp"), Qt::CaseInsensitive)) {
files.append(file);
}
}
const auto files =
trophy_dir.entryInfoList({QStringLiteral("Trophy*.ucp"), QStringLiteral("trophy*.ucp")},
QDir::Files | QDir::NoSymLinks, QDir::Name | QDir::IgnoreCase);
files.sort(Qt::CaseInsensitive);
QStringList trophy_files;
QSet<QString> seen;
for (const auto& file: files) {
auto key = file.canonicalFilePath();
auto key = QFileInfo(file).canonicalFilePath();
if (key.isEmpty()) {
key = file.absoluteFilePath();
key = file;
}
key = QDir::cleanPath(key).toCaseFolded();
if (!seen.contains(key)) {
seen.insert(key);
trophy_files.append(file.absoluteFilePath());
trophy_files.append(file);
}
}
return trophy_files;
}
@@ -506,6 +536,7 @@ void TrophyViewerDialog::ShowForGame(const Configuration* info, QWidget* parent)
}
bool TrophyViewerDialog::LoadGame(const Configuration& info, QString& error) {
const auto reader = Common::OpenArchive(GameContent::ToPath(info.basedir));
const auto trophy_files = FindTrophyFiles(&info);
if (trophy_files.isEmpty()) {
error = tr("No trophy package found in sce_sys/trophy2.");
+3 -144
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};
@@ -172,10 +161,8 @@ int KYTY_SYSV_ABI AjmDecAt9ParseConfigData(const void* config_data,
return AJM_ERROR_INVALID_PARAMETER;
}
uint8_t config[ATRAC9_CONFIG_DATA_SIZE] {};
std::memcpy(config, config_data, sizeof(config));
Atrac9CodecInfo codec_info {};
const int init_result = Atrac9InitDecoder(handle, config);
const int init_result = AjmAt9InitDecoder(handle, static_cast<const uint8_t*>(config_data));
const int info_result =
init_result == 0 ? Atrac9GetCodecInfo(handle, &codec_info) : init_result;
Atrac9ReleaseHandle(handle);
@@ -196,134 +183,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 +235,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;
}
+14 -1
View File
@@ -18,6 +18,19 @@ extern "C" {
namespace Libs::Audio::Ajm {
inline int AjmAt9InitDecoder(void* handle, const uint8_t* config_data) {
// Keep the guest configuration unchanged for metadata and decoder resets.
uint8_t config[ATRAC9_CONFIG_DATA_SIZE];
std::memcpy(config, config_data, sizeof(config));
const auto channel_config = (config[1] >> 1u) & 7u;
if (channel_config >= 6u) {
// LibAtrac9 lacks vibration layouts; their blocks match ordinary mono/dual-mono.
// Translate only the channel field, preserving the other configuration bits.
config[1] -= 6u << 1u;
}
return Atrac9InitDecoder(handle, config);
}
struct AjmDecAt9InitializeParameters {
uint8_t config_data[ATRAC9_CONFIG_DATA_SIZE];
uint32_t reserved;
@@ -278,7 +291,7 @@ private:
std::memcpy(m_config_data, config_data, ATRAC9_CONFIG_DATA_SIZE);
m_has_config = true;
const int init_ret = Atrac9InitDecoder(m_handle, m_config_data);
const int init_ret = AjmAt9InitDecoder(m_handle, m_config_data);
if (init_ret != 0) {
m_is_initialized = false;
result->result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA;
+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;
};
}
+32 -5
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"
@@ -30,6 +32,7 @@ constexpr int AUDIO_OUT_PORT_TYPE_AUDIO3D = 126;
constexpr int AUDIO_OUT_PORT_TYPE_AUX = 127;
constexpr uint32_t AUDIO_OUT_PARAM_FORMAT_MASK = 0x000000ffu;
constexpr uint64_t AUDIO_OUT_TARGET_LATENCY_US = 40000;
constexpr int AUDIO_IN_SILENT_STATE_DEVICE_NONE = 0x1;
constexpr uint32_t AUDIO_IN_GRAIN_MAX_ASYNC = 384;
@@ -105,7 +108,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 +272,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);
@@ -363,11 +369,11 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
uint32_t min_queued_size = 0;
if (blocking) {
constexpr uint64_t target_latency_us = 40000;
const auto buffer_us = port->freq != 0 ? (1000000ULL * port->samples_num) / port->freq : 0;
const auto buffers =
buffer_us != 0 ? static_cast<uint32_t>((target_latency_us + buffer_us - 1) / buffer_us)
: 2u;
buffer_us != 0
? static_cast<uint32_t>((AUDIO_OUT_TARGET_LATENCY_US + buffer_us - 1) / buffer_us)
: 2u;
min_queued_size = prepared_size * std::clamp(buffers, 2u, 16u);
const auto wait_start = LibKernel::KernelGetProcessTime();
auto queued = SDL_GetAudioStreamQueued(port->stream);
@@ -435,9 +441,14 @@ Audio::Id Audio::AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, For
port.volume[i] = 32768;
}
// Pad speaker ports keep the main output too; it plays them whenever no DualSense does.
if (type != AUDIO_OUT_PORT_TYPE_VIBRATION) {
OpenSdlDevice(&port);
}
if (type == AUDIO_OUT_PORT_TYPE_VIBRATION || type == AUDIO_OUT_PORT_TYPE_PADSPK) {
port.haptics =
Controller::DualSenseHaptics::Open(freq, type == AUDIO_OUT_PORT_TYPE_PADSPK);
}
return Id::Create(id);
}
@@ -547,7 +558,23 @@ 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);
uint64_t controller_queued_us = 0;
if (port.haptics != nullptr) {
// Keep the stream alive against close and volume changes.
Common::LockGuard lock(m_mutex);
controller_queued_us = 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);
}
if (controller_queued_us == 0) {
// No DualSense took it (e.g. it was unplugged); a pad speaker port plays on the main
// output instead, and a vibration port has none.
QueueSdlAudio(&port, params[i].data, blocking);
} else if (blocking && port.type == AUDIO_OUT_PORT_TYPE_PADSPK &&
controller_queued_us > AUDIO_OUT_TARGET_LATENCY_US) {
// Vibration never paces output; the speaker is audible, so pace it like other ports.
Common::Thread::SleepMicro(controller_queued_us - AUDIO_OUT_TARGET_LATENCY_US);
}
}
for (uint32_t i = 0; i < num; i++) {
+3
View File
@@ -70,6 +70,9 @@ int KYTY_SYSV_ABI AudioOut2PortGetState(AudioOut2PortHandle port, AudioOut2PortS
int KYTY_SYSV_ABI AudioOut2GetSystemState(AudioOut2SystemState* state);
int KYTY_SYSV_ABI AudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* handle);
int KYTY_SYSV_ABI AudioOut2UserDestroy(AudioOut2UserHandle handle);
int KYTY_SYSV_ABI AudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle,
uint32_t* context_attributes,
uint32_t* port_attributes);
size_t KYTY_SYSV_ABI AudioOut2GetSpeakerArrayMemorySize(uint32_t num_speakers, uint8_t is_3d,
uint8_t is_ambisonics);
int KYTY_SYSV_ABI AudioOut2SpeakerArrayCreate(AudioOut2SpeakerArrayHandle* handle,
+74 -54
View File
@@ -1,6 +1,6 @@
#include "common/common.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "kernel/fileSystem.h"
#include "kernel/pthread.h"
#include "libs/audio.h"
@@ -9,12 +9,14 @@
#include <algorithm>
#include <atomic>
#include <cctype>
#include <cerrno>
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <deque>
#include <limits>
#include <magic_enum.hpp>
#include <memory>
#include <mutex>
@@ -577,30 +579,40 @@ public:
explicit FileStreamer(AvPlayerFileReplacement f): file(f) {}
~FileStreamer() {
if (ctx != nullptr) {
av_freep(&ctx->buffer);
avio_context_free(&ctx);
}
if (opened && file.close != nullptr) {
if (opened) {
file.close(file.object_pointer);
}
}
bool Init(const std::string& path) {
if (file.open == nullptr || file.close == nullptr || file.read_offset == nullptr ||
file.size == nullptr) {
if (file.open != nullptr) {
if (file.close == nullptr || file.read_offset == nullptr || file.size == nullptr ||
file.open(file.object_pointer, path.c_str()) < 0) {
return false;
}
opened = true;
size = file.size(file.object_pointer);
} else {
if (!local_file.Open(LibKernel::FileSystem::GetRealFilename(path),
Common::File::Mode::Read)) {
return false;
}
size = local_file.Size();
}
if (size == 0 || size > static_cast<uint64_t>(std::numeric_limits<int64_t>::max())) {
return false;
}
if (file.open(file.object_pointer, path.c_str()) < 0) {
return false;
}
opened = true;
size = file.size(file.object_pointer);
if (size == 0) {
return false;
}
auto* buf = static_cast<uint8_t*>(av_malloc(4096));
constexpr int buffer_size = 64 * 1024;
auto* buf = static_cast<uint8_t*>(av_malloc(buffer_size));
if (buf == nullptr) {
return false;
}
ctx = avio_alloc_context(buf, 4096, 0, this, Read, nullptr, Seek);
ctx = avio_alloc_context(buf, buffer_size, 0, this, Read, nullptr, Seek);
if (ctx == nullptr) {
av_free(buf);
}
return ctx != nullptr;
}
AVIOContext* Context() const { return ctx; }
@@ -611,33 +623,50 @@ private:
if (s->pos >= s->size) {
return AVERROR_EOF;
}
len = static_cast<int>(std::min<uint64_t>(len, s->size - s->pos));
auto r =
s->file.read_offset(s->file.object_pointer, buf, s->pos, static_cast<uint32_t>(len));
if (r <= 0) {
return r == 0 ? AVERROR_EOF : r;
len = static_cast<int>(std::min<uint64_t>(len, s->size - s->pos));
int read = 0;
if (s->opened) {
read = s->file.read_offset(s->file.object_pointer, buf, s->pos,
static_cast<uint32_t>(len));
} else {
uint32_t bytes = 0;
s->local_file.Read(buf, static_cast<uint32_t>(len), &bytes);
read = static_cast<int>(bytes);
}
s->pos += static_cast<uint64_t>(r);
return r;
if (read <= 0 || read > len) {
return read < 0 ? read : AVERROR(EIO);
}
s->pos += static_cast<uint64_t>(read);
return read;
}
static int64_t Seek(void* opaque, int64_t off, int whence) {
static int64_t Seek(void* opaque, int64_t offset, int whence) {
auto* s = static_cast<FileStreamer*>(opaque);
if ((whence & AVSEEK_SIZE) != 0) {
return static_cast<int64_t>(s->size);
}
int64_t p = whence == SEEK_SET
? off
: (whence == SEEK_CUR
? static_cast<int64_t>(s->pos) + off
: (whence == SEEK_END ? static_cast<int64_t>(s->size) + off : -1));
if (p < 0) {
return -1;
whence &= ~AVSEEK_FORCE;
uint64_t base = 0;
switch (whence) {
case SEEK_SET: break;
case SEEK_CUR: base = s->pos; break;
case SEEK_END: base = s->size; break;
default: return AVERROR(EINVAL);
}
p = std::min<int64_t>(p, static_cast<int64_t>(s->size));
s->pos = static_cast<uint64_t>(p);
return p;
const auto distance = offset < 0 ? uint64_t {0} - static_cast<uint64_t>(offset)
: static_cast<uint64_t>(offset);
if ((offset < 0 && distance > base) ||
(offset >= 0 && distance > std::numeric_limits<int64_t>::max() - base)) {
return AVERROR(EINVAL);
}
const auto position = offset < 0 ? base - distance : base + distance;
if (!s->opened && !s->local_file.Seek(position)) {
return AVERROR(EIO);
}
s->pos = position;
return static_cast<int64_t>(position);
}
AvPlayerFileReplacement file;
Common::File local_file;
bool opened = false;
uint64_t pos = 0;
uint64_t size = 0;
@@ -675,25 +704,17 @@ public:
return static_cast<Source*>(opaque)->interrupt_io.load() ? 1 : 0;
};
raw->interrupt_callback.opaque = this;
if (file.open != nullptr) {
streamer = std::make_unique<FileStreamer>(file);
if (!streamer->Init(path)) {
avformat_free_context(raw);
return AVPLAYER_ERROR_OPERATION_FAILED;
}
raw->pb = streamer->Context();
if (auto rc = avformat_open_input(&raw, nullptr, nullptr, nullptr); rc < 0) {
LOGF("\t avformat_open_input callback failed: %s\n", fferr(rc).c_str());
return AVPLAYER_ERROR_OPERATION_FAILED;
}
} else {
auto real = LibKernel::FileSystem::GetRealFilename(std::string(path.c_str()));
auto real_str = Common::PathToString(real);
if (auto rc = avformat_open_input(&raw, real_str.c_str(), nullptr, nullptr); rc < 0) {
LOGF("\t avformat_open_input failed: %s path=%s\n", fferr(rc).c_str(),
real_str.c_str());
return AVPLAYER_ERROR_OPERATION_FAILED;
}
streamer = std::make_unique<FileStreamer>(file);
if (!streamer->Init(path)) {
avformat_free_context(raw);
return AVPLAYER_ERROR_OPERATION_FAILED;
}
raw->pb = streamer->Context();
if (const auto rc = avformat_open_input(&raw, file.open == nullptr ? path.c_str() : nullptr,
nullptr, nullptr);
rc < 0) {
LOGF("\t avformat_open_input failed: %s path=%s\n", fferr(rc).c_str(), path.c_str());
return AVPLAYER_ERROR_OPERATION_FAILED;
}
fmt = raw;
if (auto rc = avformat_find_stream_info(fmt, nullptr); rc < 0) {
@@ -951,7 +972,7 @@ public:
return true;
}
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;
@@ -996,7 +1017,7 @@ 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);
start_time_ms = 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);
@@ -1395,7 +1416,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);
+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);
+29 -14
View File
@@ -6,6 +6,7 @@
#include "libs/errno.h"
#include "libs/libs.h"
#include <atomic>
#include <cstdio>
#include <cstring>
#include <mutex>
@@ -306,6 +307,7 @@ LIB_NAME("SaveDataDialog", "SaveDataDialog");
constexpr int SAVE_STATUS_NONE = 0;
constexpr int SAVE_STATUS_INITIALIZED = 1;
constexpr int SAVE_STATUS_RUNNING = 2;
constexpr int SAVE_STATUS_FINISHED = 3;
constexpr int SAVE_RESULT_OK = 0;
constexpr int SAVE_BUTTON_ID_OK = 1;
@@ -351,18 +353,20 @@ struct SaveDataDialogResult {
uint8_t reserved[32];
};
static int g_save_status = SAVE_STATUS_NONE;
static int g_save_mode = 0;
static void* g_save_user_data = nullptr;
static char g_save_dir_name[sizeof(SaveDataDirName::data)] {};
static std::atomic<int> g_save_status = SAVE_STATUS_NONE;
static bool g_save_running_polled = false;
static int g_save_mode = 0;
static void* g_save_user_data = nullptr;
static char g_save_dir_name[sizeof(SaveDataDirName::data)] {};
int KYTY_SYSV_ABI SaveDataDialogInitialize() {
PRINT_NAME();
g_save_status = SAVE_STATUS_INITIALIZED;
g_save_mode = 0;
g_save_user_data = nullptr;
g_save_dir_name[0] = '\0';
g_save_status = SAVE_STATUS_INITIALIZED;
g_save_running_polled = false;
g_save_mode = 0;
g_save_user_data = nullptr;
g_save_dir_name[0] = '\0';
return OK;
}
@@ -376,6 +380,14 @@ int KYTY_SYSV_ABI SaveDataDialogGetStatus() {
int KYTY_SYSV_ABI SaveDataDialogUpdateStatus() {
PRINT_NAME();
// Some games require a RUNNING update before FINISHED.
if (g_save_status == SAVE_STATUS_RUNNING) {
if (g_save_running_polled) {
g_save_status = SAVE_STATUS_FINISHED;
}
g_save_running_polled = true;
}
return g_save_status;
}
@@ -431,7 +443,8 @@ int KYTY_SYSV_ABI SaveDataDialogOpen(const void* param) {
}
}
g_save_status = SAVE_STATUS_FINISHED;
g_save_status = SAVE_STATUS_RUNNING;
g_save_running_polled = false;
return OK;
}
@@ -441,7 +454,8 @@ int KYTY_SYSV_ABI SaveDataDialogClose(const void* close_param) {
LOGF("\t close_param = 0x%016" PRIx64 "\n", reinterpret_cast<uint64_t>(close_param));
g_save_status = SAVE_STATUS_FINISHED;
g_save_status = SAVE_STATUS_FINISHED;
g_save_running_polled = false;
return OK;
}
@@ -455,10 +469,11 @@ int KYTY_SYSV_ABI SaveDataDialogIsReadyToDisplay() {
int KYTY_SYSV_ABI SaveDataDialogTerminate() {
PRINT_NAME();
g_save_status = SAVE_STATUS_NONE;
g_save_mode = 0;
g_save_user_data = nullptr;
g_save_dir_name[0] = '\0';
g_save_status = SAVE_STATUS_NONE;
g_save_running_polled = false;
g_save_mode = 0;
g_save_user_data = nullptr;
g_save_dir_name[0] = '\0';
return 0;
}
+323
View File
@@ -0,0 +1,323 @@
#include "libs/dualSenseHaptics.h"
#include "common/logging/log.h"
#include "common/threads.h"
#include <SDL3/SDL.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <map>
#include <vector>
namespace Libs::Controller::DualSenseHaptics {
struct Stream {
uint32_t freq = 0;
bool speaker = false;
int controller = -1;
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_haptics_streams = 0;
uint8_t g_large_motor = 0, g_small_motor = 0;
uint64_t g_rumble_until = 0, g_haptics_until = 0;
int g_speaker_controller = -1;
std::map<int, int> g_speaker_streams;
void ApplyRumble(bool force = false) { // 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;
if (force && !muted) {
// SDL caches motor strengths. Force a report after raw speaker routing changed the
// controller's motor mode without changing that cache.
(void)SDL_RumbleGamepad(pad, 0, 0, 0);
}
(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 = g_rumble_until = 0;
ApplyRumble();
g_controller = controller;
g_large_motor = g_small_motor = 0;
}
}
bool RouteSpeaker(int controller, bool speaker) { // Caller holds g_mutex.
// Route the right channel to the speaker and mute the headphones with the values Linux uses
// without headphones (output report 0x02). Plugged-in headphones are not checked.
std::array<uint8_t, 38> report {};
report[0] = 0x80; // Audio control valid
if (speaker) {
report[0] |= 0x20; // Speaker volume valid
report[1] = 0x80; // Audio control 2 valid
report[5] = 0x64; // Speaker volume
report[7] = 0x30; // Output path: right channel to the speaker, headphones muted
report[37] = 0x02; // Speaker preamp +6 dB
}
bool routed = false;
SDL_LockJoysticks();
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller)); pad != nullptr) {
routed = SDL_SendGamepadEffect(pad, report.data(), static_cast<int>(report.size()));
}
SDL_UnlockJoysticks();
if (routed && controller == g_controller) {
ApplyRumble(true);
}
return routed;
}
bool SelectSpeaker(int controller) { // Caller holds g_mutex; -1 restores headphone routing.
if (g_speaker_controller == controller) {
return true;
}
if (g_speaker_controller != -1) {
RouteSpeaker(g_speaker_controller, false);
}
g_speaker_controller = -1;
if (controller != -1 && !RouteSpeaker(controller, true)) {
return false;
}
g_speaker_controller = controller;
return true;
}
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 (!stream->speaker && --g_haptics_streams == 0 && g_haptics_until != 0) {
g_haptics_until = 0;
ApplyRumble();
}
// Route back to the headphones once no speaker port plays on the controller.
if (stream->speaker && --g_speaker_streams.at(stream->controller) == 0) {
g_speaker_streams.erase(stream->controller);
if (g_speaker_controller == stream->controller) {
SelectSpeaker(-1);
}
}
}
}
bool CanUseDevice(int controller) {
// SDL exposes no portable association between a gamepad and its USB audio endpoint.
// Require one wired DualSense; guessing with multiple pads could play on the wrong one.
SDL_LockJoysticks();
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(controller));
bool usable =
pad != nullptr && SDL_GetGamepadConnectionState(pad) == SDL_JOYSTICK_CONNECTION_WIRED;
int count = 0;
auto* pads = SDL_GetGamepads(&count);
int dualsense_count = 0;
bool found = false;
for (int i = 0; i < count; i++) {
if (SDL_GetGamepadTypeForID(pads[i]) == SDL_GAMEPAD_TYPE_PS5) {
dualsense_count++;
found |= pads[i] == static_cast<SDL_JoystickID>(controller);
}
}
SDL_free(pads);
SDL_UnlockJoysticks();
return usable && found && dualsense_count == 1;
}
SDL_AudioDeviceID FindDevice() {
int count = 0;
auto* devices = SDL_GetAudioPlaybackDevices(&count);
SDL_AudioDeviceID device = 0;
for (int i = 0; i < count; 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) {
if (device != 0) {
device = 0; // Multiple matching endpoints cannot be associated reliably either.
break;
}
device = devices[i];
}
}
SDL_free(devices);
return device;
}
void RefreshDevice(Stream* stream, int controller) {
if (stream->controller != controller) {
CloseDevice(stream);
stream->controller = controller;
stream->next_check = 0;
}
const auto now = SDL_GetTicks();
if (now < stream->next_check) {
return;
}
stream->next_check = now + 2000;
const auto device = FindDevice();
if (stream->device == device && stream->sdl != nullptr) {
return;
}
CloseDevice(stream);
stream->device = device;
if (device == 0) {
return;
}
const SDL_AudioSpec desired {SDL_AUDIO_F32, 4, static_cast<int>(stream->freq)};
auto* sdl = SDL_OpenAudioDeviceStream(device, &desired, UpdateRumble, nullptr);
SDL_AudioSpec actual {};
bool ready = sdl != nullptr &&
SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl), &actual, nullptr) &&
actual.channels == 4 && SDL_ResumeAudioStreamDevice(sdl);
if (ready) {
Common::LockGuard lock(g_mutex);
if (stream->speaker) {
ready = SelectSpeaker(controller);
if (ready) {
g_speaker_streams[controller]++;
}
} else {
g_haptics_streams++;
}
}
if (ready) {
stream->sdl = sdl;
LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device));
} else {
SDL_DestroyAudioStream(sdl);
LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError());
}
}
} // namespace
Stream* Open(uint32_t freq, bool speaker) {
if (freq == 0 || !SDL_InitSubSystem(SDL_INIT_AUDIO)) {
return nullptr;
}
return new Stream {.freq = freq, .speaker = speaker};
}
void Close(Stream* stream) {
if (stream != nullptr) {
CloseDevice(stream);
delete stream;
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
}
uint64_t 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 0;
}
if (!CanUseDevice(controller)) {
CloseDevice(stream);
stream->next_check = 0; // Look for the device as soon as a DualSense is active again.
return 0;
}
RefreshDevice(stream, controller);
if (stream->sdl == nullptr) {
return 0;
}
if (stream->speaker) {
bool routed;
{
Common::LockGuard lock(g_mutex);
routed = SelectSpeaker(controller);
}
if (!routed) {
CloseDevice(stream);
return 0;
}
}
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;
// The speaker plays the right front channel; the actuators play the back ones.
stream->buffer[static_cast<size_t>(frame) * 4 + ch + (stream->speaker ? 0 : 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);
const auto queued_us =
static_cast<uint64_t>(queued + bytes) * 1000000 / (stream->freq * FRAME_BYTES);
if (audible && !stream->speaker) {
Common::LockGuard lock(g_mutex);
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) !=
SDL_GAMEPAD_TYPE_PS5) {
return 0;
}
SelectController(controller);
const auto playback_now = SDL_GetTicks();
const bool starting = g_haptics_until <= playback_now;
g_haptics_until =
std::max(g_haptics_until, playback_now + std::max<uint64_t>(queued_us / 1000, 250));
if (starting) {
ApplyRumble();
}
}
if (!SDL_PutAudioStreamData(stream->sdl, stream->buffer.data(), bytes)) {
CloseDevice(stream);
return 0;
}
return queued_us;
}
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);
SelectController(-1);
SelectSpeaker(-1);
}
} // namespace Libs::Controller::DualSenseHaptics
+26
View File
@@ -0,0 +1,26 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_
#define EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_
#include <cstdint>
namespace Libs::Controller::DualSenseHaptics {
struct Stream;
// A speaker stream plays a pad speaker port and routes the speaker to it; the others play vibration
// ports on the actuators.
Stream* Open(uint32_t freq, bool speaker);
void Close(Stream* stream);
// Returns how long the queued audio still plays, in microseconds, or 0 when no DualSense took it
// (the active pad has no usable USB audio device or its speaker cannot be routed).
// Multiple DualSenses or matching audio endpoints are ambiguous and are not selected.
uint64_t 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);
// Also restores the headphone routing.
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);
}
}
+1
View File
@@ -158,6 +158,7 @@ LIB_DEFINE(InitAudio_1_AudioOut2) {
LIB_FUNC("bkBN+CMLwRc", AudioOut2::AudioOut2GetSystemState);
LIB_FUNC("xywYcRB7nbQ", AudioOut2::AudioOut2UserCreate);
LIB_FUNC("IaZXJ9M79uo", AudioOut2::AudioOut2UserDestroy);
LIB_FUNC("iE8trxPKnAg", AudioOut2::AudioOut2UserGetSupportedAttributes);
LIB_FUNC("G1YOKDJYX2Y", AudioOut2::AudioOut2GetSpeakerArrayMemorySize);
LIB_FUNC("+k91hoTuoA8", AudioOut2::AudioOut2SpeakerArrayCreate);
LIB_FUNC("erCWQR5eKiQ", AudioOut2::AudioOut2SpeakerArrayDestroy);
+43 -4
View File
@@ -167,7 +167,7 @@ struct AudioOut2MasteringStatesV2 {
static std::atomic_uint64_t g_audioout2_next_context {1};
static std::atomic_uint64_t g_audioout2_next_port {1};
static std::atomic_uint64_t g_audioout2_next_user {1};
static AudioOut2UserHandle g_audioout2_next_user = 1;
struct AudioOut2ContextState {
bool used = false;
@@ -182,6 +182,7 @@ struct AudioOut2PortStateEntry {
bool used = false;
AudioOut2PortHandle handle = 0;
AudioOut2ContextHandle context = 0;
AudioOut2UserHandle user = 0;
uint16_t port_type = 0;
uint32_t data_format = 0;
uint32_t sampling_freq = 48000;
@@ -210,6 +211,7 @@ static Common::Mutex g_audioout2_context_mutex;
static std::array<AudioOut2ContextState, 16> g_audioout2_contexts;
static Common::Mutex g_audioout2_port_mutex;
static std::array<AudioOut2PortStateEntry, 256> g_audioout2_ports;
static std::vector<AudioOut2UserHandle> g_audioout2_users;
static Common::Mutex g_audioout2_speaker_array_mutex;
static std::array<AudioOut2SpeakerArrayState, 32> g_audioout2_speaker_arrays;
static Common::Mutex g_audioout2_latency_mutex;
@@ -218,6 +220,7 @@ static std::array<AudioOut2LatencyState, 16> g_audioout2_latencies;
static constexpr int AUDIO_OUT2_ERROR_NOT_READY = -2144960504; /* 0x80268008 */
static constexpr int AUDIO_OUT2_ERROR_PORT_FULL = -2144960494; /* 0x80268012 */
static constexpr int AUDIO_OUT2_ERROR_INVALID_PARAM = -2144960511; /* 0x80268001 */
static constexpr int AUDIO_OUT2_ERROR_BUSY = -2144960505; /* 0x80268007 */
static constexpr int AUDIO_OUT2_ERROR_MASTERING_INVALID_API_PARAM = -2144959999; /* 0x80268201 */
static constexpr int AUDIO_OUT2_ERROR_MASTERING_INVALID_STATES_ID = -2144959996; /* 0x80268204 */
static constexpr uint32_t AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM = 0;
@@ -589,6 +592,12 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
const auto samples_num = context_state->num_grains == 0 ? 512u : context_state->num_grains;
g_audioout2_port_mutex.Lock();
if (std::find(g_audioout2_users.begin(), g_audioout2_users.end(), params->user_handle) ==
g_audioout2_users.end()) {
g_audioout2_port_mutex.Unlock();
g_audioout2_context_mutex.Unlock();
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
AudioOut2PortStateEntry* port_state = nullptr;
for (auto& candidate: g_audioout2_ports) {
if (!candidate.used) {
@@ -601,6 +610,7 @@ int KYTY_SYSV_ABI AudioOut2PortCreate(AudioOut2ContextHandle ctx, const AudioOut
port_state->used = true;
port_state->handle = next_port;
port_state->context = ctx;
port_state->user = params->user_handle;
port_state->port_type = params->port_type;
port_state->data_format = params->data_format;
port_state->sampling_freq = params->sampling_freq;
@@ -730,9 +740,12 @@ int KYTY_SYSV_ABI AudioOut2GetSystemState(AudioOut2SystemState* state) {
int KYTY_SYSV_ABI AudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* handle) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(handle == nullptr);
*handle = static_cast<AudioOut2UserHandle>(
g_audioout2_next_user.fetch_add(1, std::memory_order_relaxed));
if (handle == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_port_mutex);
*handle = g_audioout2_next_user++;
g_audioout2_users.push_back(*handle);
LOGF("\t user_id = %" PRIu32 ", handle = 0x%016" PRIx64 "\n", user_id,
static_cast<uint64_t>(*handle));
return OK;
@@ -741,6 +754,32 @@ int KYTY_SYSV_ABI AudioOut2UserCreate(uint32_t user_id, AudioOut2UserHandle* han
int KYTY_SYSV_ABI AudioOut2UserDestroy(AudioOut2UserHandle handle) {
PRINT_NAME();
LOGF("\t handle = 0x%016" PRIx64 "\n", static_cast<uint64_t>(handle));
Common::LockGuard lock(g_audioout2_port_mutex);
const auto it = std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle);
if (it == g_audioout2_users.end()) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
if (std::any_of(g_audioout2_ports.begin(), g_audioout2_ports.end(),
[handle](const auto& port) { return port.used && port.user == handle; })) {
return AUDIO_OUT2_ERROR_BUSY;
}
g_audioout2_users.erase(it);
return OK;
}
int KYTY_SYSV_ABI AudioOut2UserGetSupportedAttributes(AudioOut2UserHandle handle,
uint32_t* context_attributes,
uint32_t* port_attributes) {
if (context_attributes == nullptr || port_attributes == nullptr) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
Common::LockGuard lock(g_audioout2_port_mutex);
if (std::find(g_audioout2_users.begin(), g_audioout2_users.end(), handle) ==
g_audioout2_users.end()) {
return AUDIO_OUT2_ERROR_INVALID_PARAM;
}
*context_attributes = 0;
*port_attributes = 1u << AUDIO_OUT2_PORT_ATTRIBUTE_ID_PCM;
return OK;
}
+68 -2
View File
@@ -7,6 +7,16 @@
#include <array>
#include <cstring>
#if defined(_WIN32)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#endif
namespace Libs {
namespace LibCes {
@@ -213,6 +223,54 @@ static int KYTY_SYSV_ABI CesMbcsUcsContextInit(CesContext* context, const CesPro
return 0;
}
#if defined(_WIN32)
static size_t ConvertCp932ToUtf8(const char** inbuf, size_t* inbytesleft, char** outbuf,
size_t* outbytesleft) {
const auto* in = reinterpret_cast<const uint8_t*>(*inbuf);
while (*inbytesleft != 0) {
const uint8_t lead = in[0];
// Win32 accepts 0x80 as U+0080; the SJis1997 profile rejects this byte.
if (lead == 0x80) {
*inbuf = reinterpret_cast<const char*>(in);
return SDL_ICONV_EILSEQ;
}
const bool is_lead_byte = (lead >= 0x81 && lead <= 0x9f) || (lead >= 0xe0 && lead <= 0xfc);
const size_t char_len = is_lead_byte ? 2 : 1;
if (*inbytesleft < char_len) {
*inbuf = reinterpret_cast<const char*>(in);
return SDL_ICONV_EINVAL;
}
wchar_t wide[2];
const int wide_len =
MultiByteToWideChar(932, MB_ERR_INVALID_CHARS, reinterpret_cast<const char*>(in),
static_cast<int>(char_len), wide, 2);
if (wide_len <= 0) {
*inbuf = reinterpret_cast<const char*>(in);
return SDL_ICONV_EILSEQ;
}
char utf8[8];
const int utf8_len =
WideCharToMultiByte(CP_UTF8, 0, wide, wide_len, utf8, sizeof(utf8), nullptr, nullptr);
if (utf8_len <= 0 || static_cast<size_t>(utf8_len) > *outbytesleft) {
*inbuf = reinterpret_cast<const char*>(in);
return SDL_ICONV_E2BIG;
}
std::memcpy(*outbuf, utf8, static_cast<size_t>(utf8_len));
*outbuf += utf8_len;
*outbytesleft -= static_cast<size_t>(utf8_len);
in += char_len;
*inbytesleft -= char_len;
}
*inbuf = reinterpret_cast<const char*>(in);
return 0;
}
#endif
static int ConvertMbcsToUtf8(CesContext* context, const uint8_t* source, uint32_t source_max,
uint32_t* source_len, uint8_t* destination, uint32_t destination_max,
uint32_t* destination_len, bool measure) {
@@ -246,8 +304,10 @@ static int ConvertMbcsToUtf8(CesContext* context, const uint8_t* source, uint32_
++source_size;
}
const bool bounded_end = source_max != 0 && source_size == source_max;
const auto converter = SDL_iconv_open("UTF-8", context->profile->encoding);
#if !defined(_WIN32)
const auto converter = SDL_iconv_open("UTF-8", context->profile->encoding);
EXIT_IF(converter == reinterpret_cast<SDL_iconv_t>(-1));
#endif
const char* input = reinterpret_cast<const char*>(source);
size_t input_left = source_size;
@@ -258,7 +318,11 @@ static int ConvertMbcsToUtf8(CesContext* context, const uint8_t* source, uint32_
char* output = measure ? scratch.data() : reinterpret_cast<char*>(destination) + produced;
const size_t capacity = measure ? scratch.size() : destination_max - 1 - produced;
size_t output_left = capacity;
const auto status = SDL_iconv(converter, &input, &input_left, &output, &output_left);
#if defined(_WIN32)
const auto status = ConvertCp932ToUtf8(&input, &input_left, &output, &output_left);
#else
const auto status = SDL_iconv(converter, &input, &input_left, &output, &output_left);
#endif
produced += capacity - output_left;
if (status == SDL_ICONV_E2BIG) {
if (measure) {
@@ -277,7 +341,9 @@ static int ConvertMbcsToUtf8(CesContext* context, const uint8_t* source, uint32_
}
break;
} while (input_left != 0);
#if !defined(_WIN32)
SDL_iconv_close(converter);
#endif
if (!measure) {
destination[produced] = 0;
+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);
}
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff. Show more