Compare commits

..
Author SHA1 Message Date
Matthew Hurleyandnmzik 1b6af301b5 tests: wire dormant VOP3 lane-read check and guard new dormant tests (#840)
Tests: enable lane-read coverage and clarify layout failures

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

* shader: support unsigned FFBH subword sources

* shader: apply cube-face result scaling

* shader: prune buffer reads proven out of bounds

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

---------

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

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

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

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

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

* Simplify DualSense audio haptics and restore idle rumble

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-09-27 00:37:27 +04:00
36 changed files with 1998 additions and 545 deletions

No files matched your search

+9
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)
@@ -477,6 +478,12 @@ 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"
)
@@ -569,6 +576,7 @@ 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 shader_recompiler_compute COMMAND $<TARGET_FILE:shader_recompiler_compute_tests>)
add_test(NAME shader_recompiler_alignbyte
@@ -636,6 +644,7 @@ if(BUILD_TESTING)
virtual_memory_allocation_tests
audio_out2_port_tests
ngs2_sampler_tests
pad_haptics_tests
save_data_memory_tests
)
endif()
+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);
+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 {
@@ -514,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,
@@ -560,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;
@@ -602,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,
@@ -679,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);
@@ -798,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) {
@@ -818,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);
@@ -830,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(
@@ -1045,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) {
@@ -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()) ||
@@ -537,6 +537,7 @@ constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = {
{Opcode::V_NOT_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(),
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), false},
{Opcode::V_FFBL_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_FFBH_U32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false},
{Opcode::V_CVT_F32_F16, SdwaSelWords() | SdwaSelFull(), 0, 0, true},
{Opcode::V_CVT_F16_F32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelWords(),
SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), true},
@@ -1379,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:
@@ -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++) {
@@ -1127,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;
};
+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++) {
+2 -3
View File
@@ -951,7 +951,7 @@ public:
return true;
}
auto now = CurrentTimeNoLock();
return now == 0 || candidate.info.time_stamp <= now;
return now == 0 || candidate.info.time_stamp + candidate.timestamp_offset <= now;
});
if (!frame) {
return false;
@@ -996,7 +996,7 @@ public:
out->details.audio.size = current_audio->info.details.audio.size;
std::memcpy(out->details.audio.language_code,
current_audio->info.details.audio.language_code, 4);
start_time_ms = out->time_stamp;
start_time_ms = current_audio->info.time_stamp + current_audio->timestamp_offset;
clock_start = std::chrono::steady_clock::now();
paused_extra = {};
RecordLoopBoundary(*current_audio);
@@ -1395,7 +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);
+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_
+14
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);
+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;
}
+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 &&
+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");
}
+497 -113
View File
@@ -1208,6 +1208,7 @@ struct TestCase {
std::vector<std::pair<std::string, size_t>> decoded_counts;
std::vector<std::pair<std::string, size_t>> ir_counts;
u32 expected_storage_mip_descriptors = 0;
std::optional<std::vector<u32>> expected_buffer_resources;
};
struct GraphicsCase {
@@ -1241,7 +1242,9 @@ struct CompiledShader {
std::array<u32, 64> MakeNativeUserData(const std::array<u32, 64> *source) {
std::array<u32, 64> data{};
data[3] = 3u << 28u;
data[50] = 1u << 20u;
data[51] = 3u << 28u;
if (source != nullptr) {
data = *source;
}
@@ -1564,18 +1567,26 @@ CompiledShader CompileCase(const TestCase &test, u32 host_subgroup_size = 64) {
"recompiler returned empty SPIR-V");
ValidateSpirv(test.name, result.spirv);
CheckSpirvText(test, result.spirv);
const auto *buffer_binding = ShaderRecompiler::IR::FindBinding(
result.program.bindings, ShaderRecompiler::IR::DescriptorBindingKind::Buffers);
if (test.expected_buffer_resources) {
Require(test.name, "live buffer bindings",
buffer_binding != nullptr &&
buffer_binding->resources == *test.expected_buffer_resources,
"dead buffer changed the surviving descriptor order");
}
std::vector<u32> packed_user_data;
for (const auto reg : result.program.bindings.user_data_registers) {
packed_user_data.push_back(
resources.user_data[reg - result.program.user_data_base]);
}
packed_user_data.resize(result.program.bindings.ShaderDataDwords());
const auto buffer_count =
static_cast<u32>(result.program.info.buffers.size());
const auto buffer_count = result.program.bindings.memory_offset_count;
for (u32 i = 0; i < buffer_count; i++) {
u32 offset = 0;
if (i < test.storage_buffer_offsets.size()) {
offset = test.storage_buffer_offsets[i];
const auto resource = buffer_binding->resources[i];
if (resource < test.storage_buffer_offsets.size()) {
offset = test.storage_buffer_offsets[resource];
}
Require(test.name, "shader data", offset % sizeof(u32) == 0 && offset < 256,
"storage buffer offset is not representable");
@@ -1593,7 +1604,8 @@ std::array<u32, 64> MakeStructuredStorageBufferData(u32 stride_bytes,
std::array<u32, 64> data{};
data[1] = (stride_bytes & 0x3fffu) << 16u;
data[2] = num_records;
data[3] = DstSel(4, 5, 6, 7) | (1u << 24u);
data[3] = DstSel(4, 5, 6, 7) | (1u << 24u) |
(stride_bytes == 0 ? 3u << 28u : 0u);
if (add_tid) {
data[3] |= 1u << 23u;
}
@@ -8306,12 +8318,18 @@ public:
uint32_t width, height, dimension, pitch;
uint64_t size;
Prospero::TileMode tile;
uint32_t levels = 1;
bool float16 = false;
};
const std::array cases{
TargetCase{width, height, 0, pitch, layout.size,
Prospero::TileMode::kRenderTarget},
// PPSA28068: the 1440x720 agreement target must alias its sampled texture.
TargetCase{1440, 720, 1, 1472, 0x40b000, Prospero::TileMode::kLinear},
// PPSA01416: linear RGBA16F chains place the smallest mip first.
TargetCase{128, 128, 1, 128, 0x2be00, Prospero::TileMode::kLinear, 7, true},
// PPSA03541: the single texel still occupies a padded 128x64 block.
TargetCase{1, 1, 1, 128, 0x10000, Prospero::TileMode::kRenderTarget, 1, true},
};
constexpr uint64_t allocation_size = 0x410000;
@@ -8341,10 +8359,13 @@ public:
HW::Shader shaders{};
registers.SetColorBase(0, {.addr = base});
registers.SetColorInfo(
0, {.format = Prospero::ChannelLayout::k8_8_8_8,
.channel_type = Prospero::ChannelType::kSrgb,
0, {.format = target.float16 ? Prospero::ChannelLayout::k16_16_16_16
: Prospero::ChannelLayout::k8_8_8_8,
.channel_type = target.float16 ? Prospero::ChannelType::kFloat
: Prospero::ChannelType::kSrgb,
.channel_order = Prospero::ChannelOrder::kStandard});
registers.SetColorAttrib2(0, {.height = height - 1, .width = width - 1});
registers.SetColorAttrib2(0, {.height = height - 1, .width = width - 1,
.num_mip_levels = target.levels - 1});
registers.SetColorAttrib3(0,
{.tile_mode = target.tile,
.dimension = target.dimension,
@@ -8374,11 +8395,13 @@ public:
color.desc.info.type == (is_1d ? Prospero::ImageType::kColor1D
: Prospero::ImageType::kColor2D) &&
color.desc.info.extent == vk::Extent3D{width, height, 1} &&
color.desc.info.resources == ImageSubresources{1, 1} &&
color.desc.info.resources == ImageSubresources{target.levels, 1} &&
color.desc.info.pitch == target.pitch &&
color.desc.info.data.size == target.size &&
color.desc.info.mip_layout[0].offset == 0 &&
color.desc.info.mip_layout[0].size == target.size &&
color.desc.info.mip_layout[0].offset ==
(target.levels == 1 ? 0 : 0xbe00) &&
color.desc.info.mip_layout[0].size ==
(target.levels == 1 ? target.size : 0x20000) &&
color.desc.view_info.type == (is_1d ? vk::ImageViewType::e1D
: vk::ImageViewType::e2D) &&
image.backing.image_type == (is_1d ? vk::ImageType::e1D
@@ -8388,42 +8411,117 @@ public:
rendering.num_color_attachments == 1,
"color attachment lost its dimensions or padded guest layout");
if (target.tile == Prospero::TileMode::kLinear) {
if (width == 1 && height == 1) {
Require(name, "single-texel tiled layout",
color.desc.info.mip_layout[0] == ImageMipInfo{0, 0x10000, 128, 64},
"the render target lost the expected padded block height");
}
if (target.levels == 7) {
constexpr std::array offsets{0xbe00u, 0x3e00u, 0x1e00u, 0xe00u,
0x600u, 0x200u, 0u};
constexpr std::array pitches{128u, 64u, 32u, 32u, 32u, 32u, 32u};
for (uint32_t mip = 0; mip < target.levels; ++mip) {
const auto &layout = color.desc.info.mip_layout[mip];
Require(name, "linear mip chain", layout.offset == offsets[mip] &&
layout.pitch == pitches[mip] &&
layout.size == pitches[mip] * (height >> mip) * 8,
"linear target mip offsets or padded pitches differ from the expected layout");
}
}
{
vk::ClearValue clear{};
clear.color.float32 = std::array{1.0f, 0.0f, 1.0f, 1.0f};
TextureCacheTestAccess::ClearImage(
texture_cache, scheduler.Current(), color.image_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, clear);
{vk::ImageAspectFlagBits::eColor, 0, target.levels, 0, 1}, clear);
RenderExecutorTestAccess::ResetBindings(executor);
ShaderTextureResource descriptor{{
static_cast<uint32_t>(base >> 8u),
(static_cast<uint32_t>(Prospero::BufferFormat::k8_8_8_8Srgb) << 20u) |
(static_cast<uint32_t>(target.float16
? Prospero::BufferFormat::k16_16_16_16Float
: Prospero::BufferFormat::k8_8_8_8Srgb) << 20u) |
(((width - 1u) & 3u) << 30u),
((width - 1u) >> 2u) | ((height - 1u) << 14u),
DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Prospero::ImageType::kColor2D) << 28u),
0, 0x00700000u, 0, 0}};
DstSel(4, 5, 6, 7) | ((target.levels - 1) << 16u) |
(static_cast<uint32_t>(target.tile) << 20u) |
(static_cast<uint32_t>(is_1d ? Prospero::ImageType::kColor1D
: Prospero::ImageType::kColor2D) << 28u),
0, 0x00700000u | ((target.levels - 1) << 4u), 0, 0}};
ShaderRecompiler::IR::DescriptorValue value{};
value.dword_count = 8;
std::copy_n(descriptor.fields, 8, value.dwords.begin());
ShaderRecompiler::IR::ImageResource resource{};
resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled;
resource.numeric_class = Prospero::TextureNumericClass::Float;
resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D;
resource.dimension = is_1d ? ShaderRecompiler::Decoder::ImageDimension::Dim1D
: ShaderRecompiler::Decoder::ImageDimension::Dim2D;
resource.read = true;
const auto sampled =
RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
Require(name, "linear target sampled alias",
Require(name, "target sampled alias",
sampled.image_id == color.image_id &&
sampled.desc.info.pitch == target.pitch &&
sampled.desc.info.data.size == target.size &&
sampled.desc.info.mip_layout == color.desc.info.mip_layout &&
texture_cache.FindTexture(sampled.image_id, sampled.desc) != nullptr,
"sampling the linear agreement target created a second image");
Require(name, "linear target sampled contents",
ReadCachedTexel(name, context, sampled.image_id,
{1439, 719, 0}) == std::vector<u32>{0xffff00ffu},
"the sampled linear target lost its GPU-written final texel");
"sampling the target changed its padded layout or created a second image");
const auto expected = target.float16
? std::vector<u32>{0x00003c00u, 0x3c003c00u}
: std::vector<u32>{0xffff00ffu};
for (uint32_t mip = 0; mip < target.levels; ++mip) {
Require(name, "target sampled contents",
ReadCachedTexel(name, context, sampled.image_id,
{static_cast<int32_t>((width >> mip) - 1),
static_cast<int32_t>((height >> mip) - 1), 0},
{1, 1, 1}, 0, mip) == expected,
"the sampled target lost its GPU-written final texel");
}
if (width == 1 && height == 1) {
for (const bool uint_view : {true, false}) {
descriptor.fields[1] = static_cast<uint32_t>(
uint_view ? Prospero::BufferFormat::k16_16_16_16UInt
: Prospero::BufferFormat::k16_16_16_16Float) << 20u;
descriptor.fields[3] = 0x81b00facu; // Captured 1D view, tile27, RGBA swizzle.
std::copy_n(descriptor.fields, 8, value.dwords.begin());
resource.numeric_class = uint_view ? Prospero::TextureNumericClass::Uint
: Prospero::TextureNumericClass::Float;
resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim1D;
const auto one_d =
RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
const auto one_d_view = texture_cache.FindTexture(one_d.image_id, one_d.desc);
const auto repeated =
RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
Require(name, "1D sampled backing and reuse",
one_d.image_id != color.image_id && one_d_view != nullptr &&
one_d.desc.view_info.type == vk::ImageViewType::e1D &&
texture_cache.GetImage(one_d.image_id).backing.image_type ==
vk::ImageType::e1D &&
repeated.image_id == one_d.image_id &&
texture_cache.FindTexture(repeated.image_id, repeated.desc) == one_d_view &&
ReadCachedTexel(name, context, one_d.image_id) == expected,
"the 1D alias reused an incompatible backing or lost the rendered texel");
RenderExecutorTestAccess::ResetBindings(executor);
RenderExecutorTestAccess::ResolveRenderColorTarget(
executor, scheduler.Current(), color, 0);
const auto restored = RenderExecutorTestAccess::AcquireRenderTargets(
executor, scheduler.Current(), &color, 1, no_depth);
scheduler.Current().BeginRendering(restored);
scheduler.Current().EndRendering();
Require(name, "2D target restoration",
color.image_id != one_d.image_id &&
restored.color_attachments[0].image_view != nullptr &&
texture_cache.GetImage(color.image_id).backing.image_type ==
vk::ImageType::e2D &&
texture_cache.GetImage(color.image_id).info.pixel_format ==
vk::Format::eR16G16B16A16Sfloat &&
ReadCachedTexel(name, context, color.image_id) == expected,
"restoring the 2D target lost its view type or GPU-written texel");
RenderExecutorTestAccess::ResetBindings(executor);
}
}
}
RenderExecutorTestAccess::ResetBindings(executor);
@@ -8931,8 +9029,8 @@ public:
std::printf("[gpu] %-32s ok\n", name);
}
void CheckRenderExecutorDccFixedClearFloat() {
constexpr const char *name = "RenderExecutorDccFixedClearFloat";
void CheckRenderExecutorColorMetadataClear() {
constexpr const char *name = "RenderExecutorColorMetadataClear";
constexpr uintptr_t base = 0x0000000204100000ull;
constexpr uint64_t allocation_size = 0x200000;
constexpr uint64_t allocation_alignment = 0x10000;
@@ -8941,11 +9039,13 @@ public:
uint32_t fill;
std::array<uint32_t, 2> texel;
bool reuse_unorm = false;
bool cmask = false;
};
constexpr std::array cases{
FillCase{0x40404040u, {0, 0x3c000000u}},
FillCase{0x80808080u, {0x3c003c00u, 0x00003c00u}},
FillCase{0x40404040u, {0, 0x3c000000u}, true},
FillCase{0, {0x804020ffu, 0}, false, true},
};
EnsureRuntimeContext();
// Astro's generic metadata fill, through S_ENDPGM; trailing debug data is omitted.
@@ -8983,9 +9083,9 @@ public:
{.type = Prospero::ShaderBinaryType::kCs,
.code_size_bytes = sizeof(native_fill)});
}
TileSizeAlign metadata_size{};
TileSizeAlign dcc_size{};
(void)TileGetDccSize(512, 256, 1, 8, 1,
Prospero::TileMode::kRenderTarget, metadata_size, 0);
Prospero::TileMode::kRenderTarget, dcc_size, 0);
int64_t direct_offset = -1;
Require(name, "direct allocation",
@@ -9003,6 +9103,10 @@ public:
std::memset(mapped, 0, allocation_size);
for (const auto &fill_case : cases) {
const uint32_t selected_layer = fill_case.cmask ? 1 : 0;
const uint64_t metadata_size = fill_case.cmask ? 0x2000 : dcc_size.size;
const std::vector<u32> expected(
fill_case.texel.begin(), fill_case.texel.begin() + (fill_case.cmask ? 1 : 2));
RenderContext context(m_runtime_context);
HW::Context registers{};
HW::UserConfig user_config{};
@@ -9013,17 +9117,23 @@ public:
auto &scheduler = context.GetCommandScheduler();
registers.SetColorBase(0, {.addr = base});
registers.SetColorInfo(
0, {.dcc_compression_enable = true,
.format = Prospero::ChannelLayout::k16_16_16_16,
.channel_type = Prospero::ChannelType::kFloat,
0, {.cmask_fast_clear_enable = !fill_case.reuse_unorm,
.dcc_compression_enable = !fill_case.cmask,
.format = fill_case.cmask ? Prospero::ChannelLayout::k8_8_8_8
: Prospero::ChannelLayout::k16_16_16_16,
.channel_type = fill_case.cmask ? Prospero::ChannelType::kUNorm
: Prospero::ChannelType::kFloat,
.channel_order = Prospero::ChannelOrder::kStandard});
registers.SetColorView(0, {.base_array_slice_index = selected_layer,
.last_array_slice_index = selected_layer});
registers.SetColorAttrib2(0, {.height = 255, .width = 511});
registers.SetColorAttrib3(0,
{.tile_mode = Prospero::TileMode::kRenderTarget,
.dimension = 1,
.metadata_pipe_aligned = true});
registers.SetColorDccAddr(0, {.addr = dcc_address});
registers.SetColorClearWord0(0, {.word0 = 0});
registers.SetColorCmask(0, {.addr = dcc_address});
registers.SetColorClearWord0(0, {.word0 = fill_case.cmask ? expected[0] : 0});
registers.SetColorClearWord1(0, {.word1 = 0});
registers.SetRenderTargetMask(0x0f);
scheduler.Begin(registers, user_config, shaders);
@@ -9064,7 +9174,7 @@ public:
}
executor.DispatchDirect(0, scheduler.Current(), (count + 63) / 64, 1, 1, 0x41u);
};
const auto metadata_words = static_cast<uint32_t>(metadata_size.size / 4);
const auto metadata_words = static_cast<uint32_t>(metadata_size / 4);
fill_metadata(metadata_words);
Require(name, "metadata before descriptor",
!texture_cache.IsMeta(dcc_address),
@@ -9083,28 +9193,36 @@ public:
vk::Format::eR16G16B16A16Unorm,
"the aliased clear did not reuse its existing UNORM allocation");
}
Require(name, "fixed clear on a float target",
Require(name, "color clear metadata discovery",
color.image_id &&
color.desc.info.metadata.kind == ImageMetadataKind::Dcc &&
color.desc.info.metadata.kind ==
(fill_case.cmask ? ImageMetadataKind::Cmask : ImageMetadataKind::Dcc) &&
color.desc.info.metadata.range.address == dcc_address &&
color.desc.info.metadata.range.size == metadata_size &&
rendering.num_color_attachments == 1 &&
!texture_cache.IsMeta(dcc_address),
"a DCC fixed clear code was not materialised on an RGBA16F "
"target");
const auto cleared = ReadCachedTexel(name, context, color.image_id);
Require(name, "GPU float clear value",
cleared ==
std::vector<u32>(fill_case.texel.begin(), fill_case.texel.end()),
"expected " + Hex(fill_case.texel[0]) + ", " + Hex(fill_case.texel[1]) +
"; got " + Hex(cleared[0]) + ", " + Hex(cleared[1]));
context.GetBufferCache().ReadMemory(dcc_address, metadata_size.size, false);
std::vector<uint8_t> expanded_metadata(metadata_size.size);
Require(name, "expanded native metadata",
LibKernel::Memory::TryReadBacking(dcc_address, expanded_metadata.data(),
expanded_metadata.size()) &&
std::all_of(expanded_metadata.begin(), expanded_metadata.end(),
[](uint8_t value) { return value == 0xff; }),
"materialized color retained stale native DCC clear codes");
"color metadata did not retain its type and full native allocation");
const auto read_texel = [&] {
return ReadCachedTexel(name, context, color.image_id, {}, {1, 1, 1}, selected_layer);
};
Require(name, "GPU color clear value",
read_texel() == expected &&
ReadCachedTexel(name, context, color.image_id, {511, 255, 0},
{1, 1, 1}, selected_layer) == expected,
"native metadata did not materialize the expected color");
const auto check_expanded_metadata = [&] {
context.GetBufferCache().ReadMemory(dcc_address, metadata_size, false);
std::vector<uint8_t> bytes(metadata_size);
Require(name, "expanded metadata readback",
LibKernel::Memory::TryReadBacking(dcc_address, bytes.data(), bytes.size()),
"native color metadata could not be read back");
const auto selected = bytes.begin() + (fill_case.cmask ? 0x1000 : 0);
Require(name, "selected metadata slice expansion",
std::all_of(bytes.begin(), selected, [](uint8_t v) { return v == 0; }) &&
std::all_of(selected, bytes.end(), [](uint8_t v) { return v == 0xff; }),
"materialization retained a clear key or changed an unselected slice");
};
check_expanded_metadata();
const auto bind = [&] {
RenderExecutorTestAccess::ResetBindings(executor);
@@ -9116,16 +9234,19 @@ public:
clear.color.float32 = std::array{1.0f, 0.0f, 1.0f, 1.0f};
TextureCacheTestAccess::ClearImage(
texture_cache, scheduler.Current(), color.image_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, clear);
{vk::ImageAspectFlagBits::eColor, 0, 1, selected_layer, 1}, clear);
};
std::vector<u32> painted{0x00003c00u, 0x3c003c00u};
if (fill_case.reuse_unorm) {
painted = {0x0000ffffu, 0xffffffffu};
} else if (fill_case.cmask) {
painted = {0xffff00ffu};
}
const auto retains_painted_texel = [&] {
const auto texels = ReadCachedTexel(name, context, color.image_id, {}, {512, 256, 1});
for (size_t i = 0; i < texels.size(); i += 2) {
if (texels[i] == painted[0] && texels[i + 1] == painted[1]) {
const auto texels = ReadCachedTexel(name, context, color.image_id, {},
{512, 256, 1}, selected_layer);
for (size_t i = 0; i < texels.size(); i += painted.size()) {
if (std::equal(painted.begin(), painted.end(), texels.begin() + i)) {
return true;
}
}
@@ -9134,8 +9255,19 @@ public:
paint();
bind();
Require(name, "unchanged metadata preserves rendering",
ReadCachedTexel(name, context, color.image_id) == painted,
"rebinding an unchanged DCC allocation reapplied an old clear");
read_texel() == painted,
"rebinding unchanged color metadata reapplied an old clear");
if (fill_case.cmask) {
WriteMetadata(context, dcc_address, metadata_size, 0x40404040u);
bind();
Require(name, "nonzero CMASK is not a DCC clear", read_texel() == painted,
"CMASK bytes were decoded as a DCC fixed-color key");
}
WriteMetadata(context, dcc_address, metadata_size, fill_case.fill);
bind();
Require(name, "CPU metadata re-clear", read_texel() == expected,
"CPU metadata writes did not clear an already drawn target");
paint();
fill_metadata(metadata_words - 1);
bind();
Require(name, "partial same-value metadata fill",
@@ -9144,11 +9276,10 @@ public:
fill_metadata(metadata_words, true);
bind();
Require(name, "same-value raw native re-clear",
ReadCachedTexel(name, context, color.image_id) ==
std::vector<u32>(fill_case.texel.begin(), fill_case.texel.end()),
read_texel() == expected,
"a repeated metadata fill did not clear an already drawn target");
paint();
WriteMetadata(context, dcc_address, metadata_size.size, UINT32_MAX);
WriteMetadata(context, dcc_address, metadata_size, UINT32_MAX);
fill_metadata(metadata_words - 1);
bind();
Require(name, "partial native metadata fill",
@@ -9157,8 +9288,7 @@ public:
fill_metadata(metadata_words);
bind();
Require(name, "complete fill after partial fill",
ReadCachedTexel(name, context, color.image_id) ==
std::vector<u32>(fill_case.texel.begin(), fill_case.texel.end()),
read_texel() == expected,
"completing the metadata overwrite did not restore its clear");
ImageDesc previous_depth{};
@@ -9175,7 +9305,7 @@ public:
previous_depth.info.tile_mode = Prospero::TileMode::kLinear;
previous_depth.info.mip_layout[0] = {0, 0x10000, 128, 128};
previous_depth.info.metadata.kind = ImageMetadataKind::Htile;
previous_depth.info.metadata.range = {dcc_address, metadata_size.size};
previous_depth.info.metadata.range = {dcc_address, metadata_size};
previous_depth.info.htile_clear_mask = 0;
previous_depth.view_info.format = vk::Format::eD32Sfloat;
previous_depth.view_info.type = vk::ImageViewType::e2D;
@@ -9188,21 +9318,15 @@ public:
"the old depth target did not register its metadata allocation");
paint();
bind();
Require(name, "HTile allocation reused as expanded DCC",
Require(name, "HTile allocation reused as expanded color metadata",
!texture_cache.IsMeta(dcc_address) && !texture_cache.ClearMeta(dcc_address) &&
ReadCachedTexel(name, context, color.image_id) == painted,
"DCC discovery retained the old HTile classification or erased rendered color");
read_texel() == painted,
"color discovery retained the old HTile classification or erased rendering");
fill_metadata(metadata_words);
bind();
context.GetBufferCache().ReadMemory(dcc_address, metadata_size.size);
Require(name, "native DCC dispatch after HTile reuse",
ReadCachedTexel(name, context, color.image_id) ==
std::vector<u32>(fill_case.texel.begin(), fill_case.texel.end()) &&
LibKernel::Memory::TryReadBacking(dcc_address, expanded_metadata.data(),
expanded_metadata.size()) &&
std::all_of(expanded_metadata.begin(), expanded_metadata.end(),
[](uint8_t byte) { return byte == 0xff; }),
"the former HTile entry swallowed the native DCC fill dispatch");
Require(name, "native color clear after HTile reuse", read_texel() == expected,
"the former HTile entry swallowed the native color metadata fill");
check_expanded_metadata();
RenderExecutorTestAccess::ResetBindings(executor);
resources.UnmapMemory(base, allocation_size);
scheduler.Finish();
@@ -10115,18 +10239,20 @@ public:
program.shader_info_complete = true;
ShaderRecompiler::IR::AllocateBindings(program);
};
const auto make_buffer_program =
[](ShaderType stage, ShaderRecompiler::IR::BufferResource resource) {
ShaderRecompiler::IR::CompiledShaderInfo program{};
program.stage = stage;
program.info.buffers.push_back(resource);
program.bindings.descriptors.push_back(
{ShaderRecompiler::IR::DescriptorBindingKind::Buffers, {0}});
program.bindings.memory_offset_count = 1;
program.bindings.push_data_start_dword = 0;
return program;
};
{
ShaderRecompiler::IR::Program buffer_ir{};
buffer_ir.stage = ShaderType::Compute;
buffer_ir.resource_tracking_complete = true;
buffer_ir.info.buffers.resize(1);
buffer_ir.info.buffers[0].read = true;
allocate_bindings(buffer_ir);
ShaderRecompiler::IR::CompiledShaderInfo buffer_program{};
buffer_program.stage = buffer_ir.stage;
buffer_program.info = std::move(buffer_ir.info);
buffer_program.bindings = std::move(buffer_ir.bindings);
const auto buffer_program = make_buffer_program(ShaderType::Compute, {.read = true});
constexpr uint64_t buffer_address = base + allocation_size - 0x5000;
ShaderBufferResource buffer_descriptor{};
@@ -10368,6 +10494,60 @@ public:
Prospero::TextureNumericClass::Uint;
sampled_overwide_resource.read = true;
sampled_overwide_resource.written = false;
{
// MLB The Show 21 compresses a standalone final block through mip 9.
constexpr uint64_t address = base + 0x180000;
ShaderRecompiler::IR::DescriptorValue descriptor{};
descriptor.dwords = {static_cast<uint32_t>(address >> 8u),
0x03e00000u, 0, 0x9019902cu, 0, 0x00700000u, 0, 0};
descriptor.dword_count = 8;
TestCase test;
test.name = "RebasedLastMipQueryStore";
test.has_user_data = true;
std::copy_n(descriptor.dwords.begin(), 8, test.user_data.begin());
test.opcodes = {ShaderOpcode::V_MOV_B32, ShaderOpcode::IMAGE_GET_RESINFO,
ShaderOpcode::IMAGE_STORE, ShaderOpcode::S_ENDPGM};
AppendVMovU32(&test.code, 24, 0);
AppendVMovU32(&test.code, 25, 0);
test.code.push_back(EncodeMimg0(0x0e, 0x3));
test.code.push_back(EncodeMimg1(0, 24));
test.code.push_back(EncodeMimg0(0x08, 0x3));
test.code.push_back(EncodeMimg1(0, 24));
AppendEnd(&test.code);
const auto compiled = CompileCase(test, SubgroupSize());
auto sampled_descriptor = descriptor;
sampled_descriptor.dwords[1] |= 9u << 16u;
const auto query = RenderExecutorTestAccess::ResolveTexture(
executor, compiled.program.info.images.at(0), sampled_descriptor);
const auto store = RenderExecutorTestAccess::ResolveTexture(
executor, compiled.program.info.images.at(1), descriptor);
Require(name, "rebased last mip",
query.image_id == store.image_id &&
query.desc.info.data.size == 256 &&
query.desc.info.resources.levels == 1 &&
query.desc.view_info.base_level == 0 &&
query.desc.view_info.level_count == 1 &&
query.desc.view_info.min_lod == 0 &&
store.desc.view_info.base_level == 0,
"equivalent selected mip changed backing or relative LOD");
Image sampled;
sampled.view = texture_cache.FindTexture(query.image_id, query.desc);
sampled.layout = vk::ImageLayout::eGeneral;
Image storage_view;
storage_view.view = texture_cache.FindTexture(store.image_id, store.desc);
storage_view.layout = vk::ImageLayout::eGeneral;
texture_cache.GetImage(store.image_id).Transit(
vk::ImageLayout::eGeneral,
vk::AccessFlagBits2::eShaderRead | vk::AccessFlagBits2::eShaderWrite,
{}, scheduler.Current().Handle());
scheduler.Finish();
Dispatch(test, compiled, {}, nullptr, &sampled, nullptr, &storage_view);
texture_cache.MarkGpuWritten(store.image_id);
Require(name, "rebased mip query and store",
ReadCachedTexel(name, context, store.image_id) ==
std::vector<uint32_t>{1, 1},
"query/store did not access the standalone 1x1 mip");
}
auto plain_mipped_storage_binding =
RenderExecutorTestAccess::ResolveTexture(executor, storage_resource,
mipped_storage_descriptor);
@@ -11776,6 +11956,36 @@ public:
"attachment usage leaked into the next draw");
}
// PPSA03541 reuses a tile24 depth allocation through a tile27 RGB10A2 T#.
auto pooled_resource = sampled_depth_resource;
pooled_resource.numeric_class = Prospero::TextureNumericClass::Float;
ImageId pooled_color_id{};
for (const bool color : {false, true, false, true}) {
auto value = shared_depth_value;
value.dwords[1] = static_cast<uint32_t>(phased_depth_address >> 40u) |
(static_cast<uint32_t>(color ? Prospero::BufferFormat::k10_10_10_2UNorm
: Prospero::BufferFormat::k32Float) << 20u);
value.dwords[3] = color ? 0x91b00facu : 0x91800204u;
const auto binding = RenderExecutorTestAccess::ResolveTexture(
executor, pooled_resource, value);
const auto view = texture_cache.FindTexture(binding.image_id, binding.desc);
if (color && !pooled_color_id) {
pooled_color_id = binding.image_id;
}
Require(name, "pooled depth/color tile identity",
view != nullptr &&
binding.image_id == (color ? pooled_color_id : phased_depth.image_id) &&
(!color || binding.image_id != phased_depth.image_id) &&
texture_cache.GetImage(binding.image_id).backing.format ==
(color ? vk::Format::eA2B10G10R10UnormPack32
: vk::Format::eD32SfloatS8Uint) &&
binding.desc.info.tile_mode ==
(color ? Prospero::TileMode::kRenderTarget
: Prospero::TileMode::kDepth),
"a changed tile family reused depth or repeated sampling lost its backing");
RenderExecutorTestAccess::ResetBindings(executor);
}
auto video_subresource =
make_target_desc(base + 0x20000, target_mip_size, {1, 1, 1});
video_subresource.type = BindingType::VideoOut;
@@ -11925,17 +12135,8 @@ public:
texture_cache, scheduler.Current(), expanded_array_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 1, 1}, clear);
ShaderRecompiler::IR::Program buffer_ir{};
buffer_ir.stage = ShaderType::Vertex;
buffer_ir.resource_tracking_complete = true;
buffer_ir.info.buffers.resize(1);
buffer_ir.info.buffers[0].read = true;
buffer_ir.info.buffers[0].formatted = true;
allocate_bindings(buffer_ir);
ShaderRecompiler::IR::CompiledShaderInfo buffer_program{};
buffer_program.stage = buffer_ir.stage;
buffer_program.info = std::move(buffer_ir.info);
buffer_program.bindings = std::move(buffer_ir.bindings);
const auto buffer_program = make_buffer_program(
ShaderType::Vertex, {.read = true, .formatted = true});
constexpr uint64_t buffer_size = 2 * target_mip_size;
static_assert(buffer_size > BufferCache::CACHING_PAGESIZE);
@@ -12601,17 +12802,8 @@ public:
}
auto plane_upload = CreateHostBuffer(name, plane_size,
vk::BufferUsageFlagBits::eTransferSrc, plane_words);
ShaderRecompiler::IR::Program plane_ir{};
plane_ir.stage = ShaderType::Compute;
plane_ir.resource_tracking_complete = true;
auto &plane_resource = plane_ir.info.buffers.emplace_back();
plane_resource.written = true;
plane_resource.formatted = true;
allocate_bindings(plane_ir);
ShaderRecompiler::IR::CompiledShaderInfo plane_program{};
plane_program.stage = plane_ir.stage;
plane_program.info = std::move(plane_ir.info);
plane_program.bindings = std::move(plane_ir.bindings);
const auto plane_program = make_buffer_program(
ShaderType::Compute, {.written = true, .formatted = true});
ShaderBufferResource plane_descriptor{};
plane_descriptor.UpdateAddress48(plane_address);
plane_descriptor.fields[1] |= 4u << 16u;
@@ -13233,8 +13425,9 @@ public:
info.offset = 0;
info.range = buffer.size;
if (test.storage_buffer_range_dwords != 0) {
const auto offset = i < test.storage_buffer_offsets.size()
? test.storage_buffer_offsets[i]
const auto resource = buffers->resources[i];
const auto offset = resource < test.storage_buffer_offsets.size()
? test.storage_buffer_offsets[resource]
: 0u;
info.range = static_cast<vk::DeviceSize>(
test.storage_buffer_range_dwords * sizeof(u32) + offset);
@@ -18876,6 +19069,31 @@ TestCase VectorFfbhI32NativeAndVop3OnGpu() {
return test;
}
TestCase Vop1SdwaFfbhCapturedScalarLowWordSource() {
using O = ShaderOpcode;
constexpr std::array inputs = {0xdead0000u, 0xffff0001u, 0x00008000u,
0x00010008u, 0xffff4000u, 0x0000ffffu};
std::vector<u32> code;
for (u32 i = 0; i < inputs.size(); i++) {
AppendSmemLoadOpcode(&code, 0x08, 39, i * 4u);
code.insert(code.end(), {0x7e0072f9u, 0x00840627u}); // v_ffbh_u32 v0, s39.word0
AppendStoreVgpr(&code, 0, i);
}
AppendEnd(&code);
TestCase test;
test.name = "Vop1SdwaFfbhCapturedScalarLowWordSource";
test.code = std::move(code);
test.initial.assign(inputs.begin(), inputs.end());
test.expected = {0xffffffffu, 31u, 16u, 28u, 17u, 16u};
test.opcodes = {O::S_BUFFER_LOAD_DWORD, O::V_FFBH_U32, O::V_MOV_B32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.decoded_counts = {{"V_FFBH_U32 v0, s39.sdwa(sel=4,sext=0)", inputs.size()}};
test.required_spirv = {"OpBitFieldUExtract", "FindUMsb"};
return test;
}
TestCase Vop1SdwaFfblCapturedHighWordSource() {
using O = ShaderOpcode;
@@ -21192,6 +21410,37 @@ TestCase Vop1MoveRelDestination() {
return test;
}
TestCase CubeIdCapturedNegationAndOutputScale() {
using O = ShaderOpcode;
constexpr std::array directions = {
std::array{4.0f, 2.0f, 1.0f}, std::array{-4.0f, 2.0f, 1.0f},
std::array{1.0f, -4.0f, 2.0f}, std::array{1.0f, 4.0f, 2.0f},
std::array{1.0f, 2.0f, -4.0f}, std::array{1.0f, 2.0f, 4.0f}};
constexpr std::array registers = {47u, 49u, 48u};
TestCase test;
test.name = "CubeIdCapturedNegationAndOutputScale";
for (u32 i = 0; i < directions.size(); i++) {
for (u32 component = 0; component < registers.size(); component++) {
test.initial.push_back(std::bit_cast<u32>(directions[i][component]));
AppendVMovU32(&test.code, 30, (i * 3u + component) * 4u);
AppendBufferLoadDword(&test.code, registers[component], 30);
}
// The captured instruction negates Y/Z and divides the floating face ID by two.
test.code.insert(test.code.end(), {0xd544000bu, 0xdcc2632fu});
AppendStoreVgpr(&test.code, 11, i);
}
AppendEnd(&test.code);
test.expected = {0x00000000u, 0x3f000000u, 0x3f800000u,
0x3fc00000u, 0x40000000u, 0x40200000u};
test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_CUBEID_F32,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.decoded_counts = {
{"V_CUBEID_F32 v11.omod(3), v47, v49.neg, v48.neg", directions.size()}};
test.required_spirv = {"OpFNegate", "OpFMul"};
return test;
}
TestCase VectorFloatSpecialOps() {
using O = ShaderOpcode;
@@ -24503,6 +24752,113 @@ TestCase BufferStoreFormatXDropsOutOfRangeRecord() {
return test;
}
TestCase TBufferCapturedZeroStrideOob(bool raw_bounds = false, bool scalar = false) {
using O = ShaderOpcode;
TestCase test;
test.name = scalar ? "TBufferZeroStrideOobPreservesScalarRead"
: raw_bounds ? "TBufferZeroStrideRawBoundsStillLoads"
: "TBufferCapturedZeroStrideOob";
test.initial = {0x11223344u, 0x55667788u, 0x99aabbccu, 0xddeeff00u,
0x12345678u, 0xdeadbeefu, 0xdeadbeefu};
test.expected = test.initial;
for (u32 i = 0; i < 4; i++) {
test.expected[1 + i] = raw_bounds ? test.initial[i] : 0u;
}
test.expected[5] = test.initial[4];
if (scalar) test.expected[6] = test.initial[0];
test.user_data = MakeStructuredStorageBufferData(4, test.initial.size());
std::copy_n(test.user_data.begin(), 4, test.user_data.begin() + 4);
test.user_data[4] = 0x2000u;
// Captured HITMAN descriptor in s[32:35]: mode 0, zero stride, arbitrary base.
test.user_data[32] = 0x4a398620u;
test.user_data[33] = 0;
test.user_data[34] = 0x2b5u;
test.user_data[35] = 0x0004d000u | (raw_bounds ? 3u << 28u : 0u);
test.has_user_data = true;
test.storage_buffer_offsets = {0, 0, 16};
test.expected_buffer_resources = raw_bounds || scalar
? std::vector<u32>{0, 1, 2}
: std::vector<u32>{0, 2};
auto &code = test.code;
AppendVMovLiteral(&code, 0, test.initial[0]);
AppendStoreVgpr(&code, 0, 0); // Keep resource zero before the removed middle slot.
AppendVMovU32(&code, 30, 0);
code.push_back(0xea6b2000u);
code.push_back(0x8008191eu); // Exact pc 0x47c: typed float4, s[32:35], v30.
AppendVMovU32(&code, 20, 0);
code.push_back(EncodeMubuf0(0x0cu));
code.push_back(EncodeMubuf1(33, 1, 20));
if (scalar) {
code.push_back(EncodeSmem0(0x08u, 40, 16));
code.push_back(EncodeSmem1(0));
code.push_back(EncodeVop1(0x01u, 32, 40));
}
for (u32 i = 0; i < 4; i++) AppendStoreVgpr(&code, 25 + i, 1 + i);
AppendStoreVgpr(&code, 33, 5);
if (scalar) AppendStoreVgpr(&code, 32, 6);
AppendEnd(&code);
test.opcodes = {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XYZW,
O::BUFFER_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
if (scalar) test.opcodes.push_back(O::S_BUFFER_LOAD_DWORD);
test.ir_counts = {{"LoadBufferU32x4", raw_bounds ? 1u : 0u}};
return test;
}
TestCase BufferZeroStrideOobFormatsAndWidths() {
using O = ShaderOpcode;
TestCase test;
test.name = "BufferZeroStrideOobFormatsAndWidths";
test.initial = std::vector<u32>(20, 0xdeadbeefu);
test.expected = std::vector<u32>(20, 0);
test.expected[10] = 0x3f800000u;
test.expected[12] = 0x3f800000u;
test.expected[14] = 1;
test.expected[16] = 1;
test.expected[18] = 0xabcdef01u;
test.expected[19] = 0xdeadbeefu;
test.user_data = MakeStructuredStorageBufferData(4, 20);
for (u32 base : {4u, 8u}) {
test.user_data[base] = 0x4a398620u;
test.user_data[base + 2] = 0x2b5u;
const u32 format = base == 4 ? 77u : 75u; // Four 32-bit float or unsigned integer components.
test.user_data[base + 3] = (format << 12u) | DstSel(1, 4, 1, 0);
}
test.has_user_data = true;
test.expected_buffer_resources = std::vector<u32>{0};
auto &code = test.code;
AppendVMovU32(&code, 0, 0);
AppendStoreVgpr(&code, 0, 0);
AppendVMovU32(&code, 20, 0);
u32 destination = 0;
for (const auto [opcode, width] :
{std::pair{0x08u, 1u}, {0x0au, 1u}, {0x0cu, 1u}, {0x0du, 2u}, {0x0fu, 3u}}) {
code.push_back(EncodeMubuf0(opcode));
code.push_back(EncodeMubuf1(destination, 1, 20));
destination += width;
}
AppendVMovU32(&code, 8, 0);
AppendVMovU32(&code, 9, 0);
for (u32 base : {1u, 2u}) {
code.push_back(EncodeMubuf0(0x03u));
code.push_back(EncodeMubuf1(base == 1 ? 10 : 14, base, 20));
}
AppendVMovLiteral(&code, 18, 0xabcdef01u);
code.push_back(EncodeSop1(0x04u, 60, 126u)); // Preserve the partial wave's active lanes.
code.push_back(EncodeSop1(0x04u, 126, InlineU32(0)));
code.push_back(EncodeMubuf0(0x00u));
code.push_back(EncodeMubuf1(18, 1, 20));
code.push_back(EncodeSop1(0x04u, 126, 60u));
for (u32 i = 0; i < 19; i++) AppendStoreVgpr(&code, i, i);
AppendEnd(&code);
test.opcodes = {O::V_MOV_B32, O::S_MOV_B64, O::BUFFER_LOAD_UBYTE,
O::BUFFER_LOAD_USHORT, O::BUFFER_LOAD_DWORD,
O::BUFFER_LOAD_DWORDX2, O::BUFFER_LOAD_DWORDX3,
O::BUFFER_LOAD_FORMAT_X, O::BUFFER_LOAD_FORMAT_XYZW,
O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.ir_counts = {{"LoadBuffer", 0}};
return test;
}
TestCase TBufferLoadVariants() {
using O = ShaderOpcode;
@@ -29001,6 +29357,7 @@ std::vector<TestCase> MakeCases() {
AddCase(VectorVop3MoveAppliesFloatSourceModifiers);
AddCase(VectorIntegerOps);
AddCase(VectorFfbhI32NativeAndVop3OnGpu);
AddCase(Vop1SdwaFfbhCapturedScalarLowWordSource);
AddCase(Vop1SdwaFfblCapturedHighWordSource);
AddCase(Vop1SdwaNotCapturedByte0Source);
AddCase(Vop1SdwaNotPreservesHighWordDestination);
@@ -29072,6 +29429,7 @@ std::vector<TestCase> MakeCases() {
AddCase(Vop1MoveRelSource);
AddCase(Vop1MoveRelDestination);
AddCase(VectorFloatSpecialOps);
AddCase(CubeIdCapturedNegationAndOutputScale);
AddCase(MadMixF16LiteralHalfSourceUsesOpsel);
AddCase(MadMixF16NegHiIsAbsAndNegIsIndependent);
AddCase(VectorVop3FmaF16UsesRdna2Opcode34b);
@@ -29173,6 +29531,10 @@ std::vector<TestCase> MakeCases() {
AddCase(BufferStoreFormatXAddTidUsesLaneIndex);
AddCase(BufferStoreFormatXDropsOutOfRangeRecord);
AddCase(TBufferLoadVariants);
AddCase([] { return TBufferCapturedZeroStrideOob(); });
AddCase([] { return TBufferCapturedZeroStrideOob(true); });
AddCase([] { return TBufferCapturedZeroStrideOob(false, true); });
AddCase(BufferZeroStrideOobFormatsAndWidths);
AddCase(TBufferLoadFormatXyzwSnapshotsOverlappingAddress);
AddCase(TBufferLoadFormatXyzwPackedSnapshotsOverlappingAddress);
AddCase(TBufferLoadFormatX8UintZeroExtendsByte);
@@ -34160,6 +34522,28 @@ int main(int argc, char **argv) {
RunCase(&vulkan, Vop1SdwaMovByteDestinations());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--sdwa-ffbh-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, Vop1SdwaFfbhCapturedScalarLowWordSource());
RunCase(&vulkan, VectorFfbhI32NativeAndVop3OnGpu());
RunCase(&vulkan, Vop1SdwaFfblCapturedHighWordSource());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--cubeid-omod-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, CubeIdCapturedNegationAndOutputScale());
RunCase(&vulkan, VectorFloatSpecialOps());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--zero-stride-oob-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, TBufferCapturedZeroStrideOob());
RunCase(&vulkan, TBufferCapturedZeroStrideOob(true));
RunCase(&vulkan, TBufferCapturedZeroStrideOob(false, true));
RunCase(&vulkan, BufferZeroStrideOobFormatsAndWidths());
RunCase(&vulkan, TBufferLoadFormatXIdxenUsesDescriptorStride());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--sdwa-ashr-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, Vop2SdwaAshrrevCapturedWord0SignExtends());
@@ -34244,7 +34628,7 @@ int main(int argc, char **argv) {
vulkan.CheckRasterization(false);
vulkan.CheckRenderExecutorColorDiscovery();
vulkan.CheckRenderExecutorColorVolumeDiscovery();
vulkan.CheckRenderExecutorDccFixedClearFloat();
vulkan.CheckRenderExecutorColorMetadataClear();
vulkan.CheckSampledDccClear();
vulkan.CheckRenderExecutorColorDepthTileDiscovery();
vulkan.CheckRenderExecutorStencilBindingDiscovery();
@@ -34275,7 +34659,7 @@ int main(int argc, char **argv) {
}
if (argc == 2 && std::strcmp(argv[1], "--dcc-clear-only") == 0) {
VulkanHarness vulkan;
vulkan.CheckRenderExecutorDccFixedClearFloat();
vulkan.CheckRenderExecutorColorMetadataClear();
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--compute-meta-clear-only") == 0) {
@@ -34284,7 +34668,7 @@ int main(int argc, char **argv) {
vulkan.CheckComputeMetaClearClassification();
vulkan.CheckNativeIndirectDispatch();
vulkan.CheckRenderExecutorColorVolumeDiscovery();
vulkan.CheckRenderExecutorDccFixedClearFloat();
vulkan.CheckRenderExecutorColorMetadataClear();
vulkan.CheckSampledDccClear();
vulkan.CheckRenderExecutorStencilBindingDiscovery();
return 0;
@@ -34480,7 +34864,7 @@ int main(int argc, char **argv) {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
vulkan.CheckRenderExecutorColorDiscovery();
vulkan.CheckRenderExecutorColorVolumeDiscovery();
vulkan.CheckRenderExecutorDccFixedClearFloat();
vulkan.CheckRenderExecutorColorMetadataClear();
vulkan.CheckSampledDccClear();
vulkan.CheckRenderExecutorColorStandardTileDiscovery();
vulkan.CheckRenderExecutorColorDepthTileDiscovery();
+10 -3
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);
}
@@ -6512,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),
@@ -13709,5 +13714,7 @@ int main() {
TestGraphicsPushConstantPlacement();
TestNewShaderRecompilerUnsupportedMemoryDecode();
TestNewShaderRecompilerVop3LaneReadDestinationEncoding();
return 0;
}