Compare commits

...

28 Commits

Author SHA1 Message Date
nmzik 9bc9c84312 docs: refine weekly updates wording 2026-09-27 18:06:00 +04:00
nmzik d3eaafab61 docs: use Discord server invite link 2026-09-27 18:03:49 +04:00
nmzik e568555e8b docs: add Discord development channel 2026-09-27 18:02:06 +04:00
nmzik 9b599ca92c docs: link weekly updates from README 2026-09-27 17:44:08 +04:00
Elija 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 Lobo 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
nmzik 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 Hurley 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
Pcniado 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 Uenal a22a9d9348 audio: play DualSense haptics from vibration audio ports (#843)
* audio: play DualSense haptics from vibration audio ports

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

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

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

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

* Simplify DualSense audio haptics and restore idle rumble

---------

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

* AvPlayer: initialize padded NV12 planes with neutral black

---------

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

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

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

* emulator: validate numeric CLI arguments

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

* tests: add LeastRecentlyUsedCache regression tests

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

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

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

* Run LRU cache tests in CI

---------

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

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

* Snapshot size limit and open device check

* Fall back to PCM pointer on stale snapshot

* audio: own AudioOut2 PCM at attribute set time

---------

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

(cherry picked from commit 9e74df7dc0)
2026-09-26 01:58:55 +04:00
72 changed files with 3377 additions and 798 deletions
+13 -6
View File
@@ -112,12 +112,15 @@ jobs:
- name: Build
shell: cmd
run: |
cmake --build _Build/windows --target launcher audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests --parallel
cmake --build _Build/windows --target launcher ^
audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
resource_tracking_tests scalar_provenance_tests ^
resource_materialization_tests shader_vertex_metadata_tests --parallel
- name: Test
shell: cmd
run: |
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|ime_dialog|virtual_memory_allocation)$"
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$"
- name: Install
shell: cmd
@@ -198,14 +201,16 @@ jobs:
shell: bash
run: |
cmake --build _Build/macos \
--target launcher audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests \
--target launcher audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
resource_tracking_tests scalar_provenance_tests \
resource_materialization_tests shader_vertex_metadata_tests \
--parallel
- name: Test
shell: bash
run: |
ctest --test-dir _Build/macos --output-on-failure \
-R '^(audio_out2_port|ime_dialog|virtual_memory_allocation)$'
-R '^(audio_out2_port|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$'
- name: Install
shell: bash
@@ -379,14 +384,16 @@ jobs:
run: |
cmake --build _Build/linux \
--target launcher page_manager_tests memory_tracker_tests \
audio_out2_port_tests ime_dialog_tests virtual_memory_allocation_tests \
audio_out2_port_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
resource_tracking_tests scalar_provenance_tests \
resource_materialization_tests shader_vertex_metadata_tests \
--parallel
- name: Test
shell: bash
run: |
ctest --test-dir _Build/linux --output-on-failure \
-R '^(audio_out2_port|ime_dialog|page_manager|memory_tracker|virtual_memory_allocation)$'
-R '^(audio_out2_port|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|virtual_memory_allocation)$'
- name: Install
shell: bash
+22
View File
@@ -301,6 +301,7 @@ set(gpu_blit_generated_dir "${PROJECT_BINARY_DIR}/gpu_blit_shaders")
set(gpu_blit_shader_sources
"${gpu_tiler_shader_dir}/gpu_blit_fs_triangle.vert"
"${gpu_tiler_shader_dir}/gpu_blit_color_to_ms_depth.frag"
"${gpu_tiler_shader_dir}/gpu_video_out_overlay.frag"
)
foreach(shader_source IN LISTS gpu_blit_shader_sources)
get_filename_component(shader_name "${shader_source}" NAME_WE)
@@ -411,6 +412,11 @@ add_executable(bit_array_tests EXCLUDE_FROM_ALL
)
target_include_directories(bit_array_tests PRIVATE ${inc_headers})
add_executable(lru_cache_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/LruCacheTests.cpp"
)
target_include_directories(lru_cache_tests PRIVATE ${inc_headers})
add_executable(ime_dialog_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/ImeDialogTests.cpp"
"${KYTY_SOURCE_DIR}/libs/imeCommon.cpp"
@@ -472,12 +478,21 @@ 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_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"
@@ -555,6 +570,7 @@ if(BUILD_TESTING)
add_test(NAME memory_tracker COMMAND $<TARGET_FILE:memory_tracker_tests>)
add_test(NAME page_manager COMMAND $<TARGET_FILE:page_manager_tests>)
add_test(NAME bit_array COMMAND $<TARGET_FILE:bit_array_tests>)
add_test(NAME lru_cache COMMAND $<TARGET_FILE:lru_cache_tests>)
add_test(NAME shader_vertex_metadata COMMAND $<TARGET_FILE:shader_vertex_metadata_tests>)
add_test(NAME resource_materialization COMMAND $<TARGET_FILE:resource_materialization_tests>)
add_test(NAME resource_tracking COMMAND $<TARGET_FILE:resource_tracking_tests>)
@@ -563,7 +579,9 @@ 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 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)
@@ -619,6 +637,7 @@ if(BUILD_TESTING)
memory_tracker_tests
page_manager_tests
bit_array_tests
lru_cache_tests
shader_vertex_metadata_tests
resource_materialization_tests
resource_tracking_tests
@@ -629,6 +648,9 @@ if(BUILD_TESTING)
virtual_memory_allocation_tests
audio_out2_port_tests
ngs2_sampler_tests
pad_haptics_tests
save_data_memory_tests
http_uri_parse_tests
)
endif()
+4
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
+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);
+1
View File
@@ -29,6 +29,7 @@ struct GraphicContext {
bool memory_budget_ext_enabled = false;
bool compute_subgroup_size_control_enabled = false;
bool sample_rate_shading_enabled = false;
// bool fp64_denorm_preserve = false; // Temporarily disabled.
bool attachment_feedback_loop_enabled = false;
bool provoking_vertex_last_enabled = false;
bool supports_block_texel_view = false;
+3 -5
View File
@@ -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;
}
}
+44 -33
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) {
@@ -639,9 +642,12 @@ void TextureCache::CopyImage(ImageId destination_id, ImageId source_id) {
const bool source_depth = source.info.IsDepth();
const bool dest_depth = destination.info.IsDepth();
const bool direct_copy =
source.backing.format == destination.backing.format ||
(!source_depth && !dest_depth &&
vk::blockSize(source.backing.format) == vk::blockSize(destination.backing.format));
(source.backing.image_type == destination.backing.image_type ||
(source.backing.image_type != vk::ImageType::e1D &&
destination.backing.image_type != vk::ImageType::e1D)) &&
(source.backing.format == destination.backing.format ||
(!source_depth && !dest_depth &&
vk::blockSize(source.backing.format) == vk::blockSize(destination.backing.format)));
if (direct_copy) {
destination.CopyImage(source);
} else if (!CopyD16(destination, source)) {
@@ -675,7 +681,8 @@ void TextureCache::CopyImageMip(ImageId destination_id, ImageId source_id, uint3
ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingType binding,
ImageId cached_id) {
auto& cached = m_slot_images[cached_id];
if (!cached.info.IsDepth() && !requested.IsDepth()) {
if ((!cached.info.IsDepth() && !requested.IsDepth()) ||
cached.info.tile_mode != requested.tile_mode) {
return {};
}
const bool stencil_match = requested.HasStencil() == cached.info.HasStencil();
@@ -689,8 +696,8 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
requested.samples == 1 && requested.data == cached.info.data &&
requested.extent == cached.info.extent && requested.resources == cached.info.resources &&
requested.type == cached.info.type && requested.pitch == cached.info.pitch &&
requested.tile_mode == cached.info.tile_mode && !requested.HasStencil() &&
!cached.info.HasStencil() && !requested.HasMetadata() && !cached.info.HasMetadata();
!requested.HasStencil() && !cached.info.HasStencil() && !requested.HasMetadata() &&
!cached.info.HasMetadata();
// PPSA04264
const bool retain_cached_layout =
requested.samples == 1 && cached.info.samples == 1 && cached.backing.samples == 1 &&
@@ -701,7 +708,7 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
requested.resources.layers != 0 && cached.info.resources.layers != 0 &&
requested.resources.layers < cached.info.resources.layers &&
requested.type == cached.info.type && requested.pitch == cached.info.pitch &&
requested.tile_mode == cached.info.tile_mode && requested.mip_layout[0].offset == 0 &&
requested.mip_layout[0].offset == 0 &&
cached.info.mip_layout[0].offset == 0 &&
requested.mip_layout[0].size == requested.data.size &&
cached.info.mip_layout[0].size == cached.info.data.size &&
@@ -810,12 +817,15 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
return {merged_id};
}
// PPSA08394
// A view cannot change the native image type or grow its extent.
if (requested.data.size == cached.info.data.size &&
requested.resources == cached.info.resources && requested.type == cached.info.type &&
requested.extent.width > cached.info.extent.width &&
requested.extent.height >= cached.info.extent.height &&
requested.extent.depth >= cached.info.extent.depth &&
ImageViewOps::FormatsCompatible(cached.info.pixel_format, requested.pixel_format)) {
requested.resources == cached.info.resources &&
ImageViewOps::FormatsCompatible(cached.info.pixel_format, requested.pixel_format) &&
(requested.type != cached.info.type
? requested.extent == cached.info.extent
: requested.extent.width > cached.info.extent.width &&
requested.extent.height >= cached.info.extent.height &&
requested.extent.depth >= cached.info.extent.depth)) {
return {ExpandImage(requested, cached_id)};
}
// PS5 mip tails can expose more levels without increasing the guest allocation.
@@ -1118,9 +1128,10 @@ void TextureCache::InitializeImage(ImageId id) {
}
}
void TextureCache::MaterializeDccClear(ImageId id, const ImageDesc& desc,
void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
uint32_t metadata_base_layer) {
if (desc.info.metadata.kind != ImageMetadataKind::Dcc) {
if (desc.info.metadata.kind != ImageMetadataKind::Dcc &&
desc.info.metadata.kind != ImageMetadataKind::Cmask) {
return;
}
const auto range = desc.info.metadata.range;
@@ -1128,18 +1139,18 @@ void TextureCache::MaterializeDccClear(ImageId id, const ImageDesc& desc,
std::scoped_lock lock {m_lock};
auto& image = m_slot_images[id];
image.info.metadata = desc.info.metadata;
// A native DCC allocation must not retain a reused HTile/CMask/FMask interpretation.
// Native color metadata must not retain a reused HTile/CMask/FMask clear flag.
m_surface_metas.erase(range.address);
if (range.size == 0 || desc.info.resources.levels != 1 || image.info.resources.levels != 1) {
return;
}
}
const auto layers = desc.info.TransferLayers();
// These one-mip surfaces use complete 4 KiB DCC metadata blocks.
// These one-mip surfaces use complete 4 KiB color metadata blocks.
constexpr uint64_t MetadataBlockSize = 0x1000;
if (!range.Valid() || range.address % MetadataBlockSize != 0 || layers == 0 ||
range.size % layers != 0 || (range.size / layers) % MetadataBlockSize != 0) {
EXIT("TextureCache: DCC slices must contain aligned 4 KiB blocks\n");
EXIT("TextureCache: color metadata slices must contain aligned 4 KiB blocks\n");
}
const auto& view = desc.view_info;
const bool volume_texture = desc.info.IsVolume() && view.type == vk::ImageViewType::e3D;
@@ -1147,7 +1158,7 @@ void TextureCache::MaterializeDccClear(ImageId id, const ImageDesc& desc,
const auto image_first = volume_texture ? 0u : view.base_layer;
const auto count = volume_texture ? desc.info.extent.depth : view.layer_count;
if (first >= layers || count > layers - first) {
EXIT("TextureCache: DCC view exceeds its native metadata slices\n");
EXIT("TextureCache: color view exceeds its native metadata slices\n");
}
// Finish native metadata writes before reading backing bytes. This can submit the scheduler,
// so discovery runs before final draw uploads and never holds the texture lock across it.
@@ -1159,15 +1170,15 @@ void TextureCache::MaterializeDccClear(ImageId id, const ImageDesc& desc,
const auto address = range.address + slice_size * (first + slice);
uint8_t code = 0;
if (!LibKernel::Memory::TryReadBacking(address, &code, sizeof(code))) {
EXIT("TextureCache: failed to read DCC metadata backing\n");
EXIT("TextureCache: failed to read color metadata backing\n");
}
vk::ClearValue clear {};
if (!DecodeDccClear(desc, code, clear.color)) {
if (!DecodeColorClear(desc, code, clear.color)) {
continue;
}
std::vector<uint8_t> bytes(slice_size);
if (!LibKernel::Memory::TryReadBacking(address, bytes.data(), bytes.size())) {
EXIT("TextureCache: failed to read DCC metadata slice\n");
EXIT("TextureCache: failed to read color metadata slice\n");
}
if (!std::all_of(bytes.begin(), bytes.end(), [code](uint8_t byte) { return byte == code; })) {
continue;
@@ -1312,7 +1323,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
image.tick_accessed_last = m_scheduler.CurrentTick();
TouchImage(image);
}
MaterializeDccClear(result, desc, metadata_base_layer);
MaterializeColorClear(result, desc, metadata_base_layer);
if (desc.type == BindingType::VideoOut &&
desc.info.metadata.compression != VideoOutCompression::Uncompressed) {
std::scoped_lock lock {m_lock};
+1 -1
View File
@@ -139,7 +139,7 @@ private:
ImageId cached);
[[nodiscard]] ImageId ExpandImage(const ImageInfo& info, ImageId source);
void RefreshImage(ImageId id);
void MaterializeDccClear(ImageId id, const ImageDesc& desc,
void MaterializeColorClear(ImageId id, const ImageDesc& desc,
uint32_t metadata_base_layer);
void InitializeImage(ImageId id);
[[nodiscard]] TextureTransfer
@@ -17,6 +17,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <bit>
namespace Libs::Graphics {
@@ -138,8 +139,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
// Color-control state selects the color-buffer operation and logical blend operation.
// The normal copy operation is a regular color write, not an attachment clear.
// Nonlinear clear values are still stored as normalized components.
// Fast color clears are metadata driven and must be handled explicitly when
// that metadata path is implemented; render-pass load must preserve contents.
// Metadata clears are materialized during image discovery; render-pass loads preserve contents.
uint32_t width = 0;
uint32_t height = 0;
uint32_t pitch = 0;
@@ -160,9 +160,6 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
break;
default: EXIT("unknown tile mode: %u\n", static_cast<uint32_t>(rt.attrib3.tile_mode));
}
if (!tile && levels > 1) {
EXIT("linear mipmapped render targets are unsupported\n");
}
if (samples > 1 && (!tile || levels != 1)) {
EXIT("multisampled render targets require a single-mip tiled surface\n");
}
@@ -203,8 +200,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
" layer=%u/%u\n",
rt.attrib3.dimension, rt.attrib3.depth, view.base_layer, view.image_layers);
}
// PPSA28068: the linear EULA target and its sampled view must share the padded pitch.
if (!tile || volume || texture_tile) {
if (samples == 1) {
pitch = TileGetTexturePitch(transfer_format, width, rt.attrib3.tile_mode);
} else {
pitch = TileGetRenderTargetPitch(width, bytes_per_element, rt.attrib.num_fragments);
@@ -232,37 +228,26 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
}
size = volume_layout.block_slice_size;
backing_size = volume_layout.total_size;
} else if (tile) {
} else {
TileSizeAlign layout {};
bool valid_layout = false;
if (texture_tile) {
if (samples == 1) {
TileGetTextureSize(transfer_format, width, height, levels, rt.attrib3.tile_mode,
&layout, mip_sizes, mip_padded);
valid_layout = layout.size != 0 && layout.align == texture_tile_layout.block.block_size;
valid_layout = layout.size != 0 && layout.align != 0 &&
(rt.attrib3.tile_mode != Prospero::TileMode::kRenderTarget ||
levels <= std::bit_width(std::max(width, height)));
} else {
valid_layout =
levels == 1 ? TileGetRenderTargetSize(width, height, pitch, bytes_per_element,
layout, rt.attrib.num_fragments)
: TileGetRenderTargetMipLayout(width, height, pitch, bytes_per_element,
levels, layout, mip_sizes, mip_padded);
valid_layout = TileGetRenderTargetSize(width, height, pitch, bytes_per_element,
layout, rt.attrib.num_fragments);
mip_sizes[0] = {layout.size, 0, 0, 0, 0, 0};
mip_padded[0] = {pitch, height};
}
if (!valid_layout) {
EXIT("unsupported render-target layout: %ux%u pitch=%u bytes=%u levels=%u\n", width,
height, pitch, bytes_per_element, levels);
}
size = layout.size;
EXIT_IF(size > UINT32_MAX);
if (levels == 1) {
mip_sizes[0] = {static_cast<uint32_t>(size), 0, 0, 0, 0, 0};
mip_padded[0] = {pitch, height};
}
} else {
size = static_cast<uint64_t>(pitch) * height * bytes_per_element * samples;
if (size > UINT32_MAX) {
EXIT("linear render-target slice exceeds the supported layout size\n");
}
mip_sizes[0] = {static_cast<uint32_t>(size), 0, 0, 0, 0, 0};
mip_padded[0] = {pitch, height};
}
if (size == 0 || (!volume && size > UINT64_MAX / view.image_layers)) {
EXIT("render-target memory footprint is invalid\n");
@@ -304,15 +289,25 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
desc.info.samples = samples;
desc.info.tile_mode = rt.attrib3.tile_mode;
const bool has_dcc = rt.info.dcc_compression_enable && rt.dcc_addr.addr != 0;
if (has_dcc) {
const bool has_cmask = !rt.info.dcc_compression_enable && rt.info.cmask_fast_clear_enable &&
rt.cmask.addr != 0 && samples == 1 &&
!rt.info.fmask_compression_enable &&
!rt.attrib3.write_vrs_rate_hint_to_cmask;
if (has_dcc || has_cmask) {
TileSizeAlign metadata_size {};
(void)TileGetDccSize(width, height, volume ? depth : view.image_layers, bytes_per_element,
levels, rt.attrib3.tile_mode, metadata_size, rt.attrib.num_fragments);
desc.info.metadata.kind = ImageMetadataKind::Dcc;
desc.info.metadata.range = {rt.dcc_addr.addr, metadata_size.size};
desc.info.metadata.dcc_clear_word = rt.clear_word0.word0;
desc.info.metadata.dcc_clear_register_valid = true;
desc.info.metadata.dcc_alpha_msb = DccAlphaOnMsb(rt.info);
const auto layers = volume ? depth : view.image_layers;
if (has_dcc) {
(void)TileGetDccSize(width, height, layers, bytes_per_element, levels,
rt.attrib3.tile_mode, metadata_size, rt.attrib.num_fragments);
desc.info.metadata.dcc_alpha_msb = DccAlphaOnMsb(rt.info);
} else {
(void)TileGetCmaskSize(width, height, layers, levels, metadata_size);
}
// DCC owns the clear when both planes are enabled; single-sample CMASK stays expanded.
desc.info.metadata.kind = has_dcc ? ImageMetadataKind::Dcc : ImageMetadataKind::Cmask;
desc.info.metadata.range = {has_dcc ? rt.dcc_addr.addr : rt.cmask.addr, metadata_size.size};
desc.info.metadata.clear_word = rt.clear_word0.word0;
desc.info.metadata.clear_register_valid = true;
}
for (uint32_t level = 0; level < levels; level++) {
if (volume) {
-11
View File
@@ -219,17 +219,6 @@ static void RtCheck(const HW::RenderTarget& rt) {
}
}
if (rt.info.cmask_fast_clear_enable || rt.info.dcc_compression_enable) {
static bool logged = false;
if (!logged) {
LOGF("RenderTarget: temporary: ignoring PS5 color metadata fast_clear=%s dcc=%s "
"cmask=0x%016" PRIx64 " dcc_addr=0x%016" PRIx64 "\n",
rt.info.cmask_fast_clear_enable ? "true" : "false",
rt.info.dcc_compression_enable ? "true" : "false", rt.cmask.addr,
rt.dcc_addr.addr);
logged = true;
}
}
if (rt.info.blend_bypass) {
static bool logged = false;
if (!logged) {
@@ -627,12 +627,13 @@ void Validate(const ImageInfo& info) {
}
break;
case ImageMetadataKind::Dcc:
case ImageMetadataKind::Cmask:
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) ||
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 {
@@ -335,8 +335,10 @@ void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource,
const bool raw_sint_storage = format == Prospero::BufferFormat::k32SInt && uint_resource &&
resource.written && !resource.read && !resource.atomic;
const auto numeric_class = Prospero::SampledTextureNumericClass(format);
const bool raw_float_atomic = format == Prospero::BufferFormat::k32Float && uint_resource &&
resource.atomic;
const bool format_ok =
raw_sint_storage ||
raw_sint_storage || raw_float_atomic ||
(numeric_class != Prospero::TextureNumericClass::Unsupported &&
numeric_class != Prospero::TextureNumericClass::Sint &&
uint_resource == (numeric_class == Prospero::TextureNumericClass::Uint) &&
@@ -512,19 +514,50 @@ static ImageViewInfo TextureViewInfo(const ShaderRecompiler::IR::ImageResource&
return view;
}
static bool TextureViewPreservesMipLayout(const TileSurfaceDescription& description,
uint32_t view_levels) {
static bool ResolveTextureMipView(const TileSurfaceDescription& description, bool metadata,
uint32_t view_levels, uint32_t& levels, uint32_t& base_level) {
TileSurfaceLayout physical {};
TileSurfaceLayout view {};
auto view_description = description;
view_description.levels = view_levels;
return TileGetTiledTextureLayout(description, physical) &&
TileGetTiledTextureLayout(view_description, view) &&
physical.first_tail_level == view.first_tail_level &&
physical.block_slice_size == view.block_slice_size &&
physical.total_size == view.total_size &&
std::equal(std::begin(physical.mips), std::begin(physical.mips) + description.levels,
std::begin(view.mips));
view_description.levels = levels;
if (!TileGetTiledTextureLayout(description, physical) ||
!TileGetTiledTextureLayout(view_description, view)) {
return false;
}
if (physical.first_tail_level == view.first_tail_level &&
physical.block_slice_size == view.block_slice_size &&
physical.total_size == view.total_size &&
std::equal(std::begin(physical.mips), std::begin(physical.mips) + description.levels,
std::begin(view.mips))) {
return true;
}
if (metadata || ((description.layers > 1 || description.depth > 1) &&
physical.block_slice_size != view.block_slice_size)) {
return false;
}
// T# addresses the last mip. A view can select the same stored subresources
// with different mip indices; inaccessible mips need no host representation.
for (uint32_t base = 0; base + view_levels <= description.levels; ++base) {
bool matches = true;
for (uint32_t i = 0; i < view_levels; ++i) {
const auto source = base_level + i;
const auto target = base + i;
if (physical.mips[target] != view.mips[source] ||
(target >= physical.first_tail_level) != (source >= view.first_tail_level) ||
std::max(description.width >> target, 1u) != std::max(description.width >> source, 1u) ||
std::max(description.height >> target, 1u) != std::max(description.height >> source, 1u) ||
std::max(description.depth >> target, 1u) != std::max(description.depth >> source, 1u)) {
matches = false;
break;
}
}
if (matches) {
levels = description.levels;
base_level = base;
return true;
}
}
return false;
}
TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageResource& resource,
@@ -558,7 +591,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
const auto view_levels = multisampled || single_storage_mip
? 1u
: static_cast<uint32_t>(last_level - base_level) + 1u;
const auto levels =
auto levels =
multisampled ? 1u : std::max(physical_levels, base_level + view_levels);
const auto tile = descriptor.TileMode();
const bool depth_tile = tile == Prospero::TileMode::kDepth;
@@ -600,12 +633,13 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
type == Prospero::ImageType::kColor2DArray ||
type == Prospero::ImageType::kColor2DMsaaArray;
const auto image_layers = layered ? depth : 1u;
auto view_base = static_cast<uint32_t>(base_level);
if (levels > physical_levels) {
const TileSurfaceDescription physical {
format, tile, volume ? TileSurfaceDimension::Dim3D : TileSurfaceDimension::Dim2D,
width, height, volume ? depth : 1u, physical_levels, image_layers};
// Texture mip views take precedence over the resource count, but must keep its storage layout.
if (!TextureViewPreservesMipLayout(physical, levels)) {
if (!ResolveTextureMipView(physical, !resource.r128 && descriptor.MetaCompress(),
view_levels, levels, view_base)) {
EXIT("unsupported texture mip view changes physical layout: base=%u last=%u max=%u "
"extent=%ux%ux%u tile=%u\n",
base_level, last_level, max_mip, width, height, depth,
@@ -640,7 +674,8 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
pixel_format = depth_format->depth_attachment_format;
}
}
const auto storage_view_format = storage && format == Prospero::BufferFormat::k32SInt
const auto storage_view_format = storage && (resource.atomic ||
format == Prospero::BufferFormat::k32SInt)
? vk::Format::eR32Uint
: SrgbStorageViewFormat(pixel_format);
const auto view_format = storage && storage_view_format != vk::Format::eUndefined
@@ -676,6 +711,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
}
desc.view_info = TextureViewInfo(resource, descriptor, view_format, surface_format, storage,
view_levels, desc.info.resources.layers);
desc.view_info.base_level = view_base;
desc.type = storage ? TextureCache::BindingType::Storage : TextureCache::BindingType::Texture;
auto id = texture_cache.FindImage(desc, shader_conversion);
@@ -795,9 +831,14 @@ void RenderExecutor::FindBuffers(PreparedBindings& prepared) {
auto& cache = m_context.GetBufferCache();
prepared.buffer_sources.clear();
prepared.buffer_sources.reserve(program.info.buffers.size());
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[i]);
const auto& layout = program.bindings;
if (layout.memory_offset_count == 0) {
return;
}
const auto& resources = layout.descriptors.front().resources;
prepared.buffer_sources.reserve(resources.size());
for (const auto resource: resources) {
auto descriptor = DecodeNativeDescriptor<ShaderBufferResource>(snapshot.buffers[resource]);
const auto address = descriptor.Base48();
const auto requested_size = descriptor.GetSize();
if (address == 0 || requested_size == 0) {
@@ -815,10 +856,10 @@ void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
const auto& program = *prepared.runtime->program;
const auto& snapshot = *prepared.runtime->resources;
const auto& layout = program.bindings;
EXIT_IF(prepared.buffer_sources.size() != program.info.buffers.size());
EXIT_IF(prepared.buffer_sources.size() != layout.memory_offset_count);
prepared.buffers.clear();
prepared.buffers.reserve(program.info.buffers.size());
prepared.buffers.reserve(layout.memory_offset_count);
EXIT_IF(prepared.shader_data.size() != layout.ShaderDataDwords());
std::fill(prepared.shader_data.begin() + layout.memory_offset_dword,
prepared.shader_data.end(), 0);
@@ -827,11 +868,12 @@ void RenderExecutor::RebindBuffers(PreparedBindings& prepared) {
const auto shift = (index % 4u) * 8u;
prepared.shader_data[dword] |= offset << shift;
};
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
for (uint32_t i = 0; i < layout.memory_offset_count; i++) {
const auto resource = layout.descriptors.front().resources[i];
uint32_t buffer_offset = 0;
prepared.buffers.push_back(NativeStorageBuffer(m_context, prepared.buffer_sources[i],
program.info.buffers[i], program.stage, i,
buffer_offset));
program.info.buffers[resource],
program.stage, resource, buffer_offset));
pack_memory_offset(i, buffer_offset);
}
if (ShaderRecompiler::IR::FindBinding(
@@ -1042,8 +1084,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);
}
+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) {
@@ -564,8 +564,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
vk::PhysicalDeviceVulkan11Properties properties11 {};
properties11.pNext = &subgroup_size_control;
vk::PhysicalDeviceFloatControlsProperties float_controls {};
float_controls.pNext = &properties11;
vk::PhysicalDeviceProperties2 properties2 {};
properties2.pNext = &properties11;
properties2.pNext = &float_controls;
if (graphics.mesh_shader_enabled) {
subgroup_size_control.pNext = &graphics.mesh_shader_properties;
@@ -628,6 +630,15 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
device_features.vertexPipelineStoresAndAtomics = VK_TRUE;
graphics.sample_rate_shading_enabled = true;
device_features.shaderInt64 = VK_TRUE;
device_features.shaderFloat64 =
supported_features2.features.shaderFloat64 &&
float_controls.shaderSignedZeroInfNanPreserveFloat64 &&
float_controls.shaderRoundingModeRTEFloat32;
// if (device_features.shaderFloat64 && !float_controls.shaderDenormPreserveFloat64) {
// Log::WriteToConsoleAndLog(
// "WARNING: Vulkan device does not guarantee FP64 denormal preservation; "
// "continuing with native FP64 arithmetic. Very small values may be flushed to zero.\n");
// }
vk::PhysicalDeviceRobustness2FeaturesEXT robustness2 {};
#if defined(__APPLE__)
@@ -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);
}
@@ -109,7 +93,7 @@ void ValidateNativeProgram(const IR::Program& program) {
if ((program.bindings.UsesPushData() &&
!IR::PushData::CanFit(program.bindings.push_data_start_dword, shader_data_dwords)) ||
program.bindings.memory_offset_dword != program.bindings.user_data_registers.size() ||
program.bindings.memory_offset_count != program.info.buffers.size() ||
program.bindings.memory_offset_count != buffers.size() ||
has_shader_data_storage != (shader_data_dwords != 0 && !program.bindings.UsesPushData()) ||
!std::is_sorted(program.bindings.user_data_registers.begin(),
program.bindings.user_data_registers.end()) ||
@@ -198,6 +182,7 @@ Emitter::SpirvRequirements Emitter::AnalyzeProgramRequirements(const IR::Program
SpirvRequirements requirements {};
for (const auto* block: program.blocks) {
for (const auto& inst: *block) {
requirements.float64 |= inst.GetType() == IR::Type::F64;
if (IR::BufferAccessOf(inst.GetOpcode()) == IR::BufferAccess::Atomic &&
inst.GetType() == IR::Type::U64) {
requirements.buffer_int64_atomics = true;
@@ -291,9 +291,20 @@ uint32_t EmitConvertU32F32(EmitterState& state, uint32_t arg0) {
return EmitF32ToU32(state, arg0, false);
}
uint32_t EmitConvertF32S32(EmitterState& state, uint32_t arg0) {
const auto signed_value = Unary(state, spv::OpBitcast, TypeI32(state), arg0);
return EmitNative<spv::OpConvertSToF, IR::Type::F32>(state, signed_value);
uint32_t EmitConvertF32F64(EmitterState& state, uint32_t arg0) {
const auto converted = Unary(state, spv::OpFConvert, TypeF32(state), arg0);
const auto source = EmitNative<spv::OpCompositeExtract, IR::Type::U32>(
state, Unary(state, spv::OpBitcast, TypeU64(state), arg0), 1u);
const auto exponent = EmitAndConstant(state, source, 0x7ff00000u);
const auto overflow =
Binary(state, spv::OpLogicalAnd, TypeBool(state),
EmitCompareU32Constant(state, spv::OpUGreaterThan, exponent, 0x47e00000u),
EmitCompareU32Constant(state, spv::OpULessThan, exponent, 0x7ff00000u));
const auto clamped = EmitOrU32(state, EmitAndConstant(state, source, 0x80000000u),
ConstantU32(state, 0x7f7fffffu));
return EmitFlushF32DenormToSignedZero(
state, Select(state, TypeF32(state), overflow,
Unary(state, spv::OpBitcast, TypeF32(state), clamped), converted));
}
uint32_t EmitCompositeExtractU64(EmitterState& state, uint32_t arg0, IR::Value arg1) {
@@ -453,6 +464,11 @@ uint32_t EmitFPRecip32(EmitterState& state, uint32_t arg0) {
return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), source);
}
uint32_t EmitFPRecip64(EmitterState& state, uint32_t arg0) {
const auto one = state.builder.Constant(spv::OpConstant, TypeF64(state), 0u, 0x3ff00000u);
return EmitNative<spv::OpFDiv, IR::Type::F64>(state, one, arg0);
}
uint32_t EmitFPRecipIFlag32(EmitterState& state, uint32_t arg0) {
// Integer-to-float inputs used by IFLAG cannot be denormal.
return Binary(state, spv::OpFDiv, TypeF32(state), ConstantF32(state, 0x3f800000u), arg0);
@@ -877,7 +877,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
return;
}
const auto atomic_opcode = ImageAtomicOpcode(op);
if (atomic_opcode != spv::OpNop) {
if (image_info.access == IR::ImageAccess::Atomic) {
const auto dimension = image.dimension;
ctx.Define(inst, EmitValueOrZeroIfCondition(state, ctx.Arg(inst, 3), [&]() {
const auto pointer = state.builder.AllocateId();
@@ -887,6 +887,15 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
StorageImageDescriptorPointer(state, mem.resource),
CoordU32(ctx, mem, *address, dimension),
ConstantU32(state, 0));
if (op == IR::ValueOpcode::ImageAtomicFMin32 ||
op == IR::ValueOpcode::ImageAtomicFMax32) {
return AtomicUpdate(state, pointer, IR::ResourceKind::Image,
[&](uint32_t old) {
return EmitFloatAtomicReplacement(
state, old, ctx.Arg(inst, 2),
op == IR::ValueOpcode::ImageAtomicFMax32);
});
}
const auto old = state.builder.AllocateId();
state.builder.AddFunction(atomic_opcode, TypeU32(state), old, pointer,
ConstantU32(state, spv::ScopeDevice),
@@ -13,13 +13,22 @@ inline constexpr auto EmitConvertU32U16 = EmitBitCastU16F16;
inline constexpr auto EmitConvertU32U8 = EmitBitCastU16F16;
inline constexpr auto EmitBitCastU32F32 = EmitBitCastU16F16;
EMIT_NATIVE(BitCastF32U32, OpBitcast, F32, uint32_t)
EMIT_NATIVE(BitCastU64F64, OpBitcast, U64, uint32_t)
EMIT_NATIVE(BitCastF64U64, OpBitcast, F64, uint32_t)
uint32_t EmitConvertU16U32(EmitterState& state, uint32_t arg0);
uint32_t EmitConvertU8U32(EmitterState& state, uint32_t arg0);
uint32_t EmitConvertF16F32(EmitterState& state, uint32_t arg0);
inline constexpr auto EmitConvertF32F16 = EmitF16BitsToF32;
uint32_t EmitConvertS32F32(EmitterState& state, uint32_t arg0);
uint32_t EmitConvertU32F32(EmitterState& state, uint32_t arg0);
uint32_t EmitConvertF32S32(EmitterState& state, uint32_t arg0);
template <IR::Type type>
uint32_t EmitConvertSigned32ToFloat(EmitterState& state, uint32_t arg0) {
const auto signed_value = Unary(state, spv::OpBitcast, TypeI32(state), arg0);
return EmitNative<spv::OpConvertSToF, type>(state, signed_value);
}
inline constexpr auto EmitConvertF32S32 = EmitConvertSigned32ToFloat<IR::Type::F32>;
inline constexpr auto EmitConvertF64S32 = EmitConvertSigned32ToFloat<IR::Type::F64>;
uint32_t EmitConvertF32F64(EmitterState& state, uint32_t arg0);
EMIT_NATIVE(ConvertF32U32, OpConvertUToF, F32, uint32_t)
EMIT_NATIVE(CompositeConstructU64, OpCompositeConstruct, U64, uint32_t, uint32_t)
EMIT_NATIVE(CompositeConstructU32x2, OpCompositeConstruct, U32x2, uint32_t, uint32_t)
@@ -120,6 +129,10 @@ inline constexpr auto EmitFPCmpClass32 = EmitClassMaskF32;
EMIT_NATIVE(FPAdd32, OpFAdd, F32, uint32_t, uint32_t)
EMIT_NATIVE(FPSub32, OpFSub, F32, uint32_t, uint32_t)
EMIT_NATIVE(FPMul32, OpFMul, F32, uint32_t, uint32_t)
EMIT_NATIVE(FPMul64, OpFMul, F64, uint32_t, uint32_t)
inline constexpr auto EmitFPFma64 =
EmitGlsl<GLSLstd450Fma, IR::Type::F64, uint32_t, uint32_t, uint32_t>;
uint32_t EmitFPRecip64(EmitterState& state, uint32_t arg0);
inline constexpr auto EmitFPFma32 =
EmitGlsl<GLSLstd450Fma, IR::Type::F32, uint32_t, uint32_t, uint32_t>;
uint32_t EmitFPMin32(EmitterState& state, uint32_t arg0, uint32_t arg1);
@@ -271,6 +284,8 @@ inline constexpr auto EmitImageAtomicUMax32 = EmitImage;
inline constexpr auto EmitImageAtomicAnd32 = EmitImage;
inline constexpr auto EmitImageAtomicOr32 = EmitImage;
inline constexpr auto EmitImageAtomicXor32 = EmitImage;
inline constexpr auto EmitImageAtomicFMin32 = EmitImage;
inline constexpr auto EmitImageAtomicFMax32 = EmitImage;
void EmitUnreachable(ValueEmitContext& ctx, const IR::Inst& inst);
inline constexpr auto EmitPhi = EmitUnreachable;
inline constexpr auto EmitTessellationBase = EmitUnreachable;
@@ -68,6 +68,7 @@ struct SpirvRequirements {
bool pixel_valid_mask = false;
bool buffer_int64_atomics = false;
bool coherent_buffers = false;
bool float64 = false;
};
SpirvRequirements AnalyzeProgramRequirements(const IR::Program& program);
@@ -139,6 +140,7 @@ uint32_t TypeU32Pair(EmitterState& state);
uint32_t TypeI32(EmitterState& state);
uint32_t TypeI32Pair(EmitterState& state);
uint32_t TypeF32(EmitterState& state);
uint32_t TypeF64(EmitterState& state);
uint32_t TypeU32Vector(EmitterState& state, uint32_t components);
uint32_t TypeU32Composite(EmitterState& state, uint32_t components);
@@ -167,6 +169,10 @@ template <spv::Op opcode, IR::Type type, typename... Args>
uint32_t EmitNative(EmitterState& state, Args... args) {
const auto result = state.builder.AllocateId();
state.builder.AddFunction(opcode, TypeId(state, type), result, args...);
if constexpr (type == IR::Type::F64 &&
(opcode == spv::OpFMul || opcode == spv::OpFDiv || opcode == spv::OpExtInst)) {
state.builder.AddAnnotation(spv::OpDecorate, result, spv::DecorationNoContraction);
}
return result;
}
@@ -706,26 +706,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 +777,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 +895,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++) {
@@ -47,6 +47,10 @@ uint32_t TypeF32(EmitterState& state) {
return state.builder.Type(spv::OpTypeFloat, 32);
}
uint32_t TypeF64(EmitterState& state) {
return state.builder.Type(spv::OpTypeFloat, 64);
}
uint32_t TypeU32Vector(EmitterState& state, uint32_t components) {
return state.builder.Type(spv::OpTypeVector, TypeU32(state), components);
}
@@ -679,6 +683,19 @@ void DefineModule(EmitterState& state) {
// contract prevents host compilers from treating synthesized IEEE values as finite.
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeSignedZeroInfNanPreserve,
32u);
if (state.requirements.float64) {
EXIT_NOT_IMPLEMENTED(state.program.stage != ShaderType::Compute);
EXIT_NOT_IMPLEMENTED(state.input_info.compute->float_mode != 0xc0);
// MODE=0xc0 uses round-to-nearest-even and preserves FP64 input/output denormals.
state.builder.RequireCapability(spv::CapabilityFloat64);
state.builder.RequireCapability(spv::CapabilityRoundingModeRTE);
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeSignedZeroInfNanPreserve,
64u);
// FP64 denormal preservation is temporarily disabled.
// state.builder.RequireCapability(spv::CapabilityDenormPreserve);
// state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeDenormPreserve, 64u);
state.builder.AddExecutionMode(state.main_func, spv::ExecutionModeRoundingModeRTE, 32u);
}
if (const auto* cs = ShaderWorkgroupInput(state.program.stage, state.input_info)) {
uint32_t local_x = state.requirements.compute_derivatives ? 2u : 1u;
uint32_t local_y = state.requirements.compute_derivatives ? 2u : 1u;
@@ -440,6 +440,7 @@ uint32_t TypeId(EmitterState& state, IR::Type type) {
case IR::Type::U64: return TypeU64(state);
case IR::Type::U32x2: return TypeU32Pair(state);
case IR::Type::F32: return TypeF32(state);
case IR::Type::F64: return TypeF64(state);
case IR::Type::U32x3: return TypeU32Vector(state, 3);
case IR::Type::U32x4: return TypeU32Vector(state, 4);
case IR::Type::F32x2: return TypeF32Vector(state, 2);
@@ -198,6 +198,8 @@ constexpr Detail::OpcodeMap MIMG_ATOMIC_OPCODE_LIST[] = {
{0x18u, Opcode::IMAGE_ATOMIC_AND},
{0x19u, Opcode::IMAGE_ATOMIC_OR},
{0x1au, Opcode::IMAGE_ATOMIC_XOR},
{0x1eu, Opcode::IMAGE_ATOMIC_FMIN},
{0x1fu, Opcode::IMAGE_ATOMIC_FMAX},
};
constexpr auto MIMG_SAMPLE_OPS = Detail::MakeOpcodeTable<0x100>(MIMG_SAMPLE_OPCODE_LIST);
@@ -579,6 +579,8 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::IMAGE_ATOMIC_AND:
case Opcode::IMAGE_ATOMIC_OR:
case Opcode::IMAGE_ATOMIC_XOR:
case Opcode::IMAGE_ATOMIC_FMIN:
case Opcode::IMAGE_ATOMIC_FMAX:
case Opcode::IMAGE_LOAD:
case Opcode::IMAGE_LOAD_MIP:
case Opcode::IMAGE_GET_RESINFO:
@@ -154,6 +154,8 @@ enum class Opcode {
V_CUBEMA_F32,
V_CNDMASK_B32,
V_DOT2C_F32_F16,
V_CVT_F64_I32,
V_CVT_F32_F64,
V_CVT_F32_I32,
V_CVT_F32_U32,
V_CVT_U32_F32,
@@ -173,6 +175,7 @@ enum class Opcode {
V_CVT_F32_UBYTE1,
V_CVT_F32_UBYTE2,
V_CVT_F32_UBYTE3,
V_RCP_F64,
V_RCP_F32,
V_RCP_IFLAG_F32,
V_FRACT_F32,
@@ -234,6 +237,8 @@ enum class Opcode {
V_MAD_I32_I24,
V_MAD_U32_U24,
V_MAD_U64_U32,
V_FMA_F64,
V_MUL_F64,
V_FMA_F32,
V_FMA_F16,
V_PACK_B32_F16,
@@ -571,6 +576,8 @@ enum class Opcode {
IMAGE_ATOMIC_AND,
IMAGE_ATOMIC_OR,
IMAGE_ATOMIC_XOR,
IMAGE_ATOMIC_FMIN,
IMAGE_ATOMIC_FMAX,
IMAGE_SAMPLE,
IMAGE_GATHER4_L,
IMAGE_GATHER4_LZ,
@@ -87,6 +87,9 @@ constexpr OpcodeMap VOP1_OPCODE_LIST[] = {
{0x00u, Opcode::V_NOP},
{0x01u, Opcode::V_MOV_B32},
{0x02u, Opcode::V_READFIRSTLANE_B32},
{0x04u, Opcode::V_CVT_F64_I32},
{0x0fu, Opcode::V_CVT_F32_F64},
{0x2fu, Opcode::V_RCP_F64},
{0x05u, Opcode::V_CVT_F32_I32},
{0x06u, Opcode::V_CVT_F32_U32},
{0x07u, Opcode::V_CVT_U32_F32},
@@ -146,6 +149,9 @@ constexpr OpcodeMap VOP3_ENCODED_VOP1_OPCODE_LIST[] = {
{0x00u, Opcode::V_NOP},
{0x01u, Opcode::V_MOV_B32},
{0x02u, Opcode::V_READFIRSTLANE_B32},
{0x04u, Opcode::V_CVT_F64_I32},
{0x0fu, Opcode::V_CVT_F32_F64},
{0x2fu, Opcode::V_RCP_F64},
{0x05u, Opcode::V_CVT_F32_I32},
{0x06u, Opcode::V_CVT_F32_U32},
{0x07u, Opcode::V_CVT_U32_F32},
@@ -267,6 +273,8 @@ constexpr OpcodeMap VOP3_OPCODE_LIST[] = {
{0x146u, Opcode::V_CUBETC_F32},
{0x147u, Opcode::V_CUBEMA_F32},
{0x14bu, Opcode::V_FMA_F32},
{0x14cu, Opcode::V_FMA_F64},
{0x165u, Opcode::V_MUL_F64},
{0x148u, Opcode::V_BFE_U32},
{0x149u, Opcode::V_BFE_I32},
{0x14au, Opcode::V_BFI_B32},
@@ -452,6 +460,8 @@ bool IsVopcCompareExec(Opcode opcode);
bool IsVop1FloatSourceOpcode(Opcode opcode) {
switch (opcode) {
case Opcode::V_CVT_F32_F64:
case Opcode::V_RCP_F64:
case Opcode::V_MOV_B32:
case Opcode::V_CVT_F32_F16:
case Opcode::V_CVT_U32_F32:
@@ -522,9 +532,12 @@ constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
{Opcode::V_CVT_F32_U32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_I32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_UBYTE0, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
// The source field extract and the destination insert are independent (Translate.cpp), so a
// partial destination may combine with any source selector.
{Opcode::V_NOT_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(),
SdwaSelFull(), false},
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_FFBL_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_FFBH_U32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_F16, SdwaSelWords() | SdwaSelFull(), 0, 0, true},
{Opcode::V_CVT_F16_F32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), true},
@@ -707,6 +720,11 @@ 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_RCP_F64) {
SetUnsupported(inst, Family::VOP1, opcode, "FP64 instructions do not support DPP");
return;
}
const auto modifier = code[word_index + 1u];
const auto src0 = modifier & 0xffu;
SetRawWords(inst, code, word_index, 2);
@@ -756,6 +774,7 @@ bool IsVop2FloatOpcode(Opcode opcode) {
bool IsVop1FloatResultOpcode(Opcode opcode) {
switch (opcode) {
case Opcode::V_CVT_F32_F64:
case Opcode::V_CVT_F32_I32:
case Opcode::V_CVT_F32_U32:
case Opcode::V_CVT_F32_F16:
@@ -1176,6 +1195,7 @@ void DecodeVopcDpp(uint32_t pc, std::span<const uint32_t> code, uint32_t word_in
uint32_t NativeVop3SourceCount(Opcode opcode) {
switch (opcode) {
case Opcode::V_MUL_F64:
case Opcode::V_MUL_LO_U32:
case Opcode::V_MUL_HI_U32:
case Opcode::V_MUL_LO_I32:
@@ -1326,6 +1346,8 @@ bool SupportsNativeVop3SourceModifiers(Opcode opcode) {
case Opcode::V_MAX_F32:
case Opcode::V_MAC_F32:
case Opcode::V_MAD_F32:
case Opcode::V_MUL_F64:
case Opcode::V_FMA_F64:
case Opcode::V_FMA_F32:
case Opcode::V_PACK_B32_F16:
case Opcode::V_CUBEID_F32:
@@ -1358,6 +1380,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:
@@ -223,7 +223,7 @@ Decoder::Operand MemorySourceAt(const Decoder::Instruction& decoded, uint32_t in
const bool store_or_atomic = decoded.opcode == Decoder::Opcode::IMAGE_STORE ||
decoded.opcode == Decoder::Opcode::IMAGE_STORE_MIP ||
(decoded.opcode >= Decoder::Opcode::IMAGE_ATOMIC_SWAP &&
decoded.opcode <= Decoder::Opcode::IMAGE_ATOMIC_XOR);
decoded.opcode <= Decoder::Opcode::IMAGE_ATOMIC_FMAX);
if (store_or_atomic) {
return index == 0u ? decoded.dst : decoded.src0;
}
@@ -1026,6 +1026,10 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicOr32);
case Decoder::Opcode::IMAGE_ATOMIC_XOR:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicXor32);
case Decoder::Opcode::IMAGE_ATOMIC_FMIN:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicFMin32);
case Decoder::Opcode::IMAGE_ATOMIC_FMAX:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicFMax32);
case Decoder::Opcode::FLAT_LOAD_UBYTE:
case Decoder::Opcode::FLAT_LOAD_SBYTE:
@@ -4,6 +4,7 @@
#include <algorithm>
#include <array>
#include <bit>
#include <unordered_map>
#include <utility>
@@ -225,9 +226,28 @@ IR::Value Translator::ReadOperand(const Decoder::Operand& operand, IR::Type type
}
return ir.INotEqual(ReadRawU32(operand), IR::U32(IR::Value(0u)));
}
if (type == IR::Type::U64) {
const auto pair = ReadU32Pair(operand);
return ir.ConstructU64(pair[0], pair[1]);
if (type == IR::Type::U64 || type == IR::Type::F64) {
auto pair = ReadU32Pair(operand);
if (type == IR::Type::F64) {
if (operand.kind == Decoder::OperandKind::LiteralConstant) {
pair = {IR::U32(IR::Value(0u)), IR::U32(IR::Value(operand.value))};
} else if (operand.kind == Decoder::OperandKind::FloatInlineConstant) {
const auto bits = operand.value == 0x3e22f983u
? 0x3fc45f306dc9c882ull
: std::bit_cast<uint64_t>(static_cast<double>(
std::bit_cast<float>(operand.value)));
pair = {IR::U32(IR::Value(static_cast<uint32_t>(bits))),
IR::U32(IR::Value(static_cast<uint32_t>(bits >> 32u)))};
}
if (operand.absolute) {
pair[1] = ir.BitwiseAnd(pair[1], IR::U32(IR::Value(0x7fffffffu)));
}
if (operand.negate) {
pair[1] = ir.BitwiseXor(pair[1], IR::U32(IR::Value(0x80000000u)));
}
}
const auto bits = ir.ConstructU64(pair[0], pair[1]);
return type == IR::Type::F64 ? ir.Emit(IR::ValueOpcode::BitCastF64U64, {bits}) : IR::Value(bits);
}
auto bits = ApplyBitSourceModifiers(operand, ReadRawU32(operand));
if (TypesOverlap(type, IR::Type::F32) && !TypesOverlap(type, IR::Type::U32)) {
@@ -403,6 +423,10 @@ void Translator::WriteOperand(const Decoder::Operand& operand, IR::Value value)
Write16Bits(operand, IR::U32(ir.Emit(IR::ValueOpcode::ConvertU32U16, {bits})));
return;
}
if (type == IR::Type::F64) {
value = ir.Emit(IR::ValueOpcode::BitCastU64F64, {value});
type = IR::Type::U64;
}
if (type == IR::Type::U64) {
WriteU32Pair(operand, {ir.CompositeExtract(value, 0), ir.CompositeExtract(value, 1)});
return;
@@ -842,6 +866,14 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
include_vector(inst.src1);
include_vector(inst.src2);
include_vector(inst.src3);
switch (inst.opcode) {
case Decoder::Opcode::V_CVT_F64_I32: include_vector(inst.dst, 2u); break;
case Decoder::Opcode::V_FMA_F64: include_vector(inst.src2, 2u); [[fallthrough]];
case Decoder::Opcode::V_MUL_F64: include_vector(inst.src1, 2u); [[fallthrough]];
case Decoder::Opcode::V_RCP_F64: include_vector(inst.dst, 2u); [[fallthrough]];
case Decoder::Opcode::V_CVT_F32_F64: include_vector(inst.src0, 2u); break;
default: break;
}
if (inst.family == Decoder::Family::DS) {
switch (inst.opcode) {
case Decoder::Opcode::DS_WRITE_B64:
@@ -297,6 +297,8 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CVT_F32_UBYTE3: V_CVT_F32_UBYTE(inst, 3); return;
case O::V_CVT_F32_U32: V_CVT_F32_U32(inst); return;
case O::V_CVT_F32_I32: V_CVT_F32_I32(inst); return;
case O::V_CVT_F64_I32: return FloatUnary(inst, IR::ValueOpcode::ConvertF64S32);
case O::V_CVT_F32_F64: return FloatUnary(inst, IR::ValueOpcode::ConvertF32F64);
case O::V_CVT_U32_F32: V_CVT_U32_F32(inst); return;
case O::V_CVT_I32_F32: V_CVT_I32_F32(inst); return;
case O::V_CVT_F16_F32: V_CVT_F16_F32(inst); return;
@@ -394,6 +396,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_FREXP_MANT_F32: return V_FREXP_MANT_F32(inst);
case O::V_RCP_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecip32);
case O::V_RCP_F64: return FloatUnary(inst, IR::ValueOpcode::FPRecip64);
case O::V_RCP_IFLAG_F32: return FloatUnary(inst, IR::ValueOpcode::FPRecipIFlag32);
case O::V_FRACT_F32: return FloatUnary(inst, IR::ValueOpcode::FPFract32);
case O::V_TRUNC_F32: return FloatUnary(inst, IR::ValueOpcode::FPTrunc32);
@@ -410,6 +413,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_SUB_F32: return FloatBinary(inst, IR::ValueOpcode::FPSub32, false);
case O::V_SUBREV_F32: return FloatBinary(inst, IR::ValueOpcode::FPSub32, true);
case O::V_MUL_F32: return FloatBinary(inst, IR::ValueOpcode::FPMul32, false);
case O::V_MUL_F64: return FloatBinary(inst, IR::ValueOpcode::FPMul64, false);
case O::V_MIN_F32: return FloatBinary(inst, IR::ValueOpcode::FPMin32, false);
case O::V_MAX_F32: return FloatBinary(inst, IR::ValueOpcode::FPMax32, false);
case O::V_LDEXP_F32: return FloatBinary(inst, IR::ValueOpcode::FPLdexp, false);
@@ -418,6 +422,7 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_MADAK_F32:
case O::V_MAD_F32:
case O::V_FMA_F32: return FloatTernary(inst, IR::ValueOpcode::FPFma32, false, true);
case O::V_FMA_F64: return FloatTernary(inst, IR::ValueOpcode::FPFma64, false, false);
case O::V_MIN3_F32: return FloatTernary(inst, IR::ValueOpcode::FPMinTri32, false, false);
case O::V_MAX3_F32: return FloatTernary(inst, IR::ValueOpcode::FPMaxTri32, false, false);
case O::V_MED3_F32: return FloatTernary(inst, IR::ValueOpcode::FPMedTri32, false, false);
+1 -1
View File
@@ -435,7 +435,7 @@ struct BindingLayout {
};
struct ShaderInfo {
static constexpr uint32_t MaxBuffers = 32;
static constexpr uint32_t MaxBuffers = 64;
static constexpr uint32_t MaxImages = 64;
static constexpr uint32_t MaxSamplers = 32;
static constexpr uint32_t MaxSampledPairs = 64;
@@ -30,6 +30,7 @@ std::string TypeName(Type type) {
"ImageResource",
"SamplerResource",
"ImageAddress",
"F64",
};
const auto bits = static_cast<uint32_t>(type);
if (bits == 0) {
+1
View File
@@ -27,6 +27,7 @@ enum class Type : uint32_t {
ImageResource = 1u << 17u,
SamplerResource = 1u << 18u,
ImageAddress = 1u << 19u,
F64 = 1u << 20u,
};
constexpr Type operator|(Type lhs, Type rhs) {
@@ -33,6 +33,7 @@ constexpr Type U32 = Type::U32;
constexpr Type U64 = Type::U64;
constexpr Type F16 = Type::F16;
constexpr Type F32 = Type::F32;
constexpr Type F64 = Type::F64;
constexpr Type U32x2 = Type::U32x2;
constexpr Type U32x3 = Type::U32x3;
constexpr Type U32x4 = Type::U32x4;
@@ -220,6 +221,8 @@ ImageOpcodeInfo ImageOpcodeInfoOf(ValueOpcode opcode) {
case ValueOpcode::ImageAtomicAnd32:
case ValueOpcode::ImageAtomicOr32:
case ValueOpcode::ImageAtomicXor32:
case ValueOpcode::ImageAtomicFMin32:
case ValueOpcode::ImageAtomicFMax32:
return {ImageAccess::Atomic, ImageResourceClass::Storage, false};
default: return {};
}
@@ -52,6 +52,8 @@ VALUE_OPCODE(BitCastU16F16, U16, F16)
VALUE_OPCODE(BitCastF16U16, F16, U16)
VALUE_OPCODE(BitCastU32F32, U32, F32)
VALUE_OPCODE(BitCastF32U32, F32, U32)
VALUE_OPCODE(BitCastU64F64, U64, F64)
VALUE_OPCODE(BitCastF64U64, F64, U64)
VALUE_OPCODE(ConvertU16U32, U16, U32)
VALUE_OPCODE(ConvertU32U16, U32, U16)
VALUE_OPCODE(ConvertU8U32, U8, U32)
@@ -61,6 +63,8 @@ VALUE_OPCODE(ConvertF16F32, F16, F32)
VALUE_OPCODE(ConvertS32F32, U32, F32)
VALUE_OPCODE(ConvertU32F32, U32, F32)
VALUE_OPCODE(ConvertF32S32, F32, U32)
VALUE_OPCODE(ConvertF64S32, F64, U32)
VALUE_OPCODE(ConvertF32F64, F32, F64)
VALUE_OPCODE(ConvertF32U32, F32, U32)
VALUE_OPCODE(CompositeConstructU64, U64, U32, U32)
VALUE_OPCODE(CompositeConstructU32x2, U32x2, U32, U32)
@@ -166,6 +170,9 @@ VALUE_OPCODE(FPAdd32, F32, F32, F32)
VALUE_OPCODE(FPSub32, F32, F32, F32)
VALUE_OPCODE(FPFma32, F32, F32, F32, F32)
VALUE_OPCODE(FPMul32, F32, F32, F32)
VALUE_OPCODE(FPMul64, F64, F64, F64)
VALUE_OPCODE(FPFma64, F64, F64, F64, F64)
VALUE_OPCODE(FPRecip64, F64, F64)
VALUE_OPCODE(FPMin32, F32, F32, F32)
VALUE_OPCODE(FPMax32, F32, F32, F32)
VALUE_OPCODE(FPMinTri32, F32, F32, F32, F32)
@@ -284,6 +291,8 @@ VALUE_OPCODE(ImageAtomicUMax32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicAnd32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicOr32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicXor32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicFMin32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicFMax32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(GetAttribute, U32, U32, U32)
VALUE_OPCODE(GetInterpolationParameter, U32, U32, U32, U32)
VALUE_OPCODE(SetAttribute, Void, U32x4, U1)
@@ -46,29 +46,45 @@ 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
void AllocateBindings(Program& program, uint32_t push_data_start_dword) {
if (!program.shader_info_complete || program.binding_layout_complete) {
EXIT("shader binding layout failed: %s", !program.shader_info_complete
@@ -76,18 +92,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,7 +138,7 @@ 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) {
@@ -7,6 +7,9 @@ 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);
const DescriptorBinding* FindBinding(const BindingLayout& layout, DescriptorBindingKind kind);
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -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"
@@ -400,6 +401,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 +450,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
: Prospero::BufferFormat::kInvalid,
.descriptor_swizzle =
program.info.buffers[i].formatted ? descriptor.DstSelXYZW() : DstSel(4, 5, 6, 7),
.zero_stride_oob = descriptor.OutOfBounds() == 0u && stride == 0u,
});
}
for (uint32_t i = 0; i < specialization.images.size(); i++) {
@@ -485,7 +490,8 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
image.cube = DescriptorIsCube(descriptor);
const auto format =
static_cast<Prospero::BufferFormat>((descriptor.dwords[1] >> 20u) & 0x1ffu);
if (base.atomic && format != Prospero::BufferFormat::k32UInt) {
if (base.atomic && format != Prospero::BufferFormat::k32UInt &&
format != Prospero::BufferFormat::k32Float) {
return SpecializationFail(
fmt::format("atomic image descriptor {} uses unsupported format {}", i,
static_cast<uint32_t>(format)));
@@ -514,7 +520,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
fmt::format("storage image descriptor {} uses unsupported format {}", i,
static_cast<uint32_t>(format)));
}
if (raw_sint_storage) {
if (raw_sint_storage || base.atomic) {
image.numeric_class = Prospero::TextureNumericClass::Uint;
}
} else if (image.numeric_class == Prospero::TextureNumericClass::Unsupported ||
@@ -1126,6 +1132,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;
@@ -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;
};
+2 -1
View File
@@ -622,6 +622,7 @@ static void ShaderGetStaticInputInfoCS(const HW::ComputeShaderInfo& regs,
info.threads_num[2] = regs.cs_regs.num_thread_z;
info.lds_size_dwords = static_cast<uint32_t>(regs.cs_regs.lds_size) * 128u;
info.scratch_size_dwords = data.scratch_size_dwords;
info.float_mode = regs.cs_regs.float_mode;
info.group_id[0] = regs.cs_regs.tgid_x_en != 0;
info.group_id[1] = regs.cs_regs.tgid_y_en != 0;
info.group_id[2] = regs.cs_regs.tgid_z_en != 0;
@@ -727,7 +728,7 @@ void BuildStageStaticKey(const ShaderPixelInputInfo& info, std::vector<uint32_t>
void BuildStageStaticKey(const ShaderComputeInputInfo& info, std::vector<uint32_t>& key) {
key.clear();
key.push_back(info.workgroup_register);
key.push_back(info.wave_size);
key.push_back(info.wave_size | (static_cast<uint32_t>(info.float_mode) << 8u));
key.push_back(info.host_subgroup_size);
key.push_back(info.thread_ids_num);
key.push_back(info.lds_size_dwords);
+1
View File
@@ -145,6 +145,7 @@ struct ShaderVertexInputInfo {
};
struct ShaderComputeInputInfo: ShaderWorkgroupInputInfo {
uint8_t float_mode = 0xc0;
uint32_t dispatch_threads_num[3] = {0, 0, 0};
bool group_id[3] = {false, false, false};
bool dispatch_thread_dimensions = false;
+7
View File
@@ -3,6 +3,8 @@
#include <QNetworkAccessManager>
#include <QObject>
#include <QString>
#include <QUrl>
class QByteArray;
class QWidget;
@@ -29,6 +31,11 @@ private:
QWidget* m_parent = nullptr;
QNetworkAccessManager m_network;
bool m_checking_updates = false;
// Answer from the primary update feed, kept while the GitHub fallback feed is
// being consulted, so it can still be shown if the fallback request fails.
QString m_primary_tag;
QUrl m_primary_page_url;
};
#endif // UPDATE_CHECKER_H
@@ -440,6 +440,14 @@ void ConfigurationEditDialog::add_game_directory() {
start_dir = m_game_dirs_list->item(m_game_dirs_list->count() - 1)->text();
}
#if defined(__APPLE__)
// Use the native macOS picker to browse mounted volumes.
const auto dir = QFileDialog::getExistingDirectory(this, tr("Select game folder"), start_dir);
if (dir.isEmpty()) {
return;
}
AddGameDirectoryItem(dir);
#else
QFileDialog dialog(this, tr("Select game folders"), start_dir);
dialog.setFileMode(QFileDialog::Directory);
dialog.setOption(QFileDialog::ShowDirsOnly, true);
@@ -462,6 +470,7 @@ void ConfigurationEditDialog::add_game_directory() {
for (const auto& dir: dialog.selectedFiles()) {
AddGameDirectoryItem(dir);
}
#endif
update_game_directory_buttons();
}
+22 -4
View File
@@ -84,12 +84,30 @@ void UpdateChecker::FetchUpdateInfo(const char* url, bool fallback, bool manual)
}
reply->deleteLater();
if (!fallback &&
(!info.error.isEmpty() || info.tag != QString::fromLatin1(KYTY_RELEASE_TAG))) {
FetchUpdateInfo(FALLBACK_FEED_URL, true, manual);
return;
if (!fallback) {
const bool up_to_date =
info.error.isEmpty() && info.tag == QString::fromLatin1(KYTY_RELEASE_TAG);
if (!up_to_date) {
if (info.error.isEmpty()) {
// The primary feed is reachable but disagrees with the build tag
// (e.g. a stale mirror); remember its answer in case the fallback
// request also fails.
m_primary_tag = info.tag;
m_primary_page_url = info.page_url;
}
FetchUpdateInfo(FALLBACK_FEED_URL, true, manual);
return;
}
}
if (fallback && !info.error.isEmpty() && !m_primary_tag.isEmpty()) {
// The fallback feed failed, but the primary feed already returned a
// usable answer; prefer it over reporting a network error.
info = {m_primary_tag, m_primary_page_url, {}};
}
m_primary_tag.clear();
m_primary_page_url.clear();
m_checking_updates = false;
emit CheckingChanged(false);
ShowUpdateResult(info, manual);
+2 -141
View File
@@ -5,6 +5,7 @@
#include "libs/ajm/atrac9_decoder.h"
#include "libs/ajm/decoder.h"
#include "libs/ajm/mp3_decoder.h"
#include "libs/ajm/opus_decoder.h"
#include "libs/audio.h"
#include "libs/errno.h"
#include "libs/libs.h"
@@ -75,16 +76,6 @@ struct AjmGetStatisticsResult {
uint32_t memory[6];
};
struct AjmDecOpusInitializeParameters {
uint32_t channel_num;
uint32_t sample_rate;
uint32_t mapping_family;
};
struct AjmSidebandDecOpusCodecInfo {
uint32_t frames_per_packet;
};
enum class AjmCodec : uint32_t {
DecMp3 = 0,
DecAt9 = 1,
@@ -120,8 +111,6 @@ constexpr uint64_t AJM_FLAG_SIDEBAND_STREAM = 1ull << 47u;
constexpr uint64_t AJM_INSTANCE_FLAG_FORMAT_OFFSET = 7u;
constexpr uint64_t AJM_INSTANCE_FLAG_FORMAT_MASK = 0x7u;
constexpr uint64_t AJM_INSTANCE_FLAG_MAX_CHANNEL_MASK = 0x7fu;
constexpr uint32_t AJM_DEC_OPUS_MAX_CHANNELS_FOR_10CH = 10;
constexpr uint32_t AJM_DEC_OPUS_FRAME_SAMPLES = 960;
static std::atomic_uint32_t g_ajm_next_instance {1};
static std::atomic_uint32_t g_ajm_next_batch {1};
@@ -196,134 +185,6 @@ int KYTY_SYSV_ABI AjmDecAt9ParseConfigData(const void* config_data,
return OK;
}
class AjmOpusDecoder final: public AjmDecoder {
public:
AjmOpusDecoder(uint32_t channels, uint32_t sample_rate, AjmSampleEncoding encoding,
uint64_t flags)
: AjmDecoder(channels, sample_rate, encoding) {
(void)flags;
}
AjmDecodeResult Initialize(const void* codec_parameters,
size_t codec_parameters_size) override {
auto result = MakeResult();
if (codec_parameters == nullptr ||
codec_parameters_size < sizeof(AjmDecOpusInitializeParameters)) {
result.result = AJM_RESULT_INVALID_PARAMETER;
return result;
}
const auto* params = static_cast<const AjmDecOpusInitializeParameters*>(codec_parameters);
if (params->channel_num == 0 || params->channel_num > AJM_DEC_OPUS_MAX_CHANNELS_FOR_10CH ||
params->sample_rate == 0) {
result.result = AJM_RESULT_INVALID_PARAMETER;
return result;
}
SetFormat(params->channel_num, params->sample_rate, m_sample_encoding);
m_mapping_family = params->mapping_family;
m_total_decoded_samples = 0;
m_is_initialized = true;
m_frames_per_packet = 1;
LOGF("AJM Opus initialized: %" PRIu32 " Hz, %" PRIu32 " ch, mapping=%" PRIu32 "\n",
params->sample_rate, params->channel_num, params->mapping_family);
return MakeResult();
}
void Reset() override { m_total_decoded_samples = 0; }
AjmDecodeResult Decode(const void* input, size_t input_size, void* output, size_t output_size,
bool multiple_frames, AjmGaplessState* gapless) override {
(void)multiple_frames;
auto result = MakeResult();
if (input == nullptr || input_size == 0) {
result.result = AJM_RESULT_PARTIAL_INPUT;
return result;
}
if (!m_is_initialized) {
result.result = AJM_RESULT_NOT_INITIALIZED;
return result;
}
auto samples_to_write = AJM_DEC_OPUS_FRAME_SAMPLES;
auto skip_samples = 0u;
if (gapless != nullptr) {
skip_samples = std::min<uint32_t>(samples_to_write, gapless->current.skip_samples);
samples_to_write -= skip_samples;
if (gapless->HasSampleLimit()) {
samples_to_write =
std::min<uint32_t>(samples_to_write, gapless->current.total_samples);
}
}
const auto output_bytes = static_cast<size_t>(samples_to_write) *
static_cast<size_t>(m_channels) *
AjmBytesPerSample(m_sample_encoding);
if (output_bytes != 0 && output == nullptr) {
result.result = AJM_RESULT_NOT_ENOUGH_ROOM;
return result;
}
if (output_bytes > output_size) {
result.result = AJM_RESULT_NOT_ENOUGH_ROOM;
return result;
}
if (output_bytes != 0) {
std::memset(output, 0, output_bytes);
}
if (gapless != nullptr) {
gapless->current.skip_samples =
static_cast<uint16_t>(gapless->current.skip_samples - skip_samples);
if (gapless->HasSampleLimit()) {
gapless->current.total_samples =
(gapless->current.total_samples > samples_to_write
? gapless->current.total_samples - samples_to_write
: 0);
}
const auto skipped_samples = AJM_DEC_OPUS_FRAME_SAMPLES - samples_to_write;
const auto skipped_total = std::min<uint32_t>(
std::numeric_limits<uint16_t>::max(),
static_cast<uint32_t>(gapless->current.skipped_samples) + skipped_samples);
gapless->current.skipped_samples = static_cast<uint16_t>(skipped_total);
}
result.input_consumed = input_size;
result.output_written = output_bytes;
result.frames = 1;
result.frames_per_packet = m_frames_per_packet;
m_total_decoded_samples += samples_to_write;
result.total_decoded_samples = m_total_decoded_samples;
result.format = GetFormat();
return result;
}
void WriteCodecInfo(void* output, size_t output_size,
const AjmDecodeResult& result) const override {
(void)result;
if (output == nullptr || output_size < sizeof(AjmSidebandDecOpusCodecInfo)) {
return;
}
auto* info = static_cast<AjmSidebandDecOpusCodecInfo*>(output);
info->frames_per_packet = m_frames_per_packet;
}
[[nodiscard]] size_t CodecInfoSize() const override {
return sizeof(AjmSidebandDecOpusCodecInfo);
}
private:
bool m_is_initialized = false;
uint32_t m_mapping_family = 0;
uint32_t m_frames_per_packet = 1;
};
struct AjmInstanceState {
uint32_t context = 0;
uint32_t codec = 0;
@@ -376,7 +237,7 @@ static std::unique_ptr<AjmDecoder> AjmCreateDecoder(uint32_t codec, uint64_t fla
case static_cast<uint32_t>(AjmCodec::DecM4aac):
return std::make_unique<AjmAacDecoder>(channels, 48000, encoding, flags);
case static_cast<uint32_t>(AjmCodec::DecOpus):
return std::make_unique<AjmOpusDecoder>(channels, 48000, encoding, flags);
return std::make_unique<AjmOpusDecoder>(channels, encoding);
default: break;
}
+216
View File
@@ -0,0 +1,216 @@
#pragma once
#include "libs/ajm/decoder.h"
#include "libs/ajm/ffmpeg_decoder_common.h"
#include <algorithm>
#include <cstring>
namespace Libs::Audio::Ajm {
struct AjmDecOpusInitializeParameters {
uint32_t channel_num;
uint32_t sample_rate;
uint32_t mapping_family;
};
struct AjmSidebandDecOpusCodecInfo {
uint32_t frames_per_packet;
};
static_assert(sizeof(AjmDecOpusInitializeParameters) == 12);
static_assert(sizeof(AjmSidebandDecOpusCodecInfo) == 4);
class AjmOpusDecoder final: public AjmDecoder {
public:
AjmOpusDecoder(uint32_t max_channels, AjmSampleEncoding encoding)
: AjmDecoder(0, 48000, encoding), m_max_channels(max_channels) {}
~AjmOpusDecoder() override { avcodec_free_context(&m_context); }
AjmDecodeResult Initialize(const void* parameters, size_t parameters_size) override {
avcodec_free_context(&m_context);
Reset();
auto result = MakeResult();
if (parameters == nullptr || parameters_size < sizeof(AjmDecOpusInitializeParameters)) {
result.result = AJM_RESULT_INVALID_PARAMETER | AJM_RESULT_FATAL;
return result;
}
const auto& params = *static_cast<const AjmDecOpusInitializeParameters*>(parameters);
if (params.channel_num == 0 || params.channel_num > 2 || params.sample_rate != 48000 ||
params.mapping_family != 0) {
result.result = AJM_RESULT_INVALID_PARAMETER | AJM_RESULT_FATAL;
return result;
}
if (params.channel_num > m_max_channels) {
result.result = AJM_RESULT_TOO_MANY_CHANNELS | AJM_RESULT_FATAL;
return result;
}
const auto* codec = avcodec_find_decoder(AV_CODEC_ID_OPUS);
m_context = codec != nullptr ? avcodec_alloc_context3(codec) : nullptr;
if (m_context != nullptr) {
av_channel_layout_default(&m_context->ch_layout, static_cast<int>(params.channel_num));
m_context->sample_rate = static_cast<int>(params.sample_rate);
if (avcodec_open2(m_context, codec, nullptr) < 0) {
avcodec_free_context(&m_context);
}
}
if (m_context == nullptr) {
result.result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_FATAL;
return result;
}
SetFormat(params.channel_num, params.sample_rate, m_sample_encoding);
return MakeResult();
}
void Reset() override {
if (m_context != nullptr) {
avcodec_flush_buffers(m_context);
}
m_total_decoded_samples = 0;
m_frames_per_packet = 0;
}
AjmDecodeResult Decode(const void* input, size_t input_size, void* output, size_t output_size,
bool multiple_frames, AjmGaplessState* gapless) override {
auto result = MakeResult();
if (m_context == nullptr) {
result.result = AJM_RESULT_NOT_INITIALIZED;
return result;
}
if (input == nullptr || input_size == 0) {
result.result = AJM_RESULT_PARTIAL_INPUT;
return result;
}
while (gapless == nullptr || !gapless->IsEnd()) {
const auto* data = static_cast<const uint8_t*>(input) + result.input_consumed;
const auto remaining = input_size - result.input_consumed;
if (remaining < 2) {
result.result = AJM_RESULT_PARTIAL_INPUT;
break;
}
const auto packet_size = static_cast<uint32_t>(data[0]) | (uint32_t {data[1]} << 8u);
if (packet_size > remaining - 2) {
result.result = AJM_RESULT_PARTIAL_INPUT;
break;
}
data += 2;
if (packet_size == 0 || ((data[0] & 3u) == 3 && packet_size < 2)) {
result.result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA;
result.internal_result = 0x1043;
break;
}
const uint32_t config = data[0] >> 3u;
const uint32_t frames = (data[0] & 3u) == 0 ? 1
: (data[0] & 3u) == 3 ? data[1] & 0x3fu
: 2;
const uint32_t frame_samples = config >= 16 ? 120u << (config & 3u)
: config >= 12 ? 480u << (config & 1u)
: (config & 3u) == 3 ? 2880u
: 480u << (config & 3u);
const uint32_t samples = frame_samples * frames;
if (frames == 0 || frames > 6 || frame_samples > 960) {
result.result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA;
result.internal_result = static_cast<int32_t>(0xffff8800u);
break;
}
const auto skip = gapless != nullptr
? std::min(samples, uint32_t {gapless->current.skip_samples})
: 0;
const auto write = gapless != nullptr && gapless->HasSampleLimit()
? std::min(samples - skip, gapless->current.total_samples)
: samples - skip;
const auto sample_bytes = m_channels * AjmBytesPerSample(m_sample_encoding);
const auto output_bytes = write * sample_bytes;
if (output_bytes > output_size - result.output_written ||
(output_bytes != 0 && output == nullptr)) {
result.result = AJM_RESULT_NOT_ENOUGH_ROOM;
break;
}
AVFrame* frame = DecodePacket(data, packet_size, &result);
if (frame == nullptr) {
break;
}
if (frame->nb_samples != static_cast<int>(samples) ||
frame->ch_layout.nb_channels != static_cast<int>(m_channels)) {
av_frame_free(&frame);
result.result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA;
result.internal_result = 0x1043;
break;
}
if (output_bytes != 0) {
std::memcpy(static_cast<uint8_t*>(output) + result.output_written,
frame->data[0] + skip * sample_bytes, output_bytes);
}
av_frame_free(&frame);
if (gapless != nullptr) {
gapless->current.skip_samples -= static_cast<uint16_t>(skip);
gapless->current.skipped_samples = static_cast<uint16_t>(
std::min<uint32_t>(std::numeric_limits<uint16_t>::max(),
gapless->current.skipped_samples + samples - write));
if (gapless->HasSampleLimit()) {
gapless->current.total_samples -= write;
}
}
result.input_consumed += packet_size + 2;
result.output_written += output_bytes;
result.frames += frames;
m_frames_per_packet = frames;
m_total_decoded_samples += write;
if (!multiple_frames) {
break;
}
}
result.frames_per_packet = m_frames_per_packet;
result.total_decoded_samples = m_total_decoded_samples;
return result;
}
void WriteCodecInfo(void* output, size_t size, const AjmDecodeResult&) const override {
if (output != nullptr && size >= sizeof(AjmSidebandDecOpusCodecInfo)) {
static_cast<AjmSidebandDecOpusCodecInfo*>(output)->frames_per_packet =
m_frames_per_packet;
}
}
[[nodiscard]] size_t CodecInfoSize() const override {
return sizeof(AjmSidebandDecOpusCodecInfo);
}
private:
AVFrame* DecodePacket(const uint8_t* data, uint32_t size, AjmDecodeResult* result) {
AVPacket* packet = av_packet_alloc();
AVFrame* frame = av_frame_alloc();
if (packet == nullptr || frame == nullptr ||
av_new_packet(packet, static_cast<int>(size)) < 0) {
av_packet_free(&packet);
av_frame_free(&frame);
result->result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_FATAL;
return nullptr;
}
std::memcpy(packet->data, data, size);
int rc = avcodec_send_packet(m_context, packet);
av_packet_free(&packet);
if (rc >= 0) {
rc = avcodec_receive_frame(m_context, frame);
}
if (rc < 0) {
av_frame_free(&frame);
result->result = AJM_RESULT_CODEC_ERROR | AJM_RESULT_INVALID_DATA;
result->internal_result = 0x1043;
return nullptr;
}
AVFrame* converted = AjmConvertFfmpegFrame(frame, m_sample_encoding, result);
av_frame_free(&frame);
return converted;
}
AVCodecContext* m_context = nullptr;
const uint32_t m_max_channels;
uint32_t m_frames_per_packet = 0;
};
}
+18 -3
View File
@@ -7,6 +7,8 @@
#include "common/threads.h"
#include "kernel/pthread.h"
#include "libs/audio_internal.h"
#include "libs/controller.h"
#include "libs/dualSenseHaptics.h"
#include "libs/errno.h"
#include "libs/libs.h"
@@ -105,7 +107,8 @@ private:
int channels_num = 0;
int volume[12] = {};
SDL_AudioStream* stream = nullptr;
SDL_AudioStream* stream = nullptr;
Controller::DualSenseHaptics::Stream* haptics = nullptr;
};
struct PortIn {
@@ -268,6 +271,8 @@ bool Audio::OpenSdlDevice(PortOut* port) {
void Audio::CloseSdlDevice(PortOut* port) {
EXIT_IF(port == nullptr);
Controller::DualSenseHaptics::Close(port->haptics);
port->haptics = nullptr;
if (port->stream != nullptr) {
SDL_DestroyAudioStream(port->stream);
@@ -435,7 +440,9 @@ Audio::Id Audio::AudioOutOpen(int type, uint32_t samples_num, uint32_t freq, For
port.volume[i] = 32768;
}
if (type != AUDIO_OUT_PORT_TYPE_VIBRATION) {
if (type == AUDIO_OUT_PORT_TYPE_VIBRATION) {
port.haptics = Controller::DualSenseHaptics::Open(freq);
} else {
OpenSdlDevice(&port);
}
@@ -547,7 +554,15 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
for (uint32_t i = 0; i < num; i++) {
auto& port = m_out_ports[params[i].handle.GetId()];
QueueSdlAudio(&port, params[i].data, blocking);
if (port.type == AUDIO_OUT_PORT_TYPE_VIBRATION) {
// Haptics never pace output; keep the stream alive against close and volume changes.
Common::LockGuard lock(m_mutex);
Controller::DualSenseHaptics::Queue(
port.haptics, Controller::GetActiveControllerId(), params[i].data, port.samples_num,
static_cast<uint32_t>(port.channels_num), FormatIsFloat(port.format), port.volume);
} else {
QueueSdlAudio(&port, params[i].data, blocking);
}
}
for (uint32_t i = 0; i < num; i++) {
+9 -10
View File
@@ -950,11 +950,8 @@ public:
if (deliver_seek_frame || sync_mode != 0) {
return true;
}
if (audio_id) {
return candidate.info.time_stamp <= last_audio_ts;
}
auto now = CurrentTimeNoLock();
return now == 0 || candidate.info.time_stamp <= now;
return now == 0 || candidate.info.time_stamp + candidate.timestamp_offset <= now;
});
if (!frame) {
return false;
@@ -999,7 +996,9 @@ public:
out->details.audio.size = current_audio->info.details.audio.size;
std::memcpy(out->details.audio.language_code,
current_audio->info.details.audio.language_code, 4);
last_audio_ts = out->time_stamp;
start_time_ms = current_audio->info.time_stamp + current_audio->timestamp_offset;
clock_start = std::chrono::steady_clock::now();
paused_extra = {};
RecordLoopBoundary(*current_audio);
return true;
}
@@ -1064,7 +1063,6 @@ private:
video_done = true;
audio_done = true;
seek_video_frame_pending = false;
last_audio_ts = 0;
last_output_loop_offset = 0;
pending_loop_warnings = 0;
}
@@ -1397,7 +1395,6 @@ private:
av_frame_free(&frame);
return false;
}
ready.info.time_stamp += timestamp_offset;
ready.timestamp_offset = timestamp_offset;
frames.Push(std::move(ready));
av_frame_free(&frame);
@@ -1541,11 +1538,14 @@ private:
return false;
}
auto* dst = buffer->Get();
std::memset(dst, 0, static_cast<size_t>(size));
auto* c = dst + pitch * h;
// PPSA02433 samples the padded columns; zero chroma would turn them green.
std::memset(dst, s->codecpar->color_range == AVCOL_RANGE_JPEG ? 0 : 16,
static_cast<size_t>(pitch) * h);
std::memset(c, 128, static_cast<size_t>(pitch) * h / 2);
for (int y = 0; y < src->height; y++) {
std::memcpy(dst + y * pitch, nv12->data[0] + y * nv12->linesize[0], src->width);
}
auto* c = dst + pitch * h;
for (int y = 0; y < src->height / 2; y++) {
std::memcpy(c + y * pitch, nv12->data[1] + y * nv12->linesize[1], src->width);
}
@@ -1674,7 +1674,6 @@ private:
int32_t trick_speed = AVPLAYER_TRICK_SPEED_NORMAL;
uint32_t sync_mode = 0;
uint64_t start_time_ms = 0;
uint64_t last_audio_ts = 0;
uint64_t last_output_loop_offset = 0;
uint32_t pending_loop_warnings = 0;
std::chrono::steady_clock::time_point clock_start {};
+15
View File
@@ -8,6 +8,7 @@
#include "common/stringUtils.h"
#include "common/threads.h"
#include "kernel/pthread.h"
#include "libs/dualSenseHaptics.h"
#include "libs/errno.h"
#include "libs/libs.h"
#include "libs/padData.h"
@@ -113,6 +114,7 @@ public:
void ReleaseHostPads();
void GetConnectionInfo(bool* flag, int* count);
void SetVibration(uint8_t large_motor, uint8_t small_motor);
int GetActiveControllerId();
void SetLightBar(uint8_t r, uint8_t g, uint8_t b);
bool SetTriggerEffect(const PadTriggerEffectParam& param);
void ReadState(ControllerState* state, bool* flag, int* count);
@@ -518,6 +520,7 @@ void GameController::ResetInputState() {
void GameController::ReleaseHostPads() {
Common::LockGuard lock(m_mutex);
DualSenseHaptics::Shutdown();
std::vector<SDL_Gamepad*> pads;
for (const auto id: m_connected_ids) {
@@ -551,6 +554,9 @@ void GameController::ReleaseHostPads() {
void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
Common::LockGuard lock(m_mutex);
if (DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor)) {
return;
}
if (m_active_id == HOST_INPUT_CONTROLLER_ID) {
return;
@@ -568,6 +574,11 @@ void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
}
}
int GameController::GetActiveControllerId() {
Common::LockGuard lock(m_mutex);
return m_active_id;
}
void GameController::SetLightBar(uint8_t r, uint8_t g, uint8_t b) {
Common::LockGuard lock(m_mutex);
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(m_active_id));
@@ -692,6 +703,10 @@ void ResetInputState() {
g_controller->ResetInputState();
}
int GetActiveControllerId() {
return g_controller != nullptr ? g_controller->GetActiveControllerId() : -1;
}
int KYTY_SYSV_ABI PadInit() {
PRINT_NAME();
+1
View File
@@ -73,6 +73,7 @@ void SetRightStick(int id, int x, int y);
void SetTouchPad(int id, int finger, bool down, float x, float y);
void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us);
void ResetInputState();
int GetActiveControllerId();
int KYTY_SYSV_ABI PadInit();
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param);
+205
View File
@@ -0,0 +1,205 @@
#include "libs/dualSenseHaptics.h"
#include "common/logging/log.h"
#include "common/threads.h"
#include <SDL3/SDL.h>
#include <algorithm>
#include <cmath>
#include <vector>
namespace Libs::Controller::DualSenseHaptics {
struct Stream {
uint32_t freq = 0;
SDL_AudioStream* sdl = nullptr;
SDL_AudioDeviceID device = 0;
uint64_t next_check = 0;
std::vector<float> buffer;
};
namespace {
constexpr int FRAME_BYTES = 4 * sizeof(float);
Common::Mutex g_mutex;
int g_controller = -1;
int g_playback_streams = 0;
uint8_t g_large_motor = 0, g_small_motor = 0;
uint64_t g_rumble_until = 0, g_haptics_until = 0;
void ApplyRumble() { // Caller holds g_mutex; no calls into Audio or Controller.
SDL_LockJoysticks();
if (auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(g_controller));
pad != nullptr) {
const auto now = SDL_GetTicks();
const bool muted = g_haptics_until > now || g_rumble_until <= now;
(void)SDL_RumbleGamepad(
pad, muted ? 0 : g_large_motor * 0x101U, muted ? 0 : g_small_motor * 0x101U,
g_rumble_until > now ? static_cast<uint32_t>(g_rumble_until - now) : 0);
}
SDL_UnlockJoysticks();
}
void SelectController(int controller) {
if (g_controller != controller) {
g_haptics_until = 0;
ApplyRumble();
g_controller = controller;
g_large_motor = g_small_motor = 0;
g_rumble_until = 0;
}
}
void SDLCALL UpdateRumble(void*, SDL_AudioStream*, int, int) {
Common::LockGuard lock(g_mutex);
if (g_haptics_until != 0 && SDL_GetTicks() >= g_haptics_until) {
g_haptics_until = 0;
ApplyRumble();
}
}
void CloseDevice(Stream* stream) {
if (stream->sdl != nullptr) {
// Destroy outside g_mutex: SDL waits for any running get callback.
SDL_DestroyAudioStream(stream->sdl);
stream->sdl = nullptr;
Common::LockGuard lock(g_mutex);
if (--g_playback_streams == 0 && g_haptics_until != 0) {
g_haptics_until = 0;
ApplyRumble();
}
}
}
SDL_AudioDeviceID FindDevice() {
int count = 0;
auto* devices = SDL_GetAudioPlaybackDevices(&count);
SDL_AudioDeviceID device = 0;
for (int i = 0; i < count && device == 0; i++) {
const char* name = SDL_GetAudioDeviceName(devices[i]);
SDL_AudioSpec spec {};
if (name != nullptr && SDL_strcasestr(name, "DualSense") != nullptr &&
SDL_GetAudioDeviceFormat(devices[i], &spec, nullptr) && spec.channels == 4) {
device = devices[i];
}
}
SDL_free(devices);
return device;
}
} // namespace
Stream* Open(uint32_t freq) {
if (freq == 0 || !SDL_InitSubSystem(SDL_INIT_AUDIO)) {
return nullptr;
}
return new Stream {.freq = freq};
}
void Close(Stream* stream) {
if (stream != nullptr) {
CloseDevice(stream);
delete stream;
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
}
void Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume) {
if (stream == nullptr || data == nullptr || frames == 0 || channels == 0 || volume == nullptr) {
return;
}
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) != SDL_GAMEPAD_TYPE_PS5) {
CloseDevice(stream);
return;
}
const auto now = SDL_GetTicks();
if (now >= stream->next_check) {
stream->next_check = now + 2000;
const auto device = FindDevice();
if (stream->device != device || stream->sdl == nullptr) {
CloseDevice(stream);
stream->device = device;
if (device != 0) {
const SDL_AudioSpec desired {SDL_AUDIO_F32, 4, static_cast<int>(stream->freq)};
auto* sdl = SDL_OpenAudioDeviceStream(device, &desired, UpdateRumble, nullptr);
SDL_AudioSpec actual {};
if (sdl != nullptr &&
SDL_GetAudioDeviceFormat(SDL_GetAudioStreamDevice(sdl), &actual, nullptr) &&
actual.channels == 4 && SDL_ResumeAudioStreamDevice(sdl)) {
stream->sdl = sdl;
{
Common::LockGuard lock(g_mutex);
g_playback_streams++;
}
LOGF("DualSenseHaptics: playing on '%s'\n", SDL_GetAudioDeviceName(device));
} else {
SDL_DestroyAudioStream(sdl);
LOGF("DualSenseHaptics: cannot open quad output: %s\n", SDL_GetError());
}
}
}
}
if (stream->sdl == nullptr) {
return;
}
stream->buffer.assign(static_cast<size_t>(frames) * 4, 0.0f);
bool audible = false;
for (uint32_t frame = 0; frame < frames; frame++) {
for (uint32_t ch = 0; ch < 2; ch++) {
const auto src_ch = channels == 1 ? 0 : ch;
const auto index = static_cast<size_t>(frame) * channels + src_ch;
float value = is_float ? static_cast<const float*>(data)[index]
: static_cast<const int16_t*>(data)[index] / 32768.0f;
value *= volume[src_ch] / 32768.0f;
stream->buffer[static_cast<size_t>(frame) * 4 + ch + 2] = value;
audible |= std::fabs(value) > 1.0f / 1024;
}
}
auto queued = std::max(0, SDL_GetAudioStreamQueued(stream->sdl));
if (queued > static_cast<int>(stream->freq * FRAME_BYTES * 80 / 1000)) {
SDL_ClearAudioStream(stream->sdl);
queued = 0;
}
const int bytes = static_cast<int>(frames * FRAME_BYTES);
if (audible) {
Common::LockGuard lock(g_mutex);
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) !=
SDL_GAMEPAD_TYPE_PS5) {
return;
}
SelectController(controller);
const auto playback_now = SDL_GetTicks();
const bool starting = g_haptics_until <= playback_now;
const auto duration = (queued + bytes) * 1000ULL / (stream->freq * FRAME_BYTES);
g_haptics_until =
std::max(g_haptics_until, playback_now + std::max<uint64_t>(duration, 250));
if (starting) {
ApplyRumble();
}
}
if (!SDL_PutAudioStreamData(stream->sdl, stream->buffer.data(), bytes)) {
CloseDevice(stream);
}
}
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) {
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) != SDL_GAMEPAD_TYPE_PS5) {
return false;
}
Common::LockGuard lock(g_mutex);
SelectController(controller);
g_large_motor = large_motor;
g_small_motor = small_motor;
g_rumble_until = SDL_GetTicks() + 0xffff;
ApplyRumble();
return true;
}
void Shutdown() {
Common::LockGuard lock(g_mutex);
g_rumble_until = 0;
SelectController(-1);
}
} // namespace Libs::Controller::DualSenseHaptics
+20
View File
@@ -0,0 +1,20 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_
#define EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_
#include <cstdint>
namespace Libs::Controller::DualSenseHaptics {
struct Stream;
Stream* Open(uint32_t freq);
void Close(Stream* stream);
void Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume);
// Returns false for other gamepad types, which retain the normal rumble path.
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor);
void Shutdown();
} // namespace Libs::Controller::DualSenseHaptics
#endif // EMULATOR_INCLUDE_EMULATOR_DUALSENSE_HAPTICS_H_
+22 -12
View File
@@ -188,7 +188,7 @@ struct AudioOut2PortStateEntry {
uint32_t samples_num = 512;
AudioInternal::Format audio_format = AudioInternal::Format::Unknown;
int audio_handle = 0;
const void* pcm_data = nullptr;
std::vector<uint8_t> pcm_data;
};
struct AudioOut2SpeakerArrayState {
@@ -341,11 +341,17 @@ static AudioOut2PortStateEntry* audioout2_find_port_locked(AudioOut2PortHandle p
return nullptr;
}
static size_t audioout2_pcm_size(const AudioOut2PortStateEntry& state) {
const auto bytes_per_sample = (state.data_format & 0x7fu) == 1 ? sizeof(int16_t) : sizeof(float);
return static_cast<size_t>(state.samples_num) *
audioout2_data_format_channels(state.data_format) * bytes_per_sample;
}
static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) {
Common::LockGuard lock(g_audioout2_port_mutex);
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
state.pcm_data != nullptr && AudioInternal::AudioOutHasDevice(state.audio_handle)) {
!state.pcm_data.empty() && AudioInternal::AudioOutHasDevice(state.audio_handle)) {
return true;
}
}
@@ -353,24 +359,23 @@ static bool audioout2_context_has_queueable_device(AudioOut2ContextHandle ctx) {
}
static void audioout2_queue_context_audio(AudioOut2ContextHandle ctx, bool blocking) {
// The backend consumes the buffers synchronously, but can block while pacing SDL's queue.
// Keep both PCM storage and port handles alive until it returns.
std::vector<AudioInternal::OutputParam> params;
params.reserve(AudioInternal::OUT_PORTS_MAX);
g_audioout2_port_mutex.Lock();
Common::LockGuard lock(g_audioout2_port_mutex);
for (const auto& state: g_audioout2_ports) {
if (state.used && state.context == ctx && state.audio_handle > 0 &&
state.pcm_data != nullptr && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data});
!state.pcm_data.empty() && params.size() < AudioInternal::OUT_PORTS_MAX) {
params.push_back(AudioInternal::OutputParam {state.audio_handle, state.pcm_data.data()});
}
}
g_audioout2_port_mutex.Unlock();
if (params.empty()) {
return;
if (!params.empty()) {
(void)AudioInternal::AudioOutOutputs(params.data(), static_cast<uint32_t>(params.size()),
blocking);
}
(void)AudioInternal::AudioOutOutputs(params.data(), static_cast<uint32_t>(params.size()),
blocking);
}
static void audioout2_close_audio_handle(int audio_handle) {
@@ -679,7 +684,12 @@ int KYTY_SYSV_ABI AudioOut2PortSetAttributes(AudioOut2PortHandle port,
if (has_pcm) {
g_audioout2_port_mutex.Lock();
if (auto* state = audioout2_find_port_locked(port); state != nullptr) {
state->pcm_data = pcm_data;
if (pcm_data != nullptr && state->audio_format != AudioInternal::Format::Unknown) {
const auto* bytes = static_cast<const uint8_t*>(pcm_data);
state->pcm_data.assign(bytes, bytes + audioout2_pcm_size(*state));
} else {
state->pcm_data.clear();
}
}
g_audioout2_port_mutex.Unlock();
}
+27 -3
View File
@@ -7,9 +7,11 @@
#include "libs/network.h"
#include "loader/symbolDatabase.h"
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <cstring>
#include <limits>
#include <map>
#include <mutex>
#include <string>
@@ -163,6 +165,16 @@ int KYTY_SYSV_ABI NetSetsockopt(int s, int level, int optname, const void* optva
return FinishSocketCall(Net::Setsockopt(s, level, optname, optval, optlen));
}
int KYTY_SYSV_ABI NetSend(int s, const void* buf, size_t len, int flags) {
const auto size = std::min<size_t>(len, std::numeric_limits<int>::max());
return FinishSocketCall(static_cast<int>(Net::Send(s, buf, size, flags | 0x20000)));
}
int KYTY_SYSV_ABI NetRecv(int s, void* buf, size_t len, int flags) {
const auto size = std::min<size_t>(len, std::numeric_limits<int>::max());
return FinishSocketCall(static_cast<int>(Net::Recv(s, buf, size, flags)));
}
uint32_t KYTY_SYSV_ABI NetHtonl(uint32_t host32) {
return ((host32 & 0x000000ffu) << 24u) | ((host32 & 0x0000ff00u) << 8u) |
((host32 & 0x00ff0000u) >> 8u) | ((host32 & 0xff000000u) >> 24u);
@@ -207,6 +219,8 @@ LIB_DEFINE(InitNet_1_Net) {
LIB_FUNC("Q4qBuN-c0ZM", LibNet::NetSocket);
LIB_FUNC("45ggEzakPJQ", LibNet::NetSocketClose);
LIB_FUNC("2mKX2Spso7I", LibNet::NetSetsockopt);
LIB_FUNC("beRjXBn-z+o", LibNet::NetSend);
LIB_FUNC("9wO9XrMsNhc", LibNet::NetRecv);
LIB_FUNC("9T2pDF2Ryqg", LibNet::NetHtonl);
LIB_FUNC("iWQWrwiSt8A", LibNet::NetHtons);
LIB_FUNC("pQGpHYopAIY", LibNet::NetNtohl);
@@ -277,7 +291,7 @@ static char* CopyUriPart(char*& dst, const UriPart& part) {
}
static int ParseEmptyUri(SceHttpUriElement* out, void* pool, size_t* require, size_t prepare) {
constexpr size_t needed = 3;
constexpr size_t needed = 4;
if (require != nullptr) {
*require = needed;
@@ -296,10 +310,12 @@ static int ParseEmptyUri(SceHttpUriElement* out, void* pool, size_t* require, si
auto* dst = static_cast<char*>(pool);
out->scheme = dst++;
out->hostname = dst++;
out->path = dst;
out->path = dst++;
out->query = dst;
out->scheme[0] = '\0';
out->hostname[0] = '\0';
out->path[0] = '\0';
out->query[0] = '\0';
}
return 0;
@@ -460,6 +476,9 @@ static int KYTY_SYSV_ABI HttpUriParse(SceHttpUriElement* out, const char* src_ur
needed += part.len + 1;
}
}
if (query.begin == nullptr) {
needed += 1;
}
if (require != nullptr) {
*require = needed;
@@ -482,7 +501,12 @@ static int KYTY_SYSV_ABI HttpUriParse(SceHttpUriElement* out, const char* src_ur
out->password = CopyUriPart(dst, password);
out->hostname = CopyUriPart(dst, hostname);
out->path = CopyUriPart(dst, path);
out->query = CopyUriPart(dst, query);
if (query.begin == nullptr) {
out->query = dst++;
out->query[0] = '\0';
} else {
out->query = CopyUriPart(dst, query);
}
out->fragment = CopyUriPart(dst, fragment);
}
+2
View File
@@ -15,6 +15,8 @@ LIB_DEFINE(InitVideoOut_1) {
LIB_FUNC("Up36PTk687E", VideoOut::VideoOutOpen);
LIB_FUNC("uquVH4-Du78", VideoOut::VideoOutClose);
LIB_FUNC("WkYtyOg30do", VideoOut::VideoOutSetFlipMaster);
LIB_FUNC("MBlv8YWoyOk", VideoOut::VideoOutAddBufferAttributeOption);
LIB_FUNC("PjS5uASwcV8", VideoOut::VideoOutSetBufferAttribute2);
LIB_FUNC("rKBUtgRrtbk", VideoOut::VideoOutRegisterBuffers2);
LIB_FUNC("HuViW4HnrOw", VideoOut::VideoOutSubmitChangeBufferAttribute2);
+1
View File
@@ -1003,6 +1003,7 @@ static int ConvertHostSocketError(int error) {
case ENOBUFS: posix_error = Posix::POSIX_ENOBUFS; break;
case ENOMEM: posix_error = Posix::POSIX_ENOMEM; break;
case ENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break;
case EPIPE: posix_error = Posix::POSIX_EPIPE; break;
case EPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break;
default: break;
}
+38 -6
View File
@@ -133,6 +133,26 @@ static bool ParseConsoleLanguage(const std::string& value, uint32_t& out) {
return true;
}
static bool ParseUint32(const std::string& value, uint32_t& out) {
uint32_t number = 0;
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), number);
if (error != std::errc {} || end != value.data() + value.size()) {
return false;
}
out = number;
return true;
}
static bool ParseInt32(const std::string& value, int32_t& out) {
int32_t number = 0;
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), number);
if (error != std::errc {} || end != value.data() + value.size()) {
return false;
}
out = number;
return true;
}
static bool ParseUserId(const std::string& value, int32_t& out) {
int32_t user_id = 0;
auto [end, error] = std::from_chars(value.data(), value.data() + value.size(), user_id);
@@ -246,9 +266,17 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
}
options.game_patch = path;
} else if (arg == "--screen-width") {
options.config.screen_width = static_cast<uint32_t>(Common::ToInt32(value));
if (!ParseUint32(value, options.config.screen_width) ||
options.config.screen_width == 0) {
::printf("invalid screen width: %s\n", value.c_str());
return false;
}
} else if (arg == "--screen-height") {
options.config.screen_height = static_cast<uint32_t>(Common::ToInt32(value));
if (!ParseUint32(value, options.config.screen_height) ||
options.config.screen_height == 0) {
::printf("invalid screen height: %s\n", value.c_str());
return false;
}
} else if (arg == "--user-name") {
if (value.empty() || value.size() > Config::MAX_USER_NAME_LENGTH) {
::printf("invalid user name: must contain 1-%zu bytes\n",
@@ -269,11 +297,15 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
return false;
}
} else if (arg == "--gpu") {
options.config.gpu_index = Common::ToInt32(value);
if (!ParseInt32(value, options.config.gpu_index)) {
::printf("invalid gpu index: %s\n", value.c_str());
return false;
}
} else if (arg == "--vblank-frequency") {
const int32_t vblank_frequency = Common::ToInt32(value);
options.config.vblank_frequency =
static_cast<uint32_t>(vblank_frequency < 0 ? 0 : vblank_frequency);
if (!ParseUint32(value, options.config.vblank_frequency)) {
::printf("invalid vblank frequency: %s\n", value.c_str());
return false;
}
} else if (arg == "--console-language") {
if (!ParseConsoleLanguage(value, options.config.console_language)) {
::printf("invalid console language: %s\n", value.c_str());
+65 -9
View File
@@ -8,6 +8,7 @@
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <mutex>
#include <thread>
#include <vector>
@@ -16,14 +17,16 @@ namespace {
namespace AudioOut2 = Libs::Audio::AudioOut2;
std::mutex g_device_mutex;
std::condition_variable g_device_cv;
std::vector<int> g_live_devices;
std::vector<int> g_device_backed_handles;
std::vector<bool> g_output_blocking;
int g_next_device = 1;
int g_open_waiters = 0;
bool g_block_opens = false;
std::mutex g_device_mutex;
std::condition_variable g_device_cv;
std::vector<int> g_live_devices;
std::vector<int> g_device_backed_handles;
std::vector<bool> g_output_blocking;
std::vector<std::vector<uint8_t>> g_output_pcm;
size_t g_capture_bytes = 0;
int g_next_device = 1;
int g_open_waiters = 0;
bool g_block_opens = false;
void Check(bool value, const char* text) {
if (!value) {
@@ -146,6 +149,17 @@ std::vector<bool> OutputCalls() {
return g_output_blocking;
}
void CaptureOutputPcm(size_t bytes) {
std::lock_guard lock(g_device_mutex);
g_capture_bytes = bytes;
g_output_pcm.clear();
}
std::vector<std::vector<uint8_t>> OutputPcm() {
std::lock_guard lock(g_device_mutex);
return g_output_pcm;
}
void TestSlotReuse() {
const auto context = CreateContext();
const auto param = MakeParam();
@@ -327,6 +341,41 @@ void TestHandleWithoutPcmDoesNotBypassQueue() {
AudioOut2::AudioOut2ContextDestroy(context);
}
void TestPcmCopiedBeforeScratchBufferReuse() {
const auto context = CreateContext();
const auto param = MakeParam();
AudioOut2::AudioOut2PortHandle first = 0;
AudioOut2::AudioOut2PortHandle second = 0;
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &first) == OK,
"first port create failed");
Check(AudioOut2::AudioOut2PortCreate(context, AsParam(&param), &second) == OK,
"second port create failed");
std::vector<float> scratch(512 * 2);
std::vector<float> first_pcm(scratch.size(), 0.25f);
std::vector<float> second_pcm(scratch.size(), -0.5f);
std::copy(first_pcm.begin(), first_pcm.end(), scratch.begin());
SetPcm(first, scratch.data());
std::copy(second_pcm.begin(), second_pcm.end(), scratch.begin());
SetPcm(second, scratch.data());
std::fill(scratch.begin(), scratch.end(), 0.0f);
const auto pcm_bytes = scratch.size() * sizeof(float);
CaptureOutputPcm(pcm_bytes);
Check(AudioOut2::AudioOut2ContextPush(context, 1) == OK, "shared-buffer push failed");
const auto output = OutputPcm();
Check(output.size() == 2, "shared-buffer push did not output both ports");
Check(std::memcmp(output[0].data(), first_pcm.data(), pcm_bytes) == 0,
"first port lost PCM when scratch buffer was reused");
Check(std::memcmp(output[1].data(), second_pcm.data(), pcm_bytes) == 0,
"second port lost PCM when scratch buffer was reused");
CaptureOutputPcm(0);
AudioOut2::AudioOut2PortDestroy(first);
AudioOut2::AudioOut2PortDestroy(second);
AudioOut2::AudioOut2ContextDestroy(context);
}
} // namespace
namespace Libs::Audio::AudioInternal {
@@ -363,9 +412,15 @@ bool AudioOutHasDevice(int handle) {
g_device_backed_handles.end();
}
uint32_t AudioOutOutputs(const OutputParam* /*params*/, uint32_t /*num*/, bool blocking) {
uint32_t AudioOutOutputs(const OutputParam* params, uint32_t num, bool blocking) {
std::lock_guard lock(g_device_mutex);
g_output_blocking.push_back(blocking);
if (g_capture_bytes != 0) {
for (uint32_t i = 0; i < num; i++) {
const auto* bytes = static_cast<const uint8_t*>(params[i].data);
g_output_pcm.emplace_back(bytes, bytes + g_capture_bytes);
}
}
return 0;
}
@@ -389,6 +444,7 @@ int main() {
TestFloat12ChannelPortOutputsPcm();
TestAsynchronousDevicePushKeepsQueueBounded();
TestHandleWithoutPcmDoesNotBypassQueue();
TestPcmCopiedBeforeScratchBufferReuse();
std::printf("AudioOut2PortTests: all cases passed\n");
return 0;
}
+131
View File
@@ -0,0 +1,131 @@
#include "common/abi.h"
#include "loader/symbolDatabase.h"
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstring>
namespace Libs::LibHttp {
void InitNet_1_Http(Loader::SymbolDatabase *symbols);
}
namespace {
struct SceHttpUriElement {
int opaque = 0;
char *scheme = nullptr;
char *username = nullptr;
char *password = nullptr;
char *hostname = nullptr;
char *path = nullptr;
char *query = nullptr;
char *fragment = nullptr;
uint16_t port = 0;
uint8_t reserved[10]{};
};
int failures = 0;
#define CHECK(condition) \
do { \
if (!(condition)) { \
std::fprintf(stderr, "HttpUriParseTests:%d: %s\n", __LINE__, \
#condition); \
failures++; \
} \
} while (false)
using HttpUriParse = int(KYTY_SYSV_ABI *)(SceHttpUriElement *, const char *,
void *, size_t *, size_t);
HttpUriParse GetHttpUriParse() {
Loader::SymbolDatabase symbols;
Libs::LibHttp::InitNet_1_Http(&symbols);
const auto *record =
symbols.FindByNid("IWalAn-guFs", Loader::SymbolType::Func);
CHECK(record != nullptr);
return record != nullptr ? reinterpret_cast<HttpUriParse>(record->vaddr)
: nullptr;
}
template <size_t N>
bool PointsIntoPool(const char *ptr, const std::array<char, N> &pool,
size_t used) {
if (ptr == nullptr) {
return false;
}
const auto address = reinterpret_cast<uintptr_t>(ptr);
const auto begin = reinterpret_cast<uintptr_t>(pool.data());
return address >= begin && address < begin + used;
}
void TestAbsentQuery(HttpUriParse parse) {
constexpr char url[] = "http://example.com/path";
size_t required = 0;
CHECK(parse(nullptr, url, nullptr, &required, 0) == 0);
const size_t expected_required =
sizeof("http") + sizeof("example.com") + sizeof("/path") + 1;
CHECK(required == expected_required);
std::array<char, 64> pool{};
SceHttpUriElement out{};
size_t parsed_required = 0;
CHECK(parse(&out, url, pool.data(), &parsed_required, required) == 0);
CHECK(parsed_required == required);
CHECK(out.query != nullptr);
CHECK(PointsIntoPool(out.query, pool, required));
if (out.query != nullptr) {
CHECK(out.query[0] == '\0');
}
}
void TestEmptyUri(HttpUriParse parse) {
constexpr char url[] = "";
size_t required = 0;
CHECK(parse(nullptr, url, nullptr, &required, 0) == 0);
CHECK(required == 4);
std::array<char, 4> pool{};
SceHttpUriElement out{};
size_t parsed_required = 0;
CHECK(parse(&out, url, pool.data(), &parsed_required, required) == 0);
CHECK(parsed_required == required);
CHECK(out.query != nullptr);
CHECK(PointsIntoPool(out.query, pool, required));
if (out.query != nullptr) {
CHECK(out.query[0] == '\0');
}
}
void TestPresentQuery(HttpUriParse parse) {
constexpr char url[] = "http://example.com/path?foo=bar";
size_t required = 0;
CHECK(parse(nullptr, url, nullptr, &required, 0) == 0);
std::array<char, 64> pool{};
SceHttpUriElement out{};
size_t parsed_required = 0;
CHECK(parse(&out, url, pool.data(), &parsed_required, required) == 0);
CHECK(parsed_required == required);
CHECK(out.query != nullptr);
CHECK(PointsIntoPool(out.query, pool, required));
if (out.query != nullptr) {
CHECK(std::strcmp(out.query, "?foo=bar") == 0);
}
}
} // namespace
int main() {
const auto parse = GetHttpUriParse();
if (parse == nullptr) {
return 1;
}
TestAbsentQuery(parse);
TestEmptyUri(parse);
TestPresentQuery(parse);
return failures == 0 ? 0 : 1;
}
+64 -8
View File
@@ -17,6 +17,7 @@
#include <array>
#include <chrono>
#include <csignal>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -31,6 +32,10 @@ namespace Libs::LibKernelApr {
void InitLibKernel_1_Apr(Loader::SymbolDatabase *symbols);
}
namespace Libs::LibNet {
void InitNet_1_Net(Loader::SymbolDatabase *symbols);
}
namespace {
namespace FileSystem = Libs::LibKernel::FileSystem;
@@ -407,6 +412,22 @@ void CheckAprPaths(const std::filesystem::path &root) {
void CheckSocketWakeup() {
namespace Net = Libs::Network::Net;
Loader::SymbolDatabase symbols;
Libs::LibNet::InitNet_1_Net(&symbols);
const auto *send_symbol = symbols.Find(
{"beRjXBn-z+o", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
const auto *recv_symbol = symbols.Find(
{"9wO9XrMsNhc", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
const auto *errno_symbol = symbols.Find(
{"HQOwnfMGipQ", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func});
Check(send_symbol && recv_symbol && errno_symbol,
"Net send, receive and errno exports resolve with the guest ABI versions");
using Send = int (KYTY_SYSV_ABI *)(int, const void *, size_t, int);
using Recv = int (KYTY_SYSV_ABI *)(int, void *, size_t, int);
using Errno = int *(KYTY_SYSV_ABI *)();
const auto net_send = reinterpret_cast<Send>(send_symbol->vaddr);
const auto net_recv = reinterpret_cast<Recv>(recv_symbol->vaddr);
auto *net_errno = reinterpret_cast<Errno>(errno_symbol->vaddr)();
// Guest sockaddr_in: length, family, network-order port/address, padding.
std::array<uint8_t, 16> address {16, 2, 0, 0, 127, 0, 0, 1};
const int listener = Net::Socket(2, 1, 0);
@@ -441,26 +462,61 @@ void CheckSocketWakeup() {
immediate.data()) == 0 && readable[reader / 64] == 0,
"empty socket is not readable");
const char payload[] = "wake";
Check(Net::Send(writer, payload, sizeof(payload), 0x20000) == sizeof(payload),
"send wake bytes with guest MSG_NOSIGNAL");
*net_errno = Libs::Posix::POSIX_EINVAL;
Check(net_send(writer, payload, sizeof(payload), 0) == sizeof(payload) &&
*net_errno == Libs::Posix::POSIX_EINVAL,
"Net send forwards bytes and preserves errno on success");
readable[reader / 64] = bit;
const std::array<int64_t, 2> deadline {1, 0};
Check(Net::Select(reader + 1, readable.data(), nullptr, nullptr,
deadline.data()) == 1 && readable[reader / 64] == bit,
"select reports the guest descriptor after wake");
std::array<char, sizeof(payload)> received {};
Check(Net::Recv(reader, received.data(), received.size(), 0x42) == sizeof(payload) &&
Check(net_recv(reader, received.data(), received.size(), 0x42) == sizeof(payload) &&
std::memcmp(received.data(), payload, sizeof(payload)) == 0 &&
*net_errno == Libs::Posix::POSIX_EINVAL,
"Net receive forwards PEEK and WAITALL without consuming bytes");
received.fill(0);
Check(net_recv(reader, received.data(), received.size(), 0x40) == sizeof(payload) &&
std::memcmp(received.data(), payload, sizeof(payload)) == 0,
"guest PEEK and WAITALL preserve the wake bytes");
Check(Net::Recv(reader, received.data(), received.size(), 0x40) == sizeof(payload),
"consume wake bytes with guest WAITALL");
"Net receive consumes the same bytes after peeking");
#if !defined(_WIN32)
Check(Net::Recv(reader, received.data(), received.size(), 0x80) == -1 &&
*Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK,
"empty nonblocking receive translates guest errno");
Check(net_recv(reader, received.data(), received.size(), 0x80) ==
Libs::Network::NET_ERROR_EWOULDBLOCK &&
*net_errno == Libs::Posix::POSIX_EWOULDBLOCK,
"Net nonblocking receive translates POSIX failure and Net errno");
#endif
Check(Net::SocketClose(reader) == 0 && Net::SocketClose(writer) == 0,
"close wake sockets");
Check(net_send(-1, payload, sizeof(payload), 0) == Libs::Network::NET_ERROR_EBADF &&
*net_errno == Libs::Posix::POSIX_EBADF,
"Net send translates an invalid socket instead of returning POSIX minus one");
Check(net_recv(reader, nullptr, received.size(), 0) == Libs::Network::NET_ERROR_EFAULT &&
*net_errno == Libs::Posix::POSIX_EFAULT,
"Net receive translates an invalid output buffer");
Check(net_send(writer, payload, sizeof(payload), 0x100000) ==
Libs::Network::NET_ERROR_EOPNOTSUPP &&
*net_errno == Libs::Posix::POSIX_EOPNOTSUPP,
"Net send preserves the backend's unsupported crypto flag error");
#if defined(__linux__)
const int disconnected = Net::Socket(2, 1, 0);
Check(disconnected >= 0, "create unconnected socket for broken pipe check");
const auto previous_sigpipe = std::signal(SIGPIPE, SIG_DFL);
Check(previous_sigpipe != SIG_ERR, "set default SIGPIPE disposition for Net send");
const auto broken_send = net_send(disconnected, payload, sizeof(payload), 0);
std::signal(SIGPIPE, previous_sigpipe);
Check(broken_send == Libs::Network::NET_ERROR_EPIPE &&
*net_errno == Libs::Posix::POSIX_EPIPE,
"Net send reports a broken pipe without raising host SIGPIPE");
Check(Net::SocketClose(disconnected) == 0, "close unconnected socket");
#endif
Check(Net::SocketClose(writer) == 0, "close wake writer");
*net_errno = Libs::Posix::POSIX_EINVAL;
Check(net_recv(reader, received.data(), received.size(), 0) == 0 &&
*net_errno == Libs::Posix::POSIX_EINVAL,
"Net receive returns EOF without replacing errno");
Check(Net::SocketClose(reader) == 0, "close wake reader");
readable[reader / 64] = bit;
Check(Net::Select(reader + 1, readable.data(), nullptr, nullptr,
immediate.data()) == -1 &&
+156
View File
@@ -0,0 +1,156 @@
#include "common/lruCache.h"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <string>
#include <vector>
namespace {
void Check(bool value, const char *message) {
if (!value) {
std::fprintf(stderr, "LruCacheTests: failed: %s\n", message);
std::abort();
}
}
// Collect the objects reachable through ForEachItemBelow(), in visit order.
std::vector<std::string> Collect(Common::LeastRecentlyUsedCache<std::string, uint64_t> &cache,
uint64_t tick) {
std::vector<std::string> visited;
cache.ForEachItemBelow(tick, [&](const std::string &object) { visited.push_back(object); });
return visited;
}
void TestInsertAndVisitOrder() {
Common::LeastRecentlyUsedCache<std::string, uint64_t> cache;
const auto a = cache.Insert("a", 10);
const auto b = cache.Insert("b", 20);
const auto c = cache.Insert("c", 30);
Check(a != b && b != c && a != c, "Insert returned duplicate ids");
const auto all = Collect(cache, UINT64_MAX);
Check(all.size() == 3 && all[0] == "a" && all[1] == "b" && all[2] == "c",
"inserted items are not visited in insertion order");
const auto below = Collect(cache, 20);
Check(below.size() == 2 && below[0] == "a" && below[1] == "b",
"ForEachItemBelow visited an item above the tick");
const auto none = Collect(cache, 5);
Check(none.empty(), "ForEachItemBelow visited items below an empty tick");
}
void TestEarlyExitCallback() {
Common::LeastRecentlyUsedCache<std::string, uint64_t> cache;
(void)cache.Insert("a", 10);
(void)cache.Insert("b", 20);
(void)cache.Insert("c", 30);
std::vector<std::string> visited;
cache.ForEachItemBelow(UINT64_MAX, [&](const std::string &object) {
visited.push_back(object);
return true; // stop after the first item
});
Check(visited.size() == 1 && visited[0] == "a", "boolean callback did not stop the walk");
std::vector<std::string> all_visited;
cache.ForEachItemBelow(UINT64_MAX, [&](const std::string &object) {
all_visited.push_back(object);
return false;
});
Check(all_visited.size() == 3, "false-returning callback did not visit every item");
}
void TestTouchReordersAndSkips() {
Common::LeastRecentlyUsedCache<std::string, uint64_t> cache;
const auto a = cache.Insert("a", 10);
const auto b = cache.Insert("b", 20);
const auto c = cache.Insert("c", 30);
// Touching the last item must not change the order.
cache.Touch(c, 35);
auto order = Collect(cache, UINT64_MAX);
Check(order.size() == 3 && order[0] == "a" && order[1] == "b" && order[2] == "c",
"touching the newest item changed the order");
// An older or equal tick must be ignored.
cache.Touch(a, 10);
cache.Touch(a, 5);
order = Collect(cache, UINT64_MAX);
Check(order[0] == "a" && order[1] == "b" && order[2] == "c",
"an ignored touch changed the order");
// Touching the oldest item moves it to the newest end.
cache.Touch(a, 40);
order = Collect(cache, UINT64_MAX);
Check(order.size() == 3 && order[0] == "b" && order[1] == "c" && order[2] == "a",
"touching an item did not move it to the newest end");
// The touched item now carries its new tick for tick filtering
// (a=40, b=20, c=35 after the touches above). A cutoff of 30 detects
// a stale c tick: c is visited only if Touch(c, 35) updated it.
const auto below = Collect(cache, 30);
Check(below.size() == 1 && below[0] == "b",
"touched item did not adopt its new tick");
// Touch the middle item.
cache.Touch(b, 45);
order = Collect(cache, UINT64_MAX);
Check(order.size() == 3 && order[0] == "c" && order[1] == "a" && order[2] == "b",
"touching a middle item did not move it to the end");
}
void TestFreeAndIdReuse() {
Common::LeastRecentlyUsedCache<std::string, uint64_t> cache;
const auto a = cache.Insert("a", 10);
const auto b = cache.Insert("b", 20);
const auto c = cache.Insert("c", 30);
// Free the first and the last items.
cache.Free(a);
cache.Free(c);
auto order = Collect(cache, UINT64_MAX);
Check(order.size() == 1 && order[0] == "b", "Free left a freed item in the walk");
// Freed ids must be reused by later inserts.
const auto d = cache.Insert("d", 40);
Check(d == a || d == c, "Insert did not reuse a freed id");
const auto e = cache.Insert("e", 50);
Check(e != d && (e == a || e == c), "Insert reused the same freed id twice");
order = Collect(cache, UINT64_MAX);
Check(order.size() == 3 && order[0] == "b" && order[1] == "d" && order[2] == "e",
"reused ids are not visited in tick order");
// Touching a reused id must not disturb the other items.
cache.Touch(d, 60);
order = Collect(cache, UINT64_MAX);
Check(order.size() == 3 && order[0] == "b" && order[1] == "e" && order[2] == "d",
"touching a reused id changed the wrong items");
// Free everything, then rebuild.
cache.Free(b);
cache.Free(d);
cache.Free(e);
Check(Collect(cache, UINT64_MAX).empty(), "freeing every item left items in the walk");
const auto again = cache.Insert("again", 70);
const auto order2 = Collect(cache, UINT64_MAX);
Check(order2.size() == 1 && order2[0] == "again", "insert after a full free failed");
cache.Touch(again, 70);
Check(Collect(cache, 70).size() == 1, "touching a rebuilt item failed");
}
} // namespace
int main() {
TestInsertAndVisitOrder();
TestEarlyExitCallback();
TestTouchReordersAndSkips();
TestFreeAndIdReuse();
std::puts("LruCacheTests: all cases passed");
return 0;
}
+321
View File
@@ -0,0 +1,321 @@
// Exercise the production module with real SDL streams and fake USB endpoints.
#include <SDL3/SDL.h>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <vector>
namespace {
void Check(bool condition, const char* text) {
if (!condition) {
std::fprintf(stderr, "PadHapticsTests: %s\n", text);
std::abort();
}
}
struct Device {
SDL_AudioDeviceID id;
const char* name;
int channels;
};
struct Rumble {
Uint16 large, small;
Uint32 duration;
};
std::vector<Device> devices;
std::vector<SDL_AudioStream*> streams;
Rumble rumble {};
Uint64 now = 1000;
int audio_refs = 0, opens = 0, rumble_calls = 0;
bool fail_open = false, fail_resume = false;
int actual_channels = 4;
SDL_AudioDeviceID opened_device = 0;
SDL_AudioSpec opened_spec {};
} // namespace
namespace Fake {
Uint64 GetTicks() {
return now;
}
bool InitSubSystem(SDL_InitFlags flags) {
const bool result = SDL_InitSubSystem(flags);
if (result && (flags & SDL_INIT_AUDIO)) {
audio_refs++;
}
return result;
}
void QuitSubSystem(SDL_InitFlags flags) {
if (flags & SDL_INIT_AUDIO) {
audio_refs--;
}
SDL_QuitSubSystem(flags);
}
SDL_AudioDeviceID* GetAudioPlaybackDevices(int* count) {
*count = static_cast<int>(devices.size());
auto* ids = static_cast<SDL_AudioDeviceID*>(
SDL_malloc((devices.size() + 1) * sizeof(SDL_AudioDeviceID)));
for (size_t i = 0; i < devices.size(); i++) {
ids[i] = devices[i].id;
}
ids[devices.size()] = 0;
return ids;
}
const char* GetAudioDeviceName(SDL_AudioDeviceID id) {
for (const auto& device: devices) {
if (device.id == id) {
return device.name;
}
}
return nullptr;
}
bool GetAudioDeviceFormat(SDL_AudioDeviceID id, SDL_AudioSpec* spec, int*) {
if (id == 999) {
*spec = {SDL_AUDIO_F32, actual_channels, 48000};
return true;
}
for (const auto& device: devices) {
if (device.id == id) {
*spec = {SDL_AUDIO_F32, device.channels, 48000};
return true;
}
}
return false;
}
SDL_AudioStream* OpenAudioDeviceStream(SDL_AudioDeviceID id, const SDL_AudioSpec* spec,
SDL_AudioStreamCallback callback, void* userdata) {
opens++;
opened_device = id;
opened_spec = *spec;
if (fail_open) {
return nullptr;
}
auto* stream = SDL_CreateAudioStream(spec, spec);
Check(stream != nullptr && SDL_SetAudioStreamGetCallback(stream, callback, userdata),
"real SDL stream/callback creation failed");
streams.push_back(stream);
return stream;
}
bool ResumeAudioStreamDevice(SDL_AudioStream*) {
return !fail_resume;
}
void DestroyAudioStream(SDL_AudioStream* stream) {
if (stream == nullptr) {
return;
}
const auto it = std::find(streams.begin(), streams.end(), stream);
Check(it != streams.end(), "stream destroyed twice");
streams.erase(it);
SDL_DestroyAudioStream(stream);
}
SDL_AudioDeviceID GetAudioStreamDevice(SDL_AudioStream*) {
return 999;
}
SDL_GamepadType GetGamepadTypeForID(SDL_JoystickID id) {
return id == 1 ? SDL_GAMEPAD_TYPE_PS5 : SDL_GAMEPAD_TYPE_UNKNOWN;
}
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
return id == 1 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id)) : nullptr;
}
bool RumbleGamepad(SDL_Gamepad*, Uint16 large, Uint16 small, Uint32 duration) {
rumble = {large, small, duration};
rumble_calls++;
return true;
}
} // namespace Fake
#define SDL_GetTicks Fake::GetTicks
#define SDL_InitSubSystem Fake::InitSubSystem
#define SDL_QuitSubSystem Fake::QuitSubSystem
#define SDL_GetAudioPlaybackDevices Fake::GetAudioPlaybackDevices
#define SDL_GetAudioDeviceName Fake::GetAudioDeviceName
#define SDL_GetAudioDeviceFormat Fake::GetAudioDeviceFormat
#define SDL_OpenAudioDeviceStream Fake::OpenAudioDeviceStream
#define SDL_ResumeAudioStreamDevice Fake::ResumeAudioStreamDevice
#define SDL_DestroyAudioStream Fake::DestroyAudioStream
#define SDL_GetAudioStreamDevice Fake::GetAudioStreamDevice
#define SDL_GetGamepadTypeForID Fake::GetGamepadTypeForID
#define SDL_GetGamepadFromID Fake::GetGamepadFromID
#define SDL_RumbleGamepad Fake::RumbleGamepad
#include "libs/dualSenseHaptics.cpp"
#undef SDL_GetTicks
#undef SDL_InitSubSystem
#undef SDL_QuitSubSystem
#undef SDL_GetAudioPlaybackDevices
#undef SDL_GetAudioDeviceName
#undef SDL_GetAudioDeviceFormat
#undef SDL_OpenAudioDeviceStream
#undef SDL_ResumeAudioStreamDevice
#undef SDL_DestroyAudioStream
#undef SDL_GetAudioStreamDevice
#undef SDL_GetGamepadTypeForID
#undef SDL_GetGamepadFromID
#undef SDL_RumbleGamepad
namespace {
namespace Haptics = Libs::Controller::DualSenseHaptics;
using Port = std::unique_ptr<Haptics::Stream, decltype(&Haptics::Close)>;
constexpr std::array<int, 2> unity {32768, 32768};
constexpr std::array<float, 4> pcm {0.5f, 0.5f, 0.5f, 0.5f};
struct Fixture {
Fixture() {
devices = {{10, "Speakers (DualSense Wireless Controller)", 4}};
rumble = {};
now = 1000;
opens = rumble_calls = 0;
fail_open = fail_resume = false;
actual_channels = 4;
}
~Fixture() {
Haptics::Shutdown();
Check(streams.empty() && audio_refs == 0, "stream or audio subsystem reference leaked");
}
};
Port Open() {
auto* stream = Haptics::Open(48000);
Check(stream != nullptr, "port open failed");
return Port(stream, Haptics::Close);
}
void Queue(const Port& port, const void* data, uint32_t frames = 2, uint32_t channels = 2,
bool is_float = true, const int* volume = unity.data()) {
Haptics::Queue(port.get(), 1, data, frames, channels, is_float, volume);
}
void Pull() {
std::array<float, 64> buffer {};
Check(!streams.empty() &&
SDL_GetAudioStreamData(streams.back(), buffer.data(), sizeof(buffer)) >= 0,
"audio device pull failed");
}
void ExpectPcm(std::array<float, 8> expected) {
std::array<float, 8> actual {};
Check(!streams.empty() && SDL_GetAudioStreamData(streams.back(), actual.data(),
sizeof(actual)) == sizeof(actual),
"wrong output byte count");
for (size_t i = 0; i < actual.size(); i++) {
Check(std::abs(actual[i] - expected[i]) < 1e-6f, "haptics mapping or volume is wrong");
}
}
void TestFormatsAndVolume() {
Fixture f;
auto port = Open();
const std::array<int, 2> volume {32768, 16384};
const std::array<float, 4> stereo {0.5f, -0.25f, 1.0f, 0.0f};
Queue(port, stereo.data(), 2, 2, true, volume.data());
Check(opened_device == 10 && opened_spec.channels == 4 && opened_spec.format == SDL_AUDIO_F32,
"did not select quad DualSense PCM");
ExpectPcm({0, 0, 0.5f, -0.125f, 0, 0, 1.0f, 0});
const std::array<int16_t, 2> mono {16384, -32768};
Queue(port, mono.data(), 2, 1, false, volume.data() + 1);
ExpectPcm({0, 0, 0.25f, 0.25f, 0, 0, -0.5f, -0.5f});
std::array<float, 24> multichannel {};
multichannel.fill(0.9f);
multichannel[0] = 0.1f;
multichannel[1] = 0.2f;
multichannel[12] = 0.3f;
multichannel[13] = 0.4f;
Queue(port, multichannel.data(), 2, 12);
ExpectPcm({0, 0, 0.1f, 0.2f, 0, 0, 0.3f, 0.4f});
}
void TestDiscoveryAndHotplug() {
Fixture f;
devices = {{10, "DualSense Wireless Controller", 2}, {20, "Other speakers", 4}};
auto port = Open();
Queue(port, pcm.data());
Check(streams.empty() && opens == 0, "stereo controller or unrelated speakers selected");
devices[0].channels = 4;
now += 2001;
Queue(port, pcm.data());
Check(streams.size() == 1 && opened_device == 10, "late USB connection was not discovered");
devices.clear();
now += 2001;
Queue(port, pcm.data());
Check(streams.empty(), "disconnected endpoint retained its stream");
}
void TestFailuresAndBoundedQueue() {
Fixture f;
auto port = Open();
std::array<float, 128> block {};
block.fill(0.5f);
fail_open = true;
Queue(port, block.data(), 64);
Check(streams.empty() && audio_refs == 1, "failed open leaked a stream/reference");
fail_open = false;
fail_resume = true;
now += 2001;
Queue(port, block.data(), 64);
Check(streams.empty() && audio_refs == 1, "failed resume leaked a stream/reference");
fail_resume = false;
actual_channels = 2;
now += 2001;
Queue(port, block.data(), 64);
Check(streams.empty(), "post-open stereo downgrade accepted");
actual_channels = 4;
now += 2001;
for (int i = 0; i < 70; i++) {
Queue(port, block.data(), 64);
}
constexpr int block_bytes = 64 * 4 * sizeof(float);
constexpr int max_queue = 48000 * 4 * sizeof(float) * 80 / 1000;
Check(streams.size() == 1 &&
SDL_GetAudioStreamQueued(streams.back()) <= max_queue + block_bytes,
"queue grows indefinitely when its clock stalls");
}
void TestRumbleLeaseAndDuration() {
Fixture f;
auto port = Open();
Check(Haptics::SetVibration(1, 100, 50), "set rumble failed");
Check(rumble.large == 100 * 0x101 && rumble.duration == 65535, "ordinary rumble changed");
Queue(port, pcm.data());
Check(rumble.large == 0 && rumble.small == 0, "rumble was not stopped before haptics");
now = 1100;
const std::array<float, 4> silence {};
Queue(port, silence.data());
now = 1249;
Pull();
Check(rumble.large == 0, "rumble resumed before lease expiration");
now = 1250;
Pull();
Check(rumble.large == 100 * 0x101 && rumble.small == 50 * 0x101 && rumble.duration == 65285,
"idle haptics did not restore remaining rumble duration");
now = 66500;
Queue(port, pcm.data());
now = 66751;
Pull();
Check(rumble.large == 0 && rumble.duration == 0, "expired rumble restarted after haptics");
Haptics::Shutdown();
const int calls = rumble_calls;
Pull();
Check(rumble_calls == calls, "audio pull touched rumble after shutdown");
}
void TestCloseRestoresRumble() {
Fixture f;
auto first = Open(), second = Open();
Haptics::SetVibration(1, 100, 50);
Queue(first, pcm.data());
Queue(second, pcm.data());
Check(rumble.large == 0, "haptics did not suppress rumble");
first.reset();
Check(rumble.large == 0, "closing one of two streams restored rumble early");
second.reset();
Check(rumble.large == 100 * 0x101 && rumble.small == 50 * 0x101,
"closing the final stream left rumble suppressed");
}
} // namespace
int main() {
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
TestFormatsAndVolume();
TestDiscoveryAndHotplug();
TestFailuresAndBoundedQueue();
TestRumbleLeaseAndDuration();
TestCloseRestoresRumble();
std::printf("PadHapticsTests: all cases passed\n");
}
+25
View File
@@ -2815,6 +2815,31 @@ void TestGraphicsPushConstantLayout() {
}
void TestResourceLimitIsTransactional() {
Fixture accepted;
MemoryInfo accepted_memory;
accepted_memory.kind = ResourceKind::Buffer;
for (uint32_t index = 0; index < ShaderInfo::MaxBuffers; index++) {
const auto handle = accepted.Buffer(
{Value(index), Value(index + 1u), Value(index + 2u), Value(index + 3u)},
index * 4u);
accepted.Emit(ValueOpcode::LoadBufferU32,
{handle, Value(0u), Value(0u), Value(0u), Value(true)},
accepted.AddMemory(accepted_memory, index * 4u));
}
accepted.PlanAndTrack();
Check(accepted.program.info.buffers.size() == 64u &&
accepted.program.descriptor_sources.size() == 64u &&
accepted.program.memory_info.back().resource == 63u,
"compute shader did not retain all 64 distinct buffers");
ShaderComputeInputInfo compute{};
CollectShaderInfo(accepted.program, {.compute = &compute});
AllocateBindings(accepted.program);
const auto *binding = FindBinding(accepted.program.bindings,
DescriptorBindingKind::Buffers);
Check(binding != nullptr && binding->resources.size() == 64u &&
accepted.program.bindings.memory_offset_count == 64u,
"compute shader binding layout truncated the 64 buffers");
Fixture fixture;
MemoryInfo memory;
memory.kind = ResourceKind::Buffer;
File diff suppressed because it is too large Load Diff
+10 -5
View File
@@ -86,7 +86,11 @@ ShaderRecompiler::CompileOptions MakeCompileOptions(ShaderType stage) {
static const ShaderVertexInputInfo vertex{};
static const ShaderPixelInputInfo pixel{};
static const ShaderComputeInputInfo compute{};
static const std::array<uint32_t, 64> user_data{};
static const auto user_data = [] {
std::array<uint32_t, 64> data{};
data[3] = 3u << 28u; // Default fixture buffer uses raw offset bounds.
return data;
}();
ShaderRecompiler::CompileOptions options;
options.stage = stage;
@@ -2038,9 +2042,9 @@ void TestNewShaderRecompilerVop3LaneReadDestinationEncoding() {
options.dump_ir = true;
auto result = RecompileForTest(shader, options);
Check((result.decoded_dump.find("v_readfirstlane_b32 s25, v5") != std::string::npos),
Check((result.decoded_dump.find("V_READFIRSTLANE_B32 s25, v5") != std::string::npos),
"VOP3 V_READFIRSTLANE_B32 destination was not decoded from VDST");
Check((result.decoded_dump.find("v_readlane_b32 s26, v5, 2") != std::string::npos),
Check((result.decoded_dump.find("V_READLANE_B32 s26, v5, 2") != std::string::npos),
"VOP3 V_READLANE_B32 destination was not decoded from VDST");
CheckSpirvBinaryValidates(result.spirv);
}
@@ -3523,8 +3527,6 @@ void TestNewShaderRecompilerVop1SdwaNotDestination() {
check_rejected(0x00260400u, "V_NOT_B32 SDWA accepted source absolute");
check_rejected(0x00062400u, "V_NOT_B32 SDWA accepted clamp");
check_rejected(0x00064400u, "V_NOT_B32 SDWA accepted output modifier");
check_rejected(0x00000400u,
"V_NOT_B32 SDWA partial destination accepted a byte source");
}
void TestNewShaderRecompilerBootB16PackedAndSdwaOpcodes() {
@@ -6514,6 +6516,7 @@ void TestNewShaderRecompilerNativeWideScalarMemoryIr() {
void TestNewShaderRecompilerNativeWideBufferIr() {
const uint32_t shader[] = {
EncodeSMovB32(83, 255), 3u << 28u, // Raw bounds for the s[80:83] fixture.
EncodeMubuf0(0x0d, 0),
EncodeMubuf1(0, 20, 1), // buffer_load_dwordx2 v[0:1]
EncodeMubuf0(0x1d, 16),
@@ -13711,5 +13714,7 @@ int main() {
TestGraphicsPushConstantPlacement();
TestNewShaderRecompilerUnsupportedMemoryDecode();
TestNewShaderRecompilerVop3LaneReadDestinationEncoding();
return 0;
}