Compare commits

...

26 Commits

Author SHA1 Message Date
nmzik 45f55413e0 Keep sign-in dialogs active until user dismissal through shared system presentation 2026-10-03 17:31:16 +04:00
nmzik c3330caba2 Report NP Web API memory pool statistics for active library contexts 2026-10-03 17:31:16 +04:00
nmzik 92c22d60ef Support periodic kernel timers through shared event queue deadlines 2026-10-03 17:31:15 +04:00
nmzik a0858abe35 Implement batch AIO completion polling and deletion with per-request results 2026-10-03 17:31:15 +04:00
nmzik 5766b241c3 Implement POSIX thread stack attribute import through kernel setter 2026-10-03 17:31:15 +04:00
nmzik 7f158c656e Share sampler descriptor snapshots across native variants 2026-10-03 17:31:15 +04:00
nmzik 8b0cc79acf Preserve native slices when PS5 volume texture depth grows 2026-10-03 17:31:15 +04:00
Nawid3333 8cbee7f4b7 cmake: raise the bundled-dependency policy floor to 3.10 (#975)
CMake 4 warns "Compatibility with CMake < 3.10 will be removed" for every
fetched project that declares an older minimum. zydis and its bundled
zycore both declare cmake_minimum_required(VERSION 3.9), so each configure
prints two deprecation warnings. The existing floor of 3.5 does not lift
them past the threshold; 3.10 does.

Existing build directories keep the cached 3.5 until reconfigured with
-U CMAKE_POLICY_VERSION_MINIMUM or a fresh cache.

Tested on Windows 11, clang-cl 19.1.5, CMake 4.4.3: configure prints 0
deprecation warnings (2 with the old floor); virtual_memory_allocation_tests
builds and links zydis/xbyak; ctest guest_red_zone_patcher passes.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 17:24:03 +04:00
rocketrobot99 52c8ff18d0 Update gpu_format.cpp (#1003)
* Update gpu_format.cpp

Fix SRGB render-target formats (k8Srgb, k8_8Srgb)

Corrects render_target_bytes_per_element for SRGB formats:
- k8Srgb: 1 byte (was 0)
- k8_8Srgb: 2 bytes (was 0)

These formats appear in ChannelLayout::k8 and k8_8 render-target paths.
Incorrect RT byte sizes caused TextureCache render-target validation failures
in Lucky's Tale

* Fix narrow sRGB render-target format tests

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-03 16:57:59 +04:00
nmzik f53e5d2283 fix windows ci (#998)
* fix windows ci

* Fix Windows SPIR-V gather opcode narrowing conversion
2026-10-03 15:05:20 +04:00
frans bd611bfe15 Honor explicit gather LOD with shared image mip selection
Co-authored-by: YuhiAida <21134775+YuhiAida@users.noreply.github.com>
Co-authored-by: BlackCAT <52720489+psnwd@users.noreply.github.com>
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
Co-authored-by: Senaxx <michelbeld@gmail.com>
Co-authored-by: Mac <me@itsmemac.com>
Co-authored-by: Ali Almohaya <AliAlmohaya@gmail.com>
2026-10-03 14:19:33 +04:00
chenxiao 0bb2bdad17 Skip clean memory tracker ranges during buffer synchronization 2026-10-03 14:19:33 +04:00
nmzik 0ec3655f1c Clear single-mip color targets reused from cached mip chains 2026-10-03 14:19:33 +04:00
nmzik 190608ae20 Publish consumed color metadata without invalidating pooled images 2026-10-03 14:19:32 +04:00
nmzik ebe401d052 Preserve image contents across unrelated buffer alias writes 2026-10-03 14:19:32 +04:00
BlackCAT 0d4f993358 Keep reused BC5 textures in their own guest layout 2026-10-03 14:19:32 +04:00
BlackCAT ef4c5baf3a Implement RDNA2 64-bit image atomic maximum
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-10-03 14:19:32 +04:00
Hamza Shakir e6cb880adb Clamp compute LDS to the device limit and ignore unopenable gamepads 2026-10-03 04:27:38 +04:00
nmzik 8fa116eeb1 Remove redundant per-dispatch checks and buffer resets 2026-10-03 04:22:39 +04:00
nmzik e317465d8a fix linux CI (#972)
* fix linux CI

* Fix indirect image initialization in materialization tests
2026-10-02 17:20:12 +04:00
nmzik 684330c9e4 Specialize shader resources in place and fuse buffer refresh work 2026-10-02 16:16:31 +04:00
nmzik f3dc5ee4a9 Collect shader interface metadata in place without copying resources 2026-10-02 16:07:16 +04:00
nmzik 2545723065 Stub NetResolverAbort import AzqoBha7js4 with success 2026-10-02 15:49:55 +04:00
nmzik 95c89229ad Remove dead shader instruction cycles with linear liveness traversal 2026-10-02 15:49:43 +04:00
nmzik 7804eef9b6 Lower finite GPU-selected image descriptors through shared indirect bindings 2026-10-02 15:37:17 +04:00
Devran Cosmo Uenal 82a2f4ad42 controller: cycle DualSense speaker volume, vibration and trigger intensity in game (#946)
* controller: cycle DualSense speaker volume, vibration and trigger intensity in game

Three new input mapping controls cycle the PS5's controller settings in
game (default keys 1, 2 and 3): speaker volume (100% -> off -> 2% -> 12%
-> 42%), vibration intensity and trigger effect intensity (strong -> off
-> weak -> medium). Each starts at full and is not saved.

Changing vibration or trigger intensity re-sends the game's last rumble
and trigger effect, so the change is felt immediately.

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

* Clean up controller settings test captures and formatting

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-02 15:11:34 +04:00
65 changed files with 3140 additions and 733 deletions
+6 -6
View File
@@ -113,7 +113,7 @@ jobs:
shell: cmd
run: |
cmake --build _Build/windows --target launcher ^
audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests ^
shader_recompiler_compute_tests ^
resource_tracking_tests scalar_provenance_tests ^
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests --parallel
@@ -121,7 +121,7 @@ jobs:
- name: Test
shell: cmd
run: |
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
ctest --test-dir _Build/windows --output-on-failure -R "^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$"
- name: Install
shell: cmd
@@ -202,7 +202,7 @@ jobs:
shell: bash
run: |
cmake --build _Build/macos \
--target launcher audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
--target launcher audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
memory_tracker_tests page_manager_tests shader_recompiler_compute_tests \
resource_tracking_tests scalar_provenance_tests \
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
@@ -212,7 +212,7 @@ jobs:
shell: bash
run: |
ctest --test-dir _Build/macos --output-on-failure \
-R '^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
- name: Install
shell: bash
@@ -386,7 +386,7 @@ jobs:
run: |
cmake --build _Build/linux \
--target launcher page_manager_tests memory_tracker_tests \
audio_out2_port_tests pad_haptics_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
audio_out2_port_tests pad_haptics_tests controller_settings_tests ime_dialog_tests lru_cache_tests virtual_memory_allocation_tests \
resource_tracking_tests scalar_provenance_tests \
resource_materialization_tests shader_vertex_metadata_tests archive_file_tests avplayer_file_tests \
--parallel
@@ -395,7 +395,7 @@ jobs:
shell: bash
run: |
ctest --test-dir _Build/linux --output-on-failure \
-R '^(audio_out2_port|pad_haptics|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
-R '^(audio_out2_port|pad_haptics|controller_settings|ime_dialog|lru_cache|memory_tracker|page_manager|resource_tracking|scalar_provenance|resource_materialization|shader_vertex_metadata|archive_file|avplayer_file|virtual_memory_allocation)$'
- name: Install
shell: bash
+9 -1
View File
@@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.18...4.3)
if(CMAKE_VERSION VERSION_GREATER_EQUAL 4.0)
set(CMAKE_POLICY_VERSION_MINIMUM 3.5 CACHE STRING "Minimum policy version for bundled dependencies")
set(CMAKE_POLICY_VERSION_MINIMUM 3.10 CACHE STRING "Minimum policy version for bundled dependencies")
endif()
get_property(KYTY_GENERATOR_IS_MULTI_CONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
@@ -498,6 +498,12 @@ add_executable(pad_haptics_tests EXCLUDE_FROM_ALL
target_link_libraries(pad_haptics_tests common fmt::fmt SDL3::SDL3-static opus)
target_include_directories(pad_haptics_tests PRIVATE ${inc_headers})
add_executable(controller_settings_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/ControllerSettingsTests.cpp"
)
target_link_libraries(controller_settings_tests common fmt::fmt SDL3::SDL3-static)
target_include_directories(controller_settings_tests PRIVATE ${inc_headers})
add_executable(save_data_memory_tests EXCLUDE_FROM_ALL
"${KYTY_TESTS_DIR}/SaveDataMemoryTests.cpp"
)
@@ -613,6 +619,7 @@ if(BUILD_TESTING)
add_test(NAME audio_out_timing COMMAND $<TARGET_FILE:audio_out_timing_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 controller_settings COMMAND $<TARGET_FILE:controller_settings_tests>)
add_test(NAME save_data_memory COMMAND $<TARGET_FILE:save_data_memory_tests>)
add_test(NAME png_enc COMMAND $<TARGET_FILE:png_enc_tests>)
add_test(NAME ces COMMAND $<TARGET_FILE:ces_tests>)
@@ -690,6 +697,7 @@ if(BUILD_TESTING)
audio_out_timing_tests
ngs2_sampler_tests
pad_haptics_tests
controller_settings_tests
save_data_memory_tests
png_enc_tests
ces_tests
+2 -2
View File
@@ -61,8 +61,8 @@ constexpr FormatInfo kFormatInfo[] = {
{BufferFormat::k32_32_32_32UInt, 16, 0, 16, true, true},
{BufferFormat::k32_32_32_32SInt, 16, 0, 16, true, false, true},
{BufferFormat::k32_32_32_32Float, 16, 0, 16, true, false},
{BufferFormat::k8Srgb, 1, 0, 0, true, false},
{BufferFormat::k8_8Srgb, 2, 0, 0, true, false},
{BufferFormat::k8Srgb, 1, 0, 1, true, false},
{BufferFormat::k8_8Srgb, 2, 0, 2, true, false},
{BufferFormat::k8_8_8_8Srgb, 4, 0, 4, true, false},
{BufferFormat::k9_9_9_5Float, 4, 0, 0, true, false},
{BufferFormat::k5_6_5UNorm, 2, 0, 2, true, false},
+1
View File
@@ -29,6 +29,7 @@ struct GraphicContext {
bool memory_budget_ext_enabled = false;
bool compute_subgroup_size_control_enabled = false;
bool sample_rate_shading_enabled = false;
bool shader_image_int64_atomics_enabled = false;
// bool fp64_denorm_preserve = false; // Temporarily disabled.
bool attachment_feedback_loop_enabled = false;
bool provoking_vertex_last_enabled = false;
+3
View File
@@ -114,6 +114,9 @@ public:
void ForEachModifiedRange(uint64_t vaddr, uint64_t size, Func&& func) {
const auto [start, end] = GetPageRange(vaddr, size);
auto& bits = GetBits<source>();
if (bits.FirstRangeFrom(start).first >= end) {
return;
}
RegionBits mask(bits, start, end);
if constexpr (clear) {
bits.UnsetRange(start, end);
+24 -28
View File
@@ -791,13 +791,6 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
if (const auto depth_id = ResolveDepthOverlap(requested, binding, cached_id)) {
return {depth_id};
}
if (requested.IsBlock() && !cached.info.IsBlock()) {
return {ExpandImage(requested, cached_id)};
}
if (requested.data.size == cached.info.data.size &&
(requested.IsVolume() || cached.info.IsVolume())) {
return {ExpandImage(requested, cached_id)};
}
// Equal pitch does not imply equal mip placement: a changed extent can move
// a level into or out of the mip tail. These are separate guest layouts.
if (requested.tile_mode != cached.info.tile_mode ||
@@ -808,6 +801,19 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
}
return {merged_id};
}
if (requested.IsBlock() && !cached.info.IsBlock()) {
return {ExpandImage(requested, cached_id)};
}
// Volume depth is not an array-layer count. A larger depth can retain the
// same block-slice layout while requiring a larger native image.
if ((requested.IsVolume() || cached.info.IsVolume()) &&
(requested.data.size == cached.info.data.size ||
(requested.type == cached.info.type && requested.resources == cached.info.resources &&
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)};
}
// PPSA08394
// A view cannot change the native image type or grow its extent.
if (requested.data.size == cached.info.data.size &&
@@ -878,9 +884,7 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
ImageId TextureCache::ExpandImage(const ImageInfo& info, ImageId source_id) {
RefreshCopySource(source_id);
const auto expanded_id = InsertImage(info);
auto& expanded = m_slot_images[expanded_id];
auto& source = m_slot_images[source_id];
expanded.usage = source.usage;
if (source.binding.is_bound || source.binding.is_target) {
source.binding.needs_rebind = true;
}
@@ -926,22 +930,12 @@ TextureCache::BuildTextureTransfer(const Image& image, BindingType binding,
const bool upload = direction == TransferDirection::Upload;
const bool render_target = binding == BindingType::RenderTarget;
const bool video_out = binding == BindingType::VideoOut;
auto format = info.guest_format;
uint32_t layers = info.TransferLayers();
bool volume = info.IsVolume();
bool allow_depth_tile = upload;
const char* owner = "TextureCache readback";
TextureTransfer transfer;
transfer.swap_bgra16 = info.bgra16 && (!upload || render_target || video_out);
if (render_target) {
format = ImageOps::RenderTargetTransferFormat(info.bytes_per_block);
}
if (video_out) {
allow_depth_tile = false;
} else if (render_target || binding == BindingType::Storage) {
allow_depth_tile = true;
}
transfer.swap_bgra16 = info.bgra16;
if (upload) {
if ((render_target || video_out) &&
(info.resources.layers == 0 || info.data.size % info.resources.layers != 0 ||
@@ -963,9 +957,9 @@ TextureCache::BuildTextureTransfer(const Image& image, BindingType binding,
}
}
transfer.layout = TextureCalcUploadLayout(format, info.extent.width, info.extent.height,
info.resources.levels, layers, info.tile_mode,
info.data.size, allow_depth_tile, volume, owner);
transfer.layout = TextureCalcUploadLayout(info.guest_format, info.extent.width,
info.extent.height, info.resources.levels, layers,
info.tile_mode, info.data.size, volume, owner);
transfer.regions = TextureBuildImageCopies(transfer.layout);
if (info.IsDepth()) {
for (auto& region: transfer.regions) {
@@ -1133,7 +1127,7 @@ void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
image.info.metadata = desc.info.metadata;
// 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) {
if (range.size == 0 || desc.info.resources.levels != 1) {
return;
}
}
@@ -1181,10 +1175,12 @@ void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
{vk::ImageAspectFlagBits::eColor, view.base_level, view.level_count,
image_first + slice, 1}, clear);
}
// Native expanded keys own consumption. Existing buffer tracking publishes this CPU
// write to future GPU readers; FillBuffer can fault and must run outside the texture lock.
// Publish the conversion's expanded keys without treating them as guest writes
// to overlapping image data. Invalidate the buffer before updating its backing.
if (desc.type != BindingType::VideoOut) {
m_buffer_cache.FillBuffer(address, slice_size, UINT32_MAX, false);
std::fill(bytes.begin(), bytes.end(), uint8_t {0xff});
m_buffer_cache.InvalidateMemory(address, slice_size);
LibKernel::Memory::WriteBacking(address, bytes.data(), bytes.size());
}
}
}
@@ -1850,7 +1846,7 @@ void TextureCache::InvalidateMemoryFromGPU(uint64_t address, uint64_t size) {
std::scoped_lock lock {m_lock};
for (const auto id: FindImagesInRegion(address, size, true)) {
auto& image = m_slot_images[id];
if (!image.Overlaps(address, size)) {
if (image.info.data.address != address) {
continue;
}
if (image.IsGpuModified()) {
@@ -340,7 +340,9 @@ void Image::CopyImage(Image& source) {
EXIT_IF(source.backing.samples != backing.samples);
m_scheduler.EndRendering();
const uint32_t levels = std::min(source.backing.mip_levels, backing.mip_levels);
const uint32_t base_depth = backing.image_type == vk::ImageType::e3D
const uint32_t base_depth = source.backing.image_type == backing.image_type
? std::min(source.backing.extent.depth, backing.extent.depth)
: backing.image_type == vk::ImageType::e3D
? backing.extent.depth
: source.backing.extent.depth;
const auto source_aspect =
@@ -358,10 +358,8 @@ vk::ImageView Image::FindView(const ImageViewInfo& view_info) {
}
vk::ImageViewUsageCreateInfo usage {};
usage.usage = image.usage;
if (!is_storage) {
usage.usage &= ~vk::ImageUsageFlagBits::eStorage;
}
usage.usage = is_storage ? vk::ImageUsageFlagBits::eStorage
: image.usage & ~vk::ImageUsageFlagBits::eStorage;
vk::ImageViewMinLodCreateInfoEXT min_lod {};
if (normalized.min_lod != 0) {
min_lod.minLod = static_cast<float>(normalized.base_level) +
@@ -90,8 +90,11 @@ IsSupportedSampledDepthResource(const ShaderRecompiler::IR::ImageResource& resou
if (resource.numeric_class != Prospero::TextureNumericClass::Float && resource.depth_compare) {
return false;
}
return resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::None && resource.read &&
!resource.written && !resource.atomic;
return ((resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::None &&
resource.mip_count == 1u) ||
(resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::Dynamic &&
resource.mip_count != 0u)) &&
resource.read && !resource.written && !resource.atomic;
}
inline void ValidateStorageColorView(vk::Format image_format, vk::Format view_format,
@@ -114,7 +117,7 @@ IsSupportedStorageImageResource(const ShaderRecompiler::IR::ImageResource& resou
resource.dimension == ShaderRecompiler::Decoder::ImageDimension::Dim2DArray) &&
((resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::None &&
resource.mip_count == 1u) ||
(resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::DynamicStorage &&
(resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::Dynamic &&
resource.mip_count != 0u)) &&
resource.written &&
(!resource.atomic ||
@@ -175,8 +175,7 @@ SurfaceFormatInfo TextureGetSurfaceFormatInfo(Prospero::BufferFormat format) {
TextureUploadLayout TextureCalcUploadLayout(Prospero::BufferFormat format, uint32_t width,
uint32_t height, uint32_t levels, uint32_t depth,
Prospero::TileMode tile_mode, uint64_t upload_size,
bool allow_depth_tile, bool volume_texture,
const char* owner) {
bool volume_texture, const char* owner) {
TextureUploadLayout layout {};
layout.surface.description = {
format,
@@ -204,8 +203,7 @@ TextureUploadLayout TextureCalcUploadLayout(Prospero::BufferFormat format, uint3
static_cast<uint32_t>(format));
}
} else {
if ((tile_mode == Prospero::TileMode::kDepth && !allow_depth_tile) ||
!TileGetTiledTextureLayout(description, layout.surface)) {
if (!TileGetTiledTextureLayout(description, layout.surface)) {
EXIT("%s: unsupported typed tiled upload: fmt=%u tile=%u "
"size=%" PRIu64 " extent=%ux%u levels=%u\n",
owner, static_cast<uint32_t>(format), static_cast<uint32_t>(tile_mode),
@@ -47,8 +47,7 @@ RenderTargetFormatInfo TextureGetRenderTargetFormat(Prospero::ChannelLayout layo
TextureUploadLayout TextureCalcUploadLayout(Prospero::BufferFormat format, uint32_t width,
uint32_t height, uint32_t levels, uint32_t depth,
Prospero::TileMode tile_mode, uint64_t upload_size,
bool allow_depth_tile, bool volume_texture,
const char* owner);
bool volume_texture, const char* owner);
std::vector<vk::BufferImageCopy> TextureBuildImageCopies(const TextureUploadLayout& layout);
bool TextureBuildGpuTileInfos(uint64_t tiled_size, const std::vector<vk::BufferImageCopy>& regions,
const TextureUploadLayout& layout, uint32_t levels,
@@ -312,13 +312,14 @@ void ValidateStorageTexture(const ShaderRecompiler::IR::ImageResource& resource,
resource.written && !resource.read && !resource.atomic;
const auto numeric_class = Prospero::SampledTextureNumericClass(format);
const bool raw_float_atomic = format == Prospero::BufferFormat::k32Float && uint_resource &&
resource.atomic;
resource.atomic && !resource.atomic64;
const bool format_ok =
raw_sint_storage || raw_float_atomic ||
(numeric_class != Prospero::TextureNumericClass::Unsupported &&
numeric_class != Prospero::TextureNumericClass::Sint &&
uint_resource == (numeric_class == Prospero::TextureNumericClass::Uint) &&
(!resource.atomic || format == Prospero::BufferFormat::k32UInt));
(!resource.atomic || format == (resource.atomic64 ? Prospero::BufferFormat::k32_32UInt
: Prospero::BufferFormat::k32UInt)));
if (resource_ok && descriptor_ok && encoding_ok && format_ok && size != 0) {
return;
}
@@ -354,7 +355,8 @@ static TextureCache::ImageDesc NullTextureDesc(const ShaderRecompiler::IR::Image
desc.info.guest_format = Prospero::BufferFormat::k32Float;
break;
case Prospero::TextureNumericClass::Uint:
desc.info.guest_format = Prospero::BufferFormat::k32UInt;
desc.info.guest_format = resource.atomic64 ? Prospero::BufferFormat::k32_32UInt
: Prospero::BufferFormat::k32UInt;
break;
case Prospero::TextureNumericClass::Sint:
desc.info.guest_format = Prospero::BufferFormat::k32SInt;
@@ -365,10 +367,10 @@ static TextureCache::ImageDesc NullTextureDesc(const ShaderRecompiler::IR::Image
desc.info.type = Prospero::ImageType::kColor2D;
desc.info.extent = {1, 1, 1};
desc.info.resources = {1, 1};
desc.info.bytes_per_block = 4;
desc.info.bytes_per_block = Prospero::NumBytesPerElement(desc.info.guest_format);
desc.info.samples = 1;
desc.info.mip_layout[0] = {0, 0, 1, 1};
desc.view_info.format = desc.info.pixel_format;
desc.view_info.format = resource.atomic64 ? vk::Format::eR64Uint : desc.info.pixel_format;
desc.view_info.type = vk::ImageViewType::e2D;
desc.view_info.aspect = vk::ImageAspectFlagBits::eColor;
desc.view_info.usage = binding == TextureCache::BindingType::Storage
@@ -538,6 +540,9 @@ static bool ResolveTextureMipView(const TileSurfaceDescription& description, boo
TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageResource& resource,
const ShaderRecompiler::IR::DescriptorValue& value) {
if (resource.atomic64 && !m_context.GetGraphics().shader_image_int64_atomics_enabled) {
EXIT("64-bit image atomics require shaderImageInt64Atomics\n");
}
auto descriptor = DecodeNativeDescriptor<ShaderTextureResource>(value);
const bool storage = resource.written;
if (storage) {
@@ -563,7 +568,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
const auto physical_levels = multisampled ? 1u : static_cast<uint32_t>(max_mip) + 1u;
// IMAGE_STORE addresses BASE_LEVEL; only IMAGE_STORE_MIP selects other view mips.
const bool single_storage_mip =
storage && resource.mip_mode != ShaderRecompiler::IR::ImageMipMode::DynamicStorage;
storage && resource.mip_mode != ShaderRecompiler::IR::ImageMipMode::Dynamic;
const auto view_levels = multisampled || single_storage_mip
? 1u
: static_cast<uint32_t>(last_level - base_level) + 1u;
@@ -650,8 +655,9 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
pixel_format = depth_format->depth_attachment_format;
}
}
const auto storage_view_format = storage && (resource.atomic ||
format == Prospero::BufferFormat::k32SInt)
const auto storage_view_format = resource.atomic64 ? vk::Format::eR64Uint
: storage && (resource.atomic ||
format == Prospero::BufferFormat::k32SInt)
? vk::Format::eR32Uint
: SrgbStorageViewFormat(pixel_format);
const auto view_format = storage && storage_view_format != vk::Format::eUndefined
@@ -773,8 +779,6 @@ void RenderExecutor::PrepareBindings(const ShaderStageRuntime& runtime,
prepared.gds = {nullptr, 0, VK_WHOLE_SIZE};
prepared.flattened_srt = {};
prepared.shader_data_buffer = {};
prepared.buffer_sources.clear();
prepared.buffers.clear();
prepared.images.resize(program.info.images.size());
prepared.samplers.clear();
prepared.shader_data.clear();
@@ -787,7 +791,8 @@ void RenderExecutor::PrepareBindings(const ShaderStageRuntime& runtime,
}
prepared.samplers.reserve(program.info.samplers.size());
for (uint32_t i = 0; i < program.info.samplers.size(); i++) {
prepared.samplers.push_back(NativeSampler(m_context, program, i, snapshot.samplers[i]));
prepared.samplers.push_back(NativeSampler(
m_context, program, i, snapshot.samplers[program.info.samplers[i].snapshot_index]));
}
prepared.shader_data.reserve(program.bindings.ShaderDataDwords());
for (const auto reg: program.bindings.user_data_registers) {
@@ -887,7 +892,7 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
auto& binding = images[i];
binding.mip_views.clear();
const auto& resource = program.info.images[i];
if (resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::DynamicStorage) {
if (resource.mip_mode == ShaderRecompiler::IR::ImageMipMode::Dynamic) {
EXIT_IF(resource.mip_count == 0u ||
resource.mip_count != binding.desc.view_info.level_count);
binding.mip_views.reserve(resource.mip_count);
@@ -895,6 +900,8 @@ void RenderExecutor::RebindImages(PreparedBindings& prepared) {
auto desc = binding.desc;
desc.view_info.base_level += mip;
desc.view_info.level_count = 1;
// The shader selects the mip after applying the guest minimum LOD.
desc.view_info.min_lod = 0;
binding.mip_views.push_back(texture_cache.FindTexture(binding.image_id, desc));
}
binding.image_view = binding.mip_views.front();
@@ -658,6 +658,19 @@ ShaderProgram PipelineCache::GetComputeProgram(const HW::ComputeShaderInfo& regs
ShaderComputeInputInfo& input_info) {
input_info.host_subgroup_size = m_graphics.SupportsComputeWave64() ? 64u : 32u;
const auto params = PrepareProgram(regs, sh, input_info);
// Use one effective size for the cache key, LDS declaration, and access bounds.
const auto max_lds_dwords =
m_graphics.GetPhysicalDeviceProperties().limits.maxComputeSharedMemorySize / 4u;
if (input_info.lds_size_dwords > max_lds_dwords) {
static std::atomic_bool warned = false;
if (!warned.exchange(true, std::memory_order_relaxed)) {
PipelineCacheLog("GPU warning: game compute shader requests {} bytes of LDS, but "
"the Vulkan device limit is {} bytes. Clamping LDS; rendering may "
"be incorrect.",
input_info.lds_size_dwords * 4u, max_lds_dwords * 4u);
}
}
input_info.lds_size_dwords = std::min(input_info.lds_size_dwords, max_lds_dwords);
uint32_t push_data_cursor = 0;
return m_program_cache->Get(params, input_info, push_data_cursor);
}
@@ -321,7 +321,8 @@ void RenderExecutor::DispatchDirect(uint64_t submit_id, CommandBuffer& buffer,
static_cast<uint32_t>(r.TileMode()));
}
for (uint32_t i = 0; i < program.info.samplers.size(); i++) {
const auto r = DecodeNativeDescriptor<ShaderSamplerResource>(resources.samplers[i]);
const auto r = DecodeNativeDescriptor<ShaderSamplerResource>(
resources.samplers[program.info.samplers[i].snapshot_index]);
LOGF(" CS sampler[%u]: source=%u clamp=%u/%u/%u filter=%u/%u/%u mip=%u "
"lod=%u-%u bias=%d\n",
i, program.info.samplers[i].source, static_cast<uint32_t>(r.ClampX()),
+36 -28
View File
@@ -29,9 +29,9 @@ namespace Libs::Graphics {
namespace {
namespace CoreIme = Libs::Ime;
namespace DialogIme = Libs::Dialog::ImeDialog;
namespace ErrorDialog = Libs::Dialog::ErrorDialog;
namespace CoreIme = Libs::Ime;
namespace DialogIme = Libs::Dialog::ImeDialog;
namespace SystemDialog = Libs::Dialog::SystemDialog;
namespace Ime {
@@ -104,7 +104,7 @@ bool HostQueueExternalInput(uint64_t generation, ExternalInput input) {
} // namespace Ime
enum class OverlayKind : uint8_t { None, Ime, Error };
enum class OverlayKind : uint8_t { None, Ime, Dialog };
struct OverlaySession {
OverlayKind kind = OverlayKind::None;
@@ -114,14 +114,14 @@ struct OverlaySession {
};
struct OverlaySnapshot {
OverlaySession session;
Ime::HostSnapshot ime;
ErrorDialog::HostSnapshot error;
OverlaySession session;
Ime::HostSnapshot ime;
SystemDialog::HostSnapshot dialog;
};
bool GetOverlaySnapshot(OverlaySnapshot* snapshot) {
if (ErrorDialog::GetHostSnapshot(&snapshot->error)) {
snapshot->session = {OverlayKind::Error, snapshot->error.generation};
if (SystemDialog::GetHostSnapshot(&snapshot->dialog)) {
snapshot->session = {OverlayKind::Dialog, snapshot->dialog.generation};
return true;
}
if (Ime::GetHostSnapshot(&snapshot->ime)) {
@@ -234,7 +234,7 @@ void RefreshVisibility() {
bool capture_keyboard = false;
bool text_input = false;
bool multiline = false;
if (snapshot.session.kind == OverlayKind::Error) {
if (snapshot.session.kind == OverlayKind::Dialog) {
capture_controller = true;
capture_keyboard = true;
} else if (snapshot.session.kind == OverlayKind::Ime) {
@@ -429,14 +429,14 @@ void InitializeSystemOverlayInput(SDL_Window* window) {
}
CoreIme::SetVisibilityCallback(OnCoreVisibilityChanged);
DialogIme::SetVisibilityCallback(OnDialogVisibilityChanged);
ErrorDialog::SetVisibilityCallback(RefreshVisibility);
SystemDialog::SetVisibilityCallback(RefreshVisibility);
RefreshVisibility();
}
void ShutdownSystemOverlayInput() {
CoreIme::SetVisibilityCallback(nullptr);
DialogIme::SetVisibilityCallback(nullptr);
ErrorDialog::SetVisibilityCallback(nullptr);
SystemDialog::SetVisibilityCallback(nullptr);
{
std::scoped_lock lock(g_visibility_mutex);
g_input_lifecycle_active = false;
@@ -466,9 +466,9 @@ void ShutdownSystemOverlayInput() {
SystemOverlayVisualState GetSystemOverlayVisualState() noexcept {
const auto core = CoreIme::GetVisualState();
const auto dialog = DialogIme::GetVisualState();
const auto error = ErrorDialog::GetVisualState();
return {core.active || dialog.active || error.active,
core.revision + dialog.revision + error.revision};
const auto system = SystemDialog::GetVisualState();
return {core.active || dialog.active || system.active,
core.revision + dialog.revision + system.revision};
}
bool ProcessSystemOverlayInput(const SDL_Event& event) {
@@ -521,10 +521,11 @@ bool ProcessSystemOverlayInput(const SDL_Event& event) {
if (keyboard_event && !g_input_keyboard) {
return false;
}
if (keyboard_event && session.kind == OverlayKind::Error) {
if (keyboard_event && session.kind == OverlayKind::Dialog) {
if (event.type == SDL_EVENT_KEY_DOWN && !event.key.repeat &&
(event.key.key == SDLK_RETURN || event.key.key == SDLK_KP_ENTER)) {
ErrorDialog::HostAccept(generation);
(event.key.key == SDLK_RETURN || event.key.key == SDLK_KP_ENTER ||
event.key.key == SDLK_ESCAPE)) {
SystemDialog::HostClose(generation);
}
return true;
}
@@ -904,7 +905,7 @@ struct SystemOverlay::Impl {
ImGui::End();
}
void DrawError(const ErrorDialog::HostSnapshot& snapshot, vk::Extent2D extent) {
void DrawDialog(const SystemDialog::HostSnapshot& snapshot, vk::Extent2D extent) {
const ImVec2 display(static_cast<float>(extent.width), static_cast<float>(extent.height));
const float scale = std::max(std::min(display.x / 1280.0f, display.y / 720.0f), 0.5f);
ImGui::GetBackgroundDrawList()->AddRectFilled({0.0f, 0.0f}, display,
@@ -922,17 +923,23 @@ struct SystemOverlay::Impl {
constexpr ImGuiWindowFlags flags = ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove |
ImGuiWindowFlags_NoSavedSettings |
ImGuiWindowFlags_AlwaysAutoResize;
ImGui::Begin("##SystemError", nullptr, flags);
ImGui::TextUnformatted("Error");
const bool signin = snapshot.kind == SystemDialog::Kind::Signin;
ImGui::Begin("##SystemDialog", nullptr, flags);
ImGui::TextUnformatted(signin ? "Sign in to PlayStation Network" : "Error");
ImGui::Separator();
const auto error_code = static_cast<uint32_t>(snapshot.error_code);
ImGui::TextWrapped("%s", error_code == 0x80550006u
ImGui::TextWrapped("%s", signin ? "PlayStation Network sign-in is unavailable."
: error_code == 0x80550006u
? "You are not signed in to PlayStation Network."
: "An error has occurred.");
ImGui::TextDisabled("Error code: 0x%08X", error_code);
if (!signin) {
ImGui::TextDisabled("Error code: 0x%08X", error_code);
}
const float button_width = std::min(140.0f * scale, ImGui::GetContentRegionAvail().x);
ImGui::SetCursorPosX((ImGui::GetWindowSize().x - button_width) * 0.5f);
const bool accepted = ImGui::Button("OK", {button_width, 44.0f * scale});
const bool accepted =
ImGui::Button(signin ? "Cancel" : "OK", {button_width, 44.0f * scale}) ||
(signin && ImGui::IsKeyPressed(ImGuiKey_GamepadFaceRight, false));
if (focus_pending) {
ImGui::SetItemDefaultFocus();
focus_pending = false;
@@ -941,7 +948,7 @@ struct SystemOverlay::Impl {
ImGui::PopFont();
ImGui::PopStyleVar(2);
if (accepted) {
ErrorDialog::HostAccept(snapshot.generation);
SystemDialog::HostClose(snapshot.generation);
}
}
@@ -955,7 +962,8 @@ struct SystemOverlay::Impl {
if (session != snapshot.session) {
session = snapshot.session;
focus_pending = true;
shift = (snapshot.ime.option & Ime::OPTION_NO_AUTO_CAPITALIZE) == 0;
shift = session.kind == OverlayKind::Ime &&
(snapshot.ime.option & Ime::OPTION_NO_AUTO_CAPITALIZE) == 0;
symbol_mode = false;
panel_offset = {};
right_stick = {};
@@ -981,8 +989,8 @@ struct SystemOverlay::Impl {
ImGui::EndFrame();
return false;
}
if (snapshot.session.kind == OverlayKind::Error) {
DrawError(snapshot.error, frame_extent);
if (snapshot.session.kind == OverlayKind::Dialog) {
DrawDialog(snapshot.dialog, frame_extent);
} else {
DrawIme(snapshot.ime, frame_extent);
}
@@ -25,6 +25,8 @@ struct ControlInfo {
Controller::Axis axis = Controller::Axis::AxisMax;
bool positive = false;
float touch_x = 0.0f;
Controller::Setting setting {};
};
static constexpr std::array CONTROL_INFO = {
@@ -55,6 +57,10 @@ static constexpr std::array CONTROL_INFO = {
ControlInfo {"RightStickRight", 0, Controller::Axis::RightX, true},
ControlInfo {"RightStickUp", 0, Controller::Axis::RightY, false},
ControlInfo {"RightStickDown", 0, Controller::Axis::RightY, true},
ControlInfo {.name = "SpeakerVolume", .setting = Controller::Setting::SpeakerVolume},
ControlInfo {.name = "VibrationIntensity", .setting = Controller::Setting::VibrationIntensity},
ControlInfo {.name = "TriggerEffectIntensity",
.setting = Controller::Setting::TriggerEffectIntensity},
};
constexpr std::size_t INVALID_CONTROL = CONTROL_INFO.size();
@@ -242,6 +248,12 @@ void SetStickAxis(Controller::Axis axis, bool negative, bool positive) {
Controller::SetAxis(Controller::HOST_INPUT_CONTROLLER_ID, axis, value);
}
void CycleSettingOnPress(Controller::Setting setting, bool down) {
if (down) {
Controller::CycleSetting(setting);
}
}
void SetControl(std::size_t control, bool down) {
if (control == INVALID_CONTROL) {
return;
@@ -256,6 +268,10 @@ void SetControl(std::size_t control, bool down) {
SetButton(info.button, down);
return;
}
if (info.axis == Controller::Axis::AxisMax) {
CycleSettingOnPress(info.setting, down);
return;
}
struct AxisKeys {
bool negative = false;
@@ -280,6 +296,9 @@ void DefaultKeyboardInput(int key_code, bool down) {
switch (NormalizeKey(static_cast<SDL_Keycode>(key_code))) {
case SDLK_BACKSPACE: SetTouchPad(0.25f, down); return;
case SDLK_TAB: SetTouchPad(0.75f, down); return;
case SDLK_1: CycleSettingOnPress(Controller::Setting::SpeakerVolume, down); return;
case SDLK_2: CycleSettingOnPress(Controller::Setting::VibrationIntensity, down); return;
case SDLK_3: CycleSettingOnPress(Controller::Setting::TriggerEffectIntensity, down); return;
case SDLK_A:
left.left = down;
SetStickAxis(Controller::Axis::LeftX, left.left, left.right);
@@ -460,7 +460,15 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
provoking_vertex.pNext = supported_features2.pNext;
supported_features2.pNext = &provoking_vertex;
}
const bool image_atomic_int64_extension =
HasExtension(device_extensions, VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME);
vk::PhysicalDeviceShaderImageAtomicInt64FeaturesEXT image_atomic_int64 {};
if (image_atomic_int64_extension) {
image_atomic_int64.pNext = supported_features2.pNext;
supported_features2.pNext = &image_atomic_int64;
}
physical_device.getFeatures2(&supported_features2);
graphics.shader_image_int64_atomics_enabled = image_atomic_int64.shaderImageInt64Atomics;
auto features12 = WindowContext::RequiredVulkan12Features();
features12.shaderSharedInt64Atomics = supported_features12.shaderSharedInt64Atomics;
@@ -605,6 +613,11 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
provoking_vertex.transformFeedbackPreservesProvokingVertex = VK_FALSE;
create_info.pNext = &provoking_vertex;
}
if (graphics.shader_image_int64_atomics_enabled) {
image_atomic_int64.pNext = const_cast<void*>(create_info.pNext);
image_atomic_int64.sparseImageInt64Atomics = VK_FALSE;
create_info.pNext = &image_atomic_int64;
}
create_info.pQueueCreateInfos = &queue_create_info;
create_info.queueCreateInfoCount = 1;
create_info.enabledExtensionCount = static_cast<uint32_t>(device_extensions.size());
@@ -969,6 +982,7 @@ void WindowContext::CreateVulkan() {
graphic_ctx.memory_budget_ext_enabled = true;
}
for (const auto* extension: {VK_EXT_ROBUSTNESS_2_EXTENSION_NAME,
VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME,
VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME,
VK_EXT_MESH_SHADER_EXTENSION_NAME,
VK_KHR_WORKGROUP_MEMORY_EXPLICIT_LAYOUT_EXTENSION_NAME,
+9 -3
View File
@@ -306,14 +306,20 @@ static void GameEventController([[maybe_unused]] const EventController& f) {
if (f.added) {
auto* pad = SDL_OpenGamepad(f.id);
EXIT_NOT_IMPLEMENTED(pad == nullptr);
if (pad == nullptr) {
LOGF("Controller: ignoring gamepad %d that could not be opened: %s\n", f.id,
SDL_GetError());
return;
}
int id = SDL_GetJoystickID(SDL_GetGamepadJoystick(pad));
Controller::Connect(id);
}
if (f.removed) {
Controller::Disconnect(f.id);
SDL_CloseGamepad(SDL_GetGamepadFromID(f.id));
if (auto* pad = SDL_GetGamepadFromID(f.id); pad != nullptr) {
Controller::Disconnect(f.id);
SDL_CloseGamepad(pad);
}
}
if (f.down || f.up) {
@@ -617,7 +617,6 @@ TranslateResult TranslateProgram(std::span<const uint32_t> code, const CompileOp
}
}
IR::TrackResources(ir, decoded, native_cfg);
IR::EliminateDeadCode(ir.blocks);
TranslateResult result;
result.program = std::move(ir);
if (options.dump_ir) {
@@ -41,7 +41,7 @@ void ValidateNativeProgram(const IR::Program& program) {
Fail(program, "native shader plan has an invalid image class");
}
present[static_cast<size_t>(*kind)] = true;
const auto dynamic = program.info.images[i].mip_mode == IR::ImageMipMode::DynamicStorage;
const auto dynamic = program.info.images[i].mip_mode == IR::ImageMipMode::Dynamic;
const auto count = dynamic ? program.info.images[i].mip_count : 1u;
if (count == 0u || (!dynamic && program.info.images[i].mip_count != 1u)) {
Fail(program, "native shader plan has an invalid image mip descriptor count");
@@ -124,13 +124,21 @@ uint32_t ImageType(EmitterState& state, const IR::ImageResource& image) {
sampled = 2;
if (image.atomic) {
EXIT_IF(image.numeric_class != Prospero::TextureNumericClass::Uint);
format = spv::ImageFormatR32ui;
format = image.atomic64 ? spv::ImageFormatR64ui : spv::ImageFormatR32ui;
if (image.atomic64) {
state.builder.RequireExtension("SPV_EXT_shader_image_int64");
state.builder.RequireCapability(spv::CapabilityInt64);
state.builder.RequireCapability(spv::CapabilityInt64Atomics);
state.builder.RequireCapability(spv::CapabilityInt64ImageEXT);
}
}
} else {
EXIT("invalid image resource class");
}
const auto& info = ImageDimensionInfoFor(image.dimension);
return state.builder.Type(spv::OpTypeImage, ImageScalarType(state, image.numeric_class),
const auto scalar_type = image.atomic64 ? TypeScalarU64(state)
: ImageScalarType(state, image.numeric_class);
return state.builder.Type(spv::OpTypeImage, scalar_type,
info.spirv_dimension, image.depth_compare ? 1u : 0u, info.arrayed,
info.multisampled, sampled, format);
}
@@ -144,20 +152,11 @@ uint32_t ImageViewSizeType(EmitterState& state, ImageDimension dimension) {
}
}
uint32_t LoadSampledImageDescriptor(EmitterState& state, uint32_t resource) {
const auto& image_resource = state.program.info.images.at(resource);
EXIT_IF(image_resource.resource_class != IR::ImageResourceClass::Sampled);
const auto kind = IR::DescriptorBindingForImage(image_resource);
EXIT_IF(!kind.has_value());
const auto array_index = ResourceForDescriptor(state, *kind, resource);
const auto variable = state.image_variables[IR::ImageBindingIndex(*kind)];
const auto pointer_type = state.builder.Type(
spv::OpTypePointer, spv::StorageClassUniformConstant, ImageType(state, image_resource));
const auto pointer =
DescriptorElementPointer(state, pointer_type, variable, array_index, *kind, resource,
"sampled image descriptor array was not emitted");
uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mip) {
const auto pointer = ImageDescriptorPointer(state, resource, mip);
const auto image = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, ImageType(state, image_resource), image, pointer);
state.builder.AddFunction(spv::OpLoad, ImageType(state, state.program.info.images.at(resource)),
image, pointer);
return image;
}
@@ -175,10 +174,10 @@ uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler) {
return sampler_id;
}
uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler) {
uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler_id, uint32_t mip) {
const auto& image_resource = state.program.info.images.at(resource);
const auto image = LoadSampledImageDescriptor(state, resource);
const auto sampler_id = LoadSamplerDescriptor(state, sampler);
EXIT_IF(image_resource.resource_class != IR::ImageResourceClass::Sampled);
const auto image = LoadImageDescriptor(state, resource, mip);
const auto sampled_image = state.builder.AllocateId();
const auto sampled_type =
state.builder.Type(spv::OpTypeSampledImage, ImageType(state, image_resource));
@@ -186,17 +185,17 @@ uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampl
return sampled_image;
}
uint32_t StorageImageDescriptorPointer(EmitterState& state, uint32_t resource) {
uint32_t ImageDescriptorPointer(EmitterState& state, uint32_t resource, uint32_t mip) {
const auto& image = state.program.info.images.at(resource);
EXIT_IF(image.resource_class != IR::ImageResourceClass::Storage);
EXIT_IF(mip >= image.mip_count);
const auto kind = IR::DescriptorBindingForImage(image);
EXIT_IF(!kind.has_value());
const auto array_index = ResourceForDescriptor(state, *kind, resource);
const auto array_index = ResourceForDescriptor(state, *kind, resource) + mip;
const auto pointer_type = state.builder.Type(
spv::OpTypePointer, spv::StorageClassUniformConstant, ImageType(state, image));
const auto variable = state.image_variables[IR::ImageBindingIndex(*kind)];
return DescriptorElementPointer(state, pointer_type, variable, array_index, *kind, resource,
"storage image descriptor array was not emitted");
"image descriptor array was not emitted");
}
void EmitStorageImageWrite(EmitterState& state, uint32_t resource, uint32_t mip_lod, uint32_t coord,
@@ -206,46 +205,15 @@ void EmitStorageImageWrite(EmitterState& state, uint32_t resource, uint32_t mip_
if (!image.atomic) {
state.builder.RequireCapability(spv::CapabilityStorageImageWriteWithoutFormat);
}
const auto kind = IR::DescriptorBindingForImage(image);
EXIT_IF(!kind.has_value());
const auto array_index = ResourceForDescriptor(state, *kind, resource);
const auto image_type = ImageType(state, image);
const auto pointer_type =
state.builder.Type(spv::OpTypePointer, spv::StorageClassUniformConstant, image_type);
const auto variable = state.image_variables[IR::ImageBindingIndex(*kind)];
const auto LoadAt = [&](uint32_t array_index) {
const auto pointer =
DescriptorElementPointer(state, pointer_type, variable, array_index, *kind, resource,
"storage image descriptor array was not emitted");
const auto descriptor = state.builder.AllocateId();
state.builder.AddFunction(spv::OpLoad, image_type, descriptor, pointer);
return descriptor;
const auto EmitWrite = [&](uint32_t mip) {
state.builder.AddFunction(spv::OpImageWrite, LoadImageDescriptor(state, resource, mip),
coord, texel);
};
if (image.mip_mode != IR::ImageMipMode::DynamicStorage) {
state.builder.AddFunction(spv::OpImageWrite, LoadAt(array_index), coord, texel);
if (image.mip_mode != IR::ImageMipMode::Dynamic) {
EmitWrite(0);
return;
}
if (image.mip_count == 0u) {
ExitDescriptorBindingFailure(state, *kind, resource,
"dynamic storage image has no mip descriptors");
}
const auto merge_label = state.builder.AllocateId();
std::vector<uint32_t> labels(image.mip_count);
std::vector<uint32_t> words {spv::OpSwitch, mip_lod, merge_label};
for (uint32_t mip = 0; mip < image.mip_count; mip++) {
labels[mip] = state.builder.AllocateId();
words.push_back(mip);
words.push_back(labels[mip]);
}
state.builder.AddFunction(spv::OpSelectionMerge, merge_label, spv::SelectionControlMaskNone);
state.builder.AddFunction(words);
for (uint32_t mip = 0; mip < image.mip_count; mip++) {
EmitLabel(state, labels[mip]);
state.builder.AddFunction(spv::OpImageWrite, LoadAt(array_index + mip), coord, texel);
state.builder.AddFunction(spv::OpBranch, merge_label);
}
EmitLabel(state, merge_label);
EmitImageMipSwitch(state, mip_lod, image.mip_count, 0, EmitWrite);
}
spv::ExecutionModel ExecutionModelForStage(ShaderType stage) {
@@ -3,9 +3,7 @@
#include "graphics/shader/recompiler/frontend/decode/ImageOps.h"
#include <algorithm>
#include <atomic>
#include <bit>
#include <cstdio>
namespace Libs::Graphics::ShaderRecompiler::Spirv::Emitter {
namespace {
@@ -86,6 +84,46 @@ uint32_t ZeroF32(EmitterState& state) {
return ConstantF32(state, 0);
}
uint32_t GatherMip(ValueEmitContext& ctx, const IR::Inst& inst, const IR::MemoryInfo& mem,
const IR::Inst& address, const ImageSampleLayout& layout, uint32_t mip_count) {
auto& state = ctx.state;
const auto Extract = [&](uint32_t word, uint32_t first, uint32_t count) {
return EmitBitFieldUExtract(state, word, ConstantU32(state, first),
ConstantU32(state, count));
};
const auto LodFixed = [&](uint32_t word, uint32_t first) {
return Binary(state, spv::OpFMul, TypeF32(state),
Unary(state, spv::OpConvertUToF, TypeF32(state), Extract(word, first, 12)),
ConstantF32Value(state, 1.0f / 256.0f));
};
const auto sampler_control = ctx.Arg(inst, 6);
const auto preclamp = Binary(state, spv::OpINotEqual, TypeBool(state),
Extract(sampler_control, 28, 1), ConstantU32(state, 0));
const auto half = ConstantF32Value(state, 0.5f);
const auto zero = ZeroF32(state);
const auto base = Unary(state, spv::OpConvertUToF, TypeF32(state),
Extract(ctx.Arg(inst, 4), 12, 4));
const auto minimum = Binary(state, spv::OpFSub, TypeF32(state),
LodFixed(ctx.Arg(inst, 3), 8), base);
const auto last = ConstantF32Value(state, static_cast<float>(mip_count - 1u));
auto lod = Binary(state, spv::OpFAdd, TypeF32(state),
AddressF32(ctx, mem, address, layout.lod),
Select(state, TypeF32(state), preclamp, half, zero));
lod = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(
state, lod, LodFixed(ctx.Arg(inst, 5), 0), LodFixed(ctx.Arg(inst, 5), 12));
// T# minimum LOD is in physical mip space and applies after the S# view-relative clamp.
lod = EmitGlsl<GLSLstd450FMax, IR::Type::F32>(state, lod, minimum);
lod = EmitGlsl<GLSLstd450FClamp, IR::Type::F32>(state, lod, zero, last);
lod = Binary(state, spv::OpFAdd, TypeF32(state), lod,
Select(state, TypeF32(state), preclamp, zero, half));
// The clamped value is nonnegative, so conversion performs the integer mip rounding.
const auto mip = EmitUMin32(state, Unary(state, spv::OpConvertFToU, TypeU32(state), lod),
ConstantU32(state, mip_count - 1u));
const auto mip_none = Binary(state, spv::OpIEqual, TypeBool(state),
Extract(sampler_control, 26, 2), ConstantU32(state, 0));
return Select(state, TypeU32(state), mip_none, ConstantU32(state, 0), mip);
}
uint32_t CubeAxis(EmitterState& state, uint32_t value) {
return Binary(state, spv::OpFSub, TypeF32(state), value, ConstantF32(state, 0x3f800000u));
}
@@ -252,7 +290,7 @@ uint32_t QueryDimensions(ValueEmitContext& ctx, const IR::MemoryInfo& mem,
const IR::Inst& address) {
const auto dimension = ctx.state.program.info.images.at(mem.resource).dimension;
const auto& info = ImageDimensionInfoFor(dimension);
const auto image = LoadSampledImageDescriptor(ctx.state, mem.resource);
const auto image = LoadImageDescriptor(ctx.state, mem.resource);
const auto size = ctx.state.builder.AllocateId();
if (info.multisampled != 0u) {
ctx.state.builder.AddFunction(spv::OpImageQuerySize,
@@ -433,7 +471,7 @@ uint32_t EmitOneDimensionalGatherLz(ValueEmitContext& ctx, const IR::MemoryInfo&
uint32_t coord, Prospero::TextureNumericClass numeric_class) {
auto& state = ctx.state;
state.builder.RequireCapability(spv::CapabilityImageQuery);
const auto image = LoadSampledImageDescriptor(state, mem.resource);
const auto image = LoadImageDescriptor(state, mem.resource);
const auto width = state.builder.AllocateId();
state.builder.AddFunction(spv::OpImageQuerySizeLod, TypeU32(state), width, image,
ConstantU32(state, 0));
@@ -446,7 +484,8 @@ uint32_t EmitOneDimensionalGatherLz(ValueEmitContext& ctx, const IR::MemoryInfo&
Binary(state, spv::OpFMul, TypeF32(state), coord, width_f32),
ConstantF32(state, 0x3f000000u)));
const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler);
const auto sampled = MakeSampledImage(state, mem.resource,
LoadSamplerDescriptor(state, mem.sampler));
const auto vector_type = ImageVectorType(state, numeric_class, 4);
const auto scalar_type = ImageScalarType(state, numeric_class);
const auto component =
@@ -558,7 +597,8 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
if (op == IR::ValueOpcode::ImageQueryLod) {
state.builder.RequireCapability(spv::CapabilityImageQuery);
const auto dimension = image.dimension;
const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler);
const auto sampled = MakeSampledImage(state, mem.resource,
LoadSamplerDescriptor(state, mem.sampler));
const auto lod = state.builder.AllocateId();
state.builder.AddFunction(
spv::OpImageQueryLod, TypeF32Vector(state, 2), lod, sampled,
@@ -588,7 +628,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
EmitValueOrDefaultIfCondition(
state, condition, TypeU32Vector(state, 4), ConstantU32CompositeZero(state, 4),
[&]() {
const auto descriptor = LoadSampledImageDescriptor(state, mem.resource);
const auto descriptor = LoadImageDescriptor(state, mem.resource);
const auto color = state.builder.AllocateId();
const auto coord = CoordU32(ctx, mem, *address, dimension);
if (dimension_info.multisampled != 0u) {
@@ -612,7 +652,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto dimension = image.dimension;
EmitIfCondition(state, ctx.Arg(inst, 3), [&]() {
const auto mip_lod =
state.program.info.images[mem.resource].mip_mode == IR::ImageMipMode::DynamicStorage
state.program.info.images[mem.resource].mip_mode == IR::ImageMipMode::Dynamic
? LodU32(ctx, mem, *address, dimension)
: 0u;
const auto coord = CoordU32(ctx, mem, *address, dimension);
@@ -635,13 +675,6 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
if (op == IR::ValueOpcode::ImageGatherRaw) {
const auto coord = CoordF32(ctx, mem, *address, layout.coord,
dimension_info.coordinate_components, image.cube);
if (HasFlag(mem, Decoder::ImageSampleFlagLod)) {
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
if (!warned.test_and_set(std::memory_order_relaxed)) {
std::fputs("Warning: approximating IMAGE_GATHER4_L at mip level 0; explicit LOD is ignored.\n",
stderr);
}
}
if (dimension == ImageDimension::Dim1D) {
if (dref || !HasFlag(mem, Decoder::ImageSampleFlagLevelZero) ||
HasFlag(mem, Decoder::ImageSampleFlagOffset) ||
@@ -658,44 +691,48 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
ctx.Fail(inst, "has an unsupported 1D-array gather");
return;
}
const auto sampled = MakeSampledImage(state, mem.resource, mem.sampler);
const auto sample = state.builder.AllocateId();
std::vector<uint32_t> words;
const auto result_numeric_class = dref ? Prospero::TextureNumericClass::Float
: numeric_class;
const auto result_type = ImageVectorType(state, result_numeric_class, 4);
uint32_t component_or_dref;
if (dref) {
auto dref_value = ZeroF32(state);
if (layout.dref != NoImageComponent) {
dref_value = AddressF32(ctx, mem, *address, layout.dref);
}
words = {spv::OpImageDrefGather,
TypeF32Vector(state, 4),
sample,
sampled,
coord,
dref_value};
component_or_dref = layout.dref != NoImageComponent
? AddressF32(ctx, mem, *address, layout.dref)
: ZeroF32(state);
} else {
uint32_t component = 0;
if (ImageConversionFormat(state, mem).format == Prospero::BufferFormat::kInvalid) {
component = ImageGatherComponent(mem.dmask);
}
words = {spv::OpImageGather,
ImageVectorType(state, numeric_class, 4),
sample,
sampled,
coord,
ConstantU32(state, component)};
component_or_dref = ConstantU32(
state, ImageConversionFormat(state, mem).format == Prospero::BufferFormat::kInvalid
? ImageGatherComponent(mem.dmask) : 0u);
}
uint32_t operand_mask = 0;
uint32_t offset = 0;
if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
words.push_back(spv::ImageOperandsConstOffsetsMask);
words.push_back(HorizontalOffsets(state, dimension));
operand_mask = spv::ImageOperandsConstOffsetsMask;
offset = HorizontalOffsets(state, dimension);
} else if (layout.offset != NoImageComponent) {
words.push_back(spv::ImageOperandsOffsetMask);
words.push_back(PackedOffset(ctx, mem, *address, layout, dimension));
}
state.builder.AddFunction(words);
auto result_numeric_class = numeric_class;
if (dref) {
result_numeric_class = Prospero::TextureNumericClass::Float;
operand_mask = spv::ImageOperandsOffsetMask;
offset = PackedOffset(ctx, mem, *address, layout, dimension);
}
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler);
const auto EmitGather = [&](uint32_t mip) {
const auto sampled = MakeSampledImage(state, mem.resource, sampler_id, mip);
const auto sample = state.builder.AllocateId();
std::vector<uint32_t> words {
static_cast<uint32_t>(dref ? spv::OpImageDrefGather : spv::OpImageGather), result_type,
sample, sampled, coord, component_or_dref};
if (operand_mask != 0u) {
words.push_back(operand_mask);
words.push_back(offset);
}
state.builder.AddFunction(words);
return sample;
};
const auto sample = image.mip_mode == IR::ImageMipMode::Dynamic && image.mip_count > 1u
? EmitImageMipSwitch(
state, GatherMip(ctx, inst, mem, *address, layout,
image.mip_count),
image.mip_count, result_type, EmitGather)
: EmitGather(0);
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample),
result_numeric_class, false, mem, true));
return;
@@ -738,13 +775,14 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
operand_mask |= spv::ImageOperandsBiasMask;
operands.push_back(AddressF32(ctx, mem, *address, layout.bias));
}
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler);
const auto EmitSample = [&](uint32_t resource) {
const auto& candidate = state.program.info.images[resource];
const auto coord =
CoordF32(ctx, mem, *address, layout.coord,
ImageDimensionInfoFor(candidate.dimension).coordinate_components,
candidate.cube);
const auto sampled = MakeSampledImage(state, resource, mem.sampler);
const auto sampled = MakeSampledImage(state, resource, sampler_id);
const auto sample = state.builder.AllocateId();
std::vector<uint32_t> sample_operands {result_type, sample, sampled, coord};
if (dref) {
@@ -865,12 +903,16 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
}
if (image_info.access == IR::ImageAccess::Atomic) {
const auto dimension = image.dimension;
ctx.Define(inst, EmitValueOrZeroIfCondition(state, ctx.Arg(inst, inst.NumArgs() - 1), [&]() {
const auto result_type = TypeId(state, inst.GetType());
const auto zero = image.atomic64 ? ConstantU64(state, 0) : ConstantU32(state, 0);
ctx.Define(inst, EmitValueOrDefaultIfCondition(
state, ctx.Arg(inst, inst.NumArgs() - 1), result_type, zero, [&]() {
const auto pointer = state.builder.AllocateId();
const auto pointer_type = state.builder.Type(
spv::OpTypePointer, spv::StorageClassImage, TypeU32(state));
spv::OpTypePointer, spv::StorageClassImage,
image.atomic64 ? TypeScalarU64(state) : TypeU32(state));
state.builder.AddFunction(spv::OpImageTexelPointer, pointer_type, pointer,
StorageImageDescriptorPointer(state, mem.resource),
ImageDescriptorPointer(state, mem.resource),
CoordU32(ctx, mem, *address, dimension),
ConstantU32(state, 0));
if (op == IR::ValueOpcode::ImageAtomicFMin32 ||
@@ -317,6 +317,7 @@ inline constexpr auto EmitImageAtomicSMin32 = EmitImage;
inline constexpr auto EmitImageAtomicUMin32 = EmitImage;
inline constexpr auto EmitImageAtomicSMax32 = EmitImage;
inline constexpr auto EmitImageAtomicUMax32 = EmitImage;
inline constexpr auto EmitImageAtomicUMax64 = EmitImage;
inline constexpr auto EmitImageAtomicAnd32 = EmitImage;
inline constexpr auto EmitImageAtomicOr32 = EmitImage;
inline constexpr auto EmitImageAtomicXor32 = EmitImage;
@@ -19,6 +19,7 @@
#include <map>
#include <spirv/unified1/GLSL.std.450.h>
#include <string>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>
@@ -314,13 +315,14 @@ uint32_t ImageType(EmitterState& state, const IR::ImageResource& image);
uint32_t ImageViewSizeType(EmitterState& state, ImageDimension dimension);
uint32_t LoadSampledImageDescriptor(EmitterState& state, uint32_t resource);
uint32_t ImageDescriptorPointer(EmitterState& state, uint32_t resource, uint32_t mip = 0);
uint32_t LoadImageDescriptor(EmitterState& state, uint32_t resource, uint32_t mip = 0);
uint32_t LoadSamplerDescriptor(EmitterState& state, uint32_t sampler);
uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler);
uint32_t StorageImageDescriptorPointer(EmitterState& state, uint32_t resource);
uint32_t MakeSampledImage(EmitterState& state, uint32_t resource, uint32_t sampler_id,
uint32_t mip = 0);
void EmitStorageImageWrite(EmitterState& state, uint32_t resource, uint32_t mip_lod, uint32_t coord,
uint32_t texel);
@@ -514,6 +516,45 @@ uint32_t ConstantDeviceAddress(EmitterState& state, uint64_t value);
uint32_t DeviceAddressFromWords(EmitterState& state, uint32_t low, uint32_t high);
// These templates accept local lambdas from several emitter translation units.
template <typename Fn>
auto EmitImageMipSwitch(EmitterState& state, uint32_t mip_lod, uint32_t mip_count,
uint32_t result_type, Fn&& emit) {
constexpr bool has_result = !std::is_void_v<std::invoke_result_t<Fn, uint32_t>>;
EXIT_IF(mip_count == 0u);
const auto merge_label = state.builder.AllocateId();
std::vector<uint32_t> labels(mip_count);
std::vector<uint32_t> words {spv::OpSwitch, mip_lod, merge_label};
for (uint32_t mip = 0; mip < mip_count; mip++) {
labels[mip] = state.builder.AllocateId();
words.push_back(mip);
words.push_back(labels[mip]);
}
if constexpr (has_result) {
words[2] = labels.front();
}
state.builder.AddFunction(spv::OpSelectionMerge, merge_label, spv::SelectionControlMaskNone);
state.builder.AddFunction(words);
std::vector<uint32_t> phi;
if constexpr (has_result) {
phi = {spv::OpPhi, result_type, state.builder.AllocateId()};
}
for (uint32_t mip = 0; mip < mip_count; mip++) {
EmitLabel(state, labels[mip]);
if constexpr (has_result) {
phi.push_back(emit(mip));
phi.push_back(state.current_label);
} else {
emit(mip);
}
state.builder.AddFunction(spv::OpBranch, merge_label);
}
EmitLabel(state, merge_label);
if constexpr (has_result) {
state.builder.AddFunction(phi);
return phi[2];
}
}
template <typename Fn>
void EmitIfCondition(EmitterState& state, uint32_t condition, Fn&& fn) {
const auto then_label = state.builder.AllocateId();
@@ -1044,17 +1044,11 @@ uint32_t EmitBufferAtomic64(ValueEmitContext& ctx, const IR::Inst& inst) {
return EmitValueOrDefaultIfCondition(
state, EmitMemoryElementInBounds(state, resource, index), TypeU64(state),
ConstantU64(state, 0), [&]() {
const auto value = Unary(state, spv::OpBitcast, TypeScalarU64(state),
ctx.Arg(inst, inst.NumArgs() - 2));
const auto old = state.builder.AllocateId();
state.builder.AddFunction(
SpirvAtomicOpcode(inst.GetOpcode()), TypeScalarU64(state), old,
EmitStorageBufferElementPointer(state, resource, index,
TypeStorageBufferU64ElementPointer(state)),
ConstantU32(state, spv::ScopeDevice),
ConstantU32(state, spv::MemorySemanticsMaskNone), value);
const auto pointer = EmitStorageBufferElementPointer(
state, resource, index, TypeStorageBufferU64ElementPointer(state));
const auto old = EmitAtomicOperation(ctx, inst, pointer, spv::ScopeDevice);
EmitAtomicMemoryBarrier(state, mem.kind);
return Unary(state, spv::OpBitcast, TypeU64(state), old);
return old;
});
});
}
@@ -330,6 +330,7 @@ spv::Op SpirvAtomicOpcode(IR::ValueOpcode opcode) {
case IR::ValueOpcode::BufferAtomicSMax32:
case IR::ValueOpcode::SharedAtomicSMax32: return spv::OpAtomicSMax;
case IR::ValueOpcode::ImageAtomicUMax32:
case IR::ValueOpcode::ImageAtomicUMax64:
case IR::ValueOpcode::BufferAtomicUMax32:
case IR::ValueOpcode::SharedAtomicUMax32: return spv::OpAtomicUMax;
case IR::ValueOpcode::ImageAtomicAnd32:
@@ -352,6 +353,8 @@ uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32
uint32_t scope) {
const auto opcode = SpirvAtomicOpcode(inst.GetOpcode());
const auto old = ctx.state.builder.AllocateId();
const bool wide = inst.GetType() == IR::Type::U64;
const auto type = wide ? TypeScalarU64(ctx.state) : TypeU32(ctx.state);
if (opcode == spv::OpAtomicCompareExchange) {
const auto desired = ctx.Arg(inst, inst.NumArgs() - 3);
const auto comparator = ctx.Arg(inst, inst.NumArgs() - 2);
@@ -360,12 +363,13 @@ uint32_t EmitAtomicOperation(ValueEmitContext& ctx, const IR::Inst& inst, uint32
ConstantU32(ctx.state, scope), ConstantU32(ctx.state, spv::MemorySemanticsMaskNone),
ConstantU32(ctx.state, spv::MemorySemanticsMaskNone), desired, comparator);
} else {
const auto value = ctx.Arg(inst, inst.NumArgs() - 2);
ctx.state.builder.AddFunction(opcode, TypeU32(ctx.state), old, pointer,
auto value = ctx.Arg(inst, inst.NumArgs() - 2);
if (wide) value = Unary(ctx.state, spv::OpBitcast, type, value);
ctx.state.builder.AddFunction(opcode, type, old, pointer,
ConstantU32(ctx.state, scope),
ConstantU32(ctx.state, spv::MemorySemanticsMaskNone), value);
}
return old;
return wide ? Unary(ctx.state, spv::OpBitcast, TypeU64(ctx.state), old) : old;
}
void EmitAtomicMemoryBarrier(EmitterState& state, IR::ResourceKind kind) {
@@ -360,9 +360,19 @@ void DecodeMimg(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
SetUnsupported(inst, Family::MIMG, opcode,
"MIMG image gather requires exactly one dmask bit");
}
if (inst.opcode == Opcode::IMAGE_ATOMIC_CMPSWAP && inst.dmask != 0x3u) {
SetUnsupported(inst, Family::MIMG, opcode,
"MIMG image compare-and-swap requires 32-bit DMASK 0x3");
if (atomic != nullptr) {
const bool compare_swap = inst.opcode == Opcode::IMAGE_ATOMIC_CMPSWAP;
const auto mask32 = compare_swap ? 0x3u : 0x1u;
const auto mask64 = compare_swap ? 0xfu : 0x3u;
if (inst.dmask != mask32 && inst.dmask != mask64) {
SetUnsupported(inst, Family::MIMG, opcode, "MIMG image atomic has invalid DMASK");
} else if (inst.dmask == mask64) {
inst.data_bits = 64u;
if (inst.opcode != Opcode::IMAGE_ATOMIC_UMAX) {
SetUnsupported(inst, Family::MIMG, opcode,
"MIMG 64-bit image atomic opcode is not implemented");
}
}
}
const bool supports_d16 = sample != nullptr || gather != nullptr || opcode == 0x00u ||
opcode == 0x01u || opcode == 0x08u || opcode == 0x09u;
@@ -568,7 +568,9 @@ void Translator::IMAGE_ATOMIC(const Decoder::Instruction& inst, IR::ValueOpcode
ir.GetExec()},
flags);
} else {
result = ir.Emit(opcode, {resource, address, ReadU32(data), ir.GetExec()}, flags);
const IR::Value value =
memory.data_bits == 64u ? IR::Value(ReadU64(data)) : IR::Value(ReadU32(data));
result = ir.Emit(opcode, {resource, address, value, ir.GetExec()}, flags);
}
if (inst.glc) {
WriteOperand(inst.dst, result);
@@ -706,8 +708,16 @@ void Translator::IMAGE_GATHER(const Decoder::Instruction& inst) {
const auto resource = GetImageResource(memory);
const auto sampler = GetSamplerResource(memory);
const auto address = MakeImageAddress(inst, MemorySourceAt(inst, 0));
const auto result = ir.Emit(IR::ValueOpcode::ImageGatherRaw, {resource, sampler, address},
AddMemoryInfo(memory, inst.pc));
const bool has_lod = (memory.image_sample_flags & Decoder::ImageSampleFlagLod) != 0u;
// LOD selection consumes these words on the GPU after descriptor handles are stripped.
const auto result = ir.Emit(
IR::ValueOpcode::ImageGatherRaw,
{resource, sampler, address,
has_lod ? resource.Instruction()->Arg(1) : IR::Value(0u),
has_lod ? resource.Instruction()->Arg(3) : IR::Value(0u),
has_lod ? sampler.Instruction()->Arg(1) : IR::Value(0u),
has_lod ? sampler.Instruction()->Arg(2) : IR::Value(0u)},
AddMemoryInfo(memory, inst.pc));
for (uint32_t index = 0; index < memory.data_dwords; index++) {
WriteOperand(OffsetOperand(inst.dst, index),
ir.Emit(IR::ValueOpcode::CompositeExtractU32x4, {result, IR::Value(index)}));
@@ -1052,7 +1062,8 @@ void Translator::EmitMemory(const Decoder::Instruction& inst) {
case Decoder::Opcode::IMAGE_ATOMIC_SMAX:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicSMax32);
case Decoder::Opcode::IMAGE_ATOMIC_UMAX:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicUMax32);
return IMAGE_ATOMIC(inst, inst.data_bits == 64u ? IR::ValueOpcode::ImageAtomicUMax64
: IR::ValueOpcode::ImageAtomicUMax32);
case Decoder::Opcode::IMAGE_ATOMIC_AND:
return IMAGE_ATOMIC(inst, IR::ValueOpcode::ImageAtomicAnd32);
case Decoder::Opcode::IMAGE_ATOMIC_OR:
+10 -5
View File
@@ -114,7 +114,7 @@ struct BufferResource {
bool operator==(const BufferResource& other) const = default;
};
enum class ImageMipMode { None, DynamicStorage };
enum class ImageMipMode { None, Dynamic };
constexpr uint32_t ShaderImageIdentitySwizzle = 0x00000facu;
@@ -133,6 +133,7 @@ struct ImageResource {
bool read = false;
bool written = false;
bool atomic = false;
bool atomic64 = false;
bool depth_compare = false;
bool cube = false;
bool r128 = false;
@@ -147,9 +148,12 @@ struct ImageResource {
struct SamplerResource {
uint32_t source = 0;
uint32_t first_use_pc = 0;
// Native filtering/border variants share the original sampler's runtime descriptor.
uint32_t snapshot_index = 0;
bool force_point_filtering = false;
bool depth_compare = false;
bool integer_border = false;
bool gather_lod = false;
bool operator==(const SamplerResource& other) const = default;
};
@@ -278,7 +282,7 @@ struct StageOutput {
inline constexpr uint32_t FirstImageBinding = 1u;
inline constexpr uint32_t FirstComparisonImageBinding = 22u;
inline constexpr uint32_t FirstStorageImageBinding = 29u;
inline constexpr uint32_t ImageBindingCount = 43u;
inline constexpr uint32_t ImageBindingCount = 48u;
enum class DescriptorBindingKind : uint32_t {
Buffers = 0u,
@@ -291,8 +295,8 @@ enum class DescriptorBindingKind : uint32_t {
Count,
};
static_assert(static_cast<uint32_t>(DescriptorBindingKind::Samplers) == 44u);
static_assert(static_cast<uint32_t>(DescriptorBindingKind::Count) == 50u);
static_assert(static_cast<uint32_t>(DescriptorBindingKind::Samplers) == 49u);
static_assert(static_cast<uint32_t>(DescriptorBindingKind::Count) == 55u);
struct PushData {
static constexpr uint32_t DwordCount = 32;
@@ -369,7 +373,7 @@ DescriptorBindingForImage(const ImageResource& image) {
if (image.numeric_class != Prospero::TextureNumericClass::Uint) {
return std::nullopt;
}
base = AtomicUintBinding;
base = AtomicUintBinding + (image.atomic64 ? 5u : 0u);
} else {
switch (image.numeric_class) {
case Prospero::TextureNumericClass::Float: base = StorageFloatBinding; break;
@@ -476,6 +480,7 @@ struct DescriptorSource {
uint32_t table_offset = 0;
Value key_count;
Value selector_mask;
std::vector<uint32_t> sources;
bool operator==(const IndirectImage& other) const = default;
};
@@ -152,6 +152,8 @@ public:
}
private:
friend void EliminateDeadCode(const std::vector<Block*>& blocks);
void AddUse(Inst* used, size_t operand);
void RemoveUse(Inst* used, size_t operand);
void ClearArgs();
@@ -161,6 +163,7 @@ private:
ValueOpcode opcode;
uint8_t num_args;
bool live = false;
mutable uint32_t evaluation_index = UINT32_MAX;
uint64_t flags;
Block* parent = nullptr;
@@ -227,6 +227,7 @@ ImageOpcodeInfo ImageOpcodeInfoOf(ValueOpcode opcode) {
case ValueOpcode::ImageAtomicUMin32:
case ValueOpcode::ImageAtomicSMax32:
case ValueOpcode::ImageAtomicUMax32:
case ValueOpcode::ImageAtomicUMax64:
case ValueOpcode::ImageAtomicAnd32:
case ValueOpcode::ImageAtomicOr32:
case ValueOpcode::ImageAtomicXor32:
@@ -297,7 +297,7 @@ VALUE_OPCODE(ImageQueryLod, U32x4, ImageResource, SamplerResource, ImageAddress)
VALUE_OPCODE(ImageRead, U32x4, ImageResource, ImageAddress, U1)
VALUE_OPCODE(ImageWrite, Void, ImageResource, ImageAddress, U32x4, U1)
VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress)
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress)
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U32, U32, U32, U32)
VALUE_OPCODE(ImageAtomicCompareSwap32, U32, ImageResource, ImageAddress, U32, U32, U1)
VALUE_OPCODE(ImageAtomicSwap32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicIAdd32, U32, ImageResource, ImageAddress, U32, U1)
@@ -305,6 +305,7 @@ VALUE_OPCODE(ImageAtomicSMin32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicUMin32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicSMax32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicUMax32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicUMax64, U64, ImageResource, ImageAddress, U64, U1)
VALUE_OPCODE(ImageAtomicAnd32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicOr32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicXor32, U32, ImageResource, ImageAddress, U32, U1)
@@ -126,7 +126,7 @@ void AllocateBindings(Program& program, uint32_t push_data_start_dword) {
EXIT("shader binding layout failed: image %u has an unmapped binding class", i);
}
auto& resources = image_groups[group];
const auto dynamic = program.info.images[i].mip_mode == ImageMipMode::DynamicStorage;
const auto dynamic = program.info.images[i].mip_mode == ImageMipMode::Dynamic;
const auto count = dynamic ? program.info.images[i].mip_count : 1u;
if (count == 0u || (!dynamic && program.info.images[i].mip_count != 1u)) {
EXIT("shader binding layout failed: image %u has invalid specialized mip count %u", i,
@@ -1,5 +1,7 @@
#include "graphics/shader/recompiler/ir/passes/DeadCodeElimination.h"
#include <algorithm>
namespace Libs::Graphics::ShaderRecompiler::IR {
void RemoveIdentities(const BlockList& blocks) {
@@ -18,23 +20,32 @@ void RemoveIdentities(const BlockList& blocks) {
}
void EliminateDeadCode(const BlockList& blocks) {
bool changed;
do {
changed = false;
for (auto block = blocks.rbegin(); block != blocks.rend(); block++) {
auto& instructions = (*block)->Instructions();
auto inst = instructions.end();
while (inst != instructions.begin()) {
--inst;
if (inst->HasUses() || inst->MayHaveSideEffects()) {
continue;
}
inst->Invalidate();
inst = instructions.erase(inst);
changed = true;
std::vector<Inst*> pending;
for (auto* block: blocks) {
for (auto& inst: *block) {
// Retained resource-planning instructions live outside the block list.
inst.live = inst.MayHaveSideEffects() || std::ranges::any_of(inst.Uses(),
[](const Use& use) { return use.user->Parent() == nullptr; });
if (inst.live) pending.push_back(&inst);
}
}
while (!pending.empty()) {
const auto* inst = pending.back();
pending.pop_back();
for (size_t arg = 0; arg < inst->NumArgs(); ++arg) {
auto* input = inst->Arg(arg).TryInstruction();
if (input != nullptr && input->Parent() != nullptr && !input->live) {
input->live = true;
pending.push_back(input);
}
}
} while (changed);
}
// Unlink every dead operand before freeing nodes in a dead Phi cycle.
for (auto* block: blocks)
for (auto& inst: *block)
if (!inst.live) inst.Invalidate();
for (auto* block: blocks)
std::erase_if(block->Instructions(), [](const Inst& inst) { return !inst.live; });
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -109,8 +109,7 @@ Prospero::BufferFormat ImageConversionFormat(Prospero::BufferFormat format) {
enum class SamplerClass : uint8_t { Float, Integer, PointInteger };
template <typename Image>
SamplerClass ClassifySampler(const Image& image) {
SamplerClass ClassifySampler(const ImageResource& image) {
if (image.numeric_class == Prospero::TextureNumericClass::Sint ||
image.conversion_format != Prospero::BufferFormat::kInvalid) {
return SamplerClass::PointInteger;
@@ -124,8 +123,8 @@ bool DescriptorIsCube(const DescriptorValue& descriptor) {
Prospero::ImageType::kCube;
}
uint32_t StorageMipCount(const ImageResource& image, const DescriptorValue& descriptor) {
if (image.mip_mode != ImageMipMode::DynamicStorage || NullImageDescriptor(descriptor)) {
uint32_t ImageMipCount(const ImageResource& image, const DescriptorValue& descriptor) {
if (image.mip_mode != ImageMipMode::Dynamic || NullImageDescriptor(descriptor)) {
return 1;
}
const auto base = (descriptor.dwords[3] >> 12u) & 0xfu;
@@ -227,100 +226,114 @@ bool ReadScalarTable(uint64_t base, uint64_t size, uint64_t dynamic_offset,
bool MaterializeIndirectImage(const ResourcePlan& program,
const DescriptorSource::IndirectImage& indirect,
const DescriptorValue& material_value,
const DescriptorValue& table_value, uint32_t image_index,
uint32_t image_index,
const SrtRuntime& runtime, SrtWalker& clean,
ResourceSnapshot& snapshot,
ResourceSpecialization& specialization) {
const auto& sources = indirect.sources;
auto& keys = program.material_keys;
uint64_t table_base = 0;
uint64_t table_size = UINT64_MAX; // Scalar addresses have no buffer descriptor bounds.
ShaderBufferResource table;
if (table_value.dword_count == 2u) {
table_base = (static_cast<uint64_t>(table_value.dwords[1]) << 32u) | table_value.dwords[0];
} else if (DecodeBufferDescriptor(table_value, table)) {
table_base = table.Base48();
table_size = table.GetSize();
} else {
return false;
}
auto& keys = program.material_keys;
keys.clear();
if (indirect.material_source == UINT32_MAX) {
uint32_t key_count = 0;
const bool evaluated = clean.Evaluate(indirect.key_count, key_count);
if (std::bit_cast<int32_t>(key_count) <= 0) key_count = 0;
if (table_value.dword_count != 2u || !evaluated ||
key_count > MaxIndirectImageProbes ||
uint64_t {indirect.table_offset} + uint64_t {key_count} * 32u > UINT32_MAX + 1ull) {
if (sources.empty()) {
keys.clear();
DescriptorValue material_value;
DescriptorValue table_value;
if ((indirect.material_source != UINT32_MAX &&
!clean.EvaluateDescriptor(indirect.material_source, material_value)) ||
!clean.EvaluateDescriptor(indirect.table_source, table_value)) {
return false;
}
keys.resize(key_count);
std::iota(keys.begin(), keys.end(), 0u);
} else if (!indirect.selector_mask.IsEmpty()) {
uint32_t mask = 0;
uint32_t count = 0;
if (material_value.dword_count != 2u || table_value.dword_count != 2u ||
!clean.Evaluate(indirect.selector_mask, mask) ||
!clean.Evaluate(indirect.key_count, count) || count == 0u || count > 32u) {
ShaderBufferResource table;
if (table_value.dword_count == 2u) {
table_base = (static_cast<uint64_t>(table_value.dwords[1]) << 32u) | table_value.dwords[0];
} else if (DecodeBufferDescriptor(table_value, table)) {
table_base = table.Base48();
table_size = table.GetSize();
} else {
return false;
}
if (count < 32u) mask &= (1u << count) - 1u;
const auto material_base =
(static_cast<uint64_t>(material_value.dwords[1]) << 32u) | material_value.dwords[0];
keys.reserve(std::popcount(mask));
while (mask != 0u) {
const auto index = std::countr_zero(mask);
const auto offset = static_cast<uint64_t>(indirect.selector_offset) +
static_cast<uint64_t>(index) * indirect.selector_stride;
if (offset > UINT32_MAX) return false;
uint32_t key = 0;
if (!ReadScalarTable(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
runtime, {&key, 1})) return false;
keys.push_back(key);
mask &= mask - 1u;
}
std::ranges::sort(keys);
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
} else {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u ||
material.Stride() != indirect.selector_stride) {
return false;
}
// The first aligned offset includes the immediate added after shader U32 arithmetic.
const auto step = std::max<uint64_t>(4u,
std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u));
const uint64_t first = indirect.selector_offset;
const auto size = material.GetSize();
const auto limit = std::min(first + (uint64_t {1} << 32u) - step, size >= 4u ? size - 4u : 0u);
const auto probe_count = size >= 4u && first <= limit ? (limit - first) / step + 1u : 0u;
if (probe_count > MaxIndirectImageProbes) {
return false;
}
keys.reserve(static_cast<size_t>(probe_count) + 1u);
keys.push_back(0u);
for (uint64_t probe = 0, offset = first; probe < probe_count; ++probe, offset += step) {
uint32_t key = 0;
if (!ReadScalarTable(material.Base48(), size, offset, runtime, {&key, 1})) {
if (indirect.material_source == UINT32_MAX) {
uint32_t key_count = 0;
const bool evaluated = clean.Evaluate(indirect.key_count, key_count);
if (std::bit_cast<int32_t>(key_count) <= 0) key_count = 0;
if (table_value.dword_count != 2u || !evaluated ||
key_count > MaxIndirectImageProbes ||
uint64_t {indirect.table_offset} + uint64_t {key_count} * 32u > UINT32_MAX + 1ull) {
return false;
}
keys.push_back(key);
keys.resize(key_count);
std::iota(keys.begin(), keys.end(), 0u);
} else if (!indirect.selector_mask.IsEmpty()) {
uint32_t mask = 0;
uint32_t count = 0;
if (material_value.dword_count != 2u || table_value.dword_count != 2u ||
!clean.Evaluate(indirect.selector_mask, mask) ||
!clean.Evaluate(indirect.key_count, count) || count == 0u || count > 32u) {
return false;
}
if (count < 32u) mask &= (1u << count) - 1u;
const auto material_base =
(static_cast<uint64_t>(material_value.dwords[1]) << 32u) | material_value.dwords[0];
keys.reserve(std::popcount(mask));
while (mask != 0u) {
const auto index = std::countr_zero(mask);
const auto offset = static_cast<uint64_t>(indirect.selector_offset) +
static_cast<uint64_t>(index) * indirect.selector_stride;
if (offset > UINT32_MAX) return false;
uint32_t key = 0;
if (!ReadScalarTable(material_base, UINT64_MAX, static_cast<uint32_t>(offset),
runtime, {&key, 1})) return false;
keys.push_back(key);
mask &= mask - 1u;
}
std::ranges::sort(keys);
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
} else {
ShaderBufferResource material;
if (!DecodeBufferDescriptor(material_value, material) || table_value.dword_count != 4u ||
material.Stride() != indirect.selector_stride) {
return false;
}
// The first aligned offset includes the immediate added after shader U32 arithmetic.
const auto step = std::max<uint64_t>(4u,
std::gcd<uint64_t>(indirect.selector_stride, uint64_t {1} << 32u));
const uint64_t first = indirect.selector_offset;
const auto size = material.GetSize();
const auto limit = std::min(first + (uint64_t {1} << 32u) - step, size >= 4u ? size - 4u : 0u);
const auto probe_count = size >= 4u && first <= limit ? (limit - first) / step + 1u : 0u;
if (probe_count > MaxIndirectImageProbes) {
return false;
}
keys.reserve(static_cast<size_t>(probe_count) + 1u);
keys.push_back(0u);
for (uint64_t probe = 0, offset = first; probe < probe_count; ++probe, offset += step) {
uint32_t key = 0;
if (!ReadScalarTable(material.Base48(), size, offset, runtime, {&key, 1})) {
return false;
}
keys.push_back(key);
}
std::ranges::sort(keys);
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
}
std::ranges::sort(keys);
keys.erase(std::unique(keys.begin(), keys.end()), keys.end());
}
const auto children_begin = snapshot.images.size();
const auto mapping_offset = snapshot.flattened_srt.size();
const auto root_image = specialization.images[image_index];
snapshot.flattened_srt.resize(mapping_offset + 1u + keys.size() * 2u);
snapshot.flattened_srt[mapping_offset] = static_cast<uint32_t>(keys.size());
for (uint32_t entry = 0; entry < keys.size(); ++entry) {
const auto key = keys[entry];
const auto key_count = sources.empty() ? keys.size() : sources.size();
snapshot.flattened_srt.resize(mapping_offset + 1u + key_count * 2u);
snapshot.flattened_srt[mapping_offset] = static_cast<uint32_t>(key_count);
for (uint32_t entry = 0; entry < key_count; ++entry) {
const auto key = sources.empty() ? keys[entry] : entry;
DescriptorValue candidate;
candidate.dword_count = 8u;
const auto table_offset = (key << 5u) + indirect.table_offset;
if (!ReadScalarTable(table_base, table_size, table_offset, runtime, candidate.dwords)) {
if (sources.empty()) {
const auto table_offset = (key << 5u) + indirect.table_offset;
if (!ReadScalarTable(table_base, table_size, table_offset, runtime, candidate.dwords)) {
return false;
}
} else if (!clean.EvaluateDescriptor(sources[entry], candidate)) {
return false;
}
if (NullImageDescriptor(candidate) ||
@@ -353,7 +366,7 @@ bool MaterializeIndirectImage(const ResourcePlan& program,
auto& root = specialization.images[image_index];
root.indirect_root = image_index;
root.indirect_mapping_offset = static_cast<uint32_t>(mapping_offset);
root.indirect_search_iterations = std::bit_width(keys.size());
root.indirect_search_iterations = std::bit_width(key_count);
}
return true;
}
@@ -404,41 +417,8 @@ private:
uint32_t count = 0;
};
template <typename Images>
bool BuildSamplerPlan(const ShaderInfo& base, const Images& images, SamplerPlan& plan);
static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSnapshot& snapshot,
ResourceSpecialization& specialization) {
specialization.buffers.clear();
specialization.buffers.reserve(program.info.buffers.size());
for (uint32_t i = 0; i < program.info.buffers.size(); i++) {
auto& descriptor_value = snapshot.buffers[i];
ShaderBufferResource descriptor;
if (!DecodeBufferDescriptor(descriptor_value, descriptor)) {
return SpecializationFail(fmt::format("buffer descriptor {} has invalid width", i));
}
if (descriptor.Type() != 0) {
descriptor_value.dwords.fill(0);
descriptor = {};
}
auto packed_stride = descriptor.PackedStride();
const auto stride = packed_stride & 0x3fffu;
const bool swizzle = stride != 0u && ((packed_stride >> 14u) & 1u) != 0u;
if (stride == 0u) {
packed_stride &= ~((1u << 14u) | (3u << 16u));
} else if (!swizzle) {
packed_stride &= ~(3u << 16u);
}
specialization.buffers.push_back({
.packed_stride = packed_stride,
.descriptor_format = program.info.buffers[i].formatted
? descriptor.Format()
: 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++) {
const auto& descriptor = snapshot.images[i];
auto& image = specialization.images[i];
@@ -451,10 +431,10 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
(base.atomic && base.resource_class != ImageResourceClass::Storage)) {
return SpecializationFail(fmt::format("image resource {} has an invalid class", i));
}
image.mip_count = StorageMipCount(base, descriptor);
image.mip_count = ImageMipCount(base, descriptor);
if (image.mip_count == 0u) {
return SpecializationFail(
fmt::format("storage image descriptor {} has an invalid mip range", i));
fmt::format("image descriptor {} has an invalid mip range", i));
}
if (NullImageDescriptor(descriptor)) {
image.numeric_class = base.atomic ? Prospero::TextureNumericClass::Uint
@@ -476,8 +456,10 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
image.cube = DescriptorIsCube(descriptor);
const auto format =
static_cast<Prospero::BufferFormat>((descriptor.dwords[1] >> 20u) & 0x1ffu);
if (base.atomic && format != Prospero::BufferFormat::k32UInt &&
format != Prospero::BufferFormat::k32Float) {
if (base.atomic &&
(base.atomic64 ? format != Prospero::BufferFormat::k32_32UInt
: format != Prospero::BufferFormat::k32UInt &&
format != Prospero::BufferFormat::k32Float)) {
return SpecializationFail(
fmt::format("atomic image descriptor {} uses unsupported format {}", i,
static_cast<uint32_t>(format)));
@@ -577,30 +559,21 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna
}
}
}
SamplerPlan sampler_plan;
if (!BuildSamplerPlan(program.info, specialization.images, sampler_plan)) {
return SpecializationFail("specialized sampler layout exceeds its resource limit");
}
for (uint32_t index = static_cast<uint32_t>(program.info.samplers.size());
index < sampler_plan.sampler_count; index++) {
snapshot.samplers.push_back(snapshot.samplers[sampler_plan.bindings[index].source]);
}
ImageRemap(specialization).Apply(snapshot.images);
return true;
}
template <typename Images>
bool BuildSamplerPlan(const ShaderInfo& base, const Images& images, SamplerPlan& plan) {
bool BuildSamplerPlan(const ShaderInfo& base, SamplerPlan& plan) {
if (base.samplers.size() > plan.mapping.size()) {
return false;
}
std::array<uint8_t, ShaderInfo::MaxSamplers> usage {};
plan.sampler_count = static_cast<uint32_t>(base.samplers.size());
for (const auto& pair: base.sampled_pairs) {
if (pair.image >= images.size() || pair.sampler >= base.samplers.size()) {
if (pair.image >= base.images.size() || pair.sampler >= base.samplers.size()) {
return false;
}
usage[pair.sampler] |= 1u << static_cast<uint32_t>(ClassifySampler(images[pair.image]));
usage[pair.sampler] |= 1u << static_cast<uint32_t>(ClassifySampler(base.images[pair.image]));
}
for (uint32_t index = 0; index < base.samplers.size(); index++) {
auto& mapping = plan.mapping[index];
@@ -936,20 +909,27 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
plan.uniform_fill.values[i] = Clone(plan.uniform_fill.values[i]);
}
plan.clean_flat_slots.resize(plan.srt_reads.size());
bool masked_image = false;
bool capture_image_reads = false;
for (const auto& image: plan.info.images) {
const auto* source = Source(plan, image.source);
if (source == nullptr || !source->indirect_image.has_value()) {
continue;
}
plan.requires_specialization_memory = true;
masked_image |= !source->indirect_image->selector_mask.IsEmpty();
capture_image_reads |= !source->indirect_image->selector_mask.IsEmpty() ||
!source->indirect_image->sources.empty();
MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->material_source),
plan.clean_flat_slots, source->indirect_image->selector_mask);
MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->table_source),
plan.clean_flat_slots);
if (source->indirect_image->sources.empty()) {
MarkCleanFlatSlots(plan, Source(plan, source->indirect_image->table_source),
plan.clean_flat_slots);
} else {
for (const auto candidate: source->indirect_image->sources) {
MarkCleanFlatSlots(plan, Source(plan, candidate), plan.clean_flat_slots);
}
}
}
plan.capture_specialization_reads = masked_image || !plan.control_flow.empty();
plan.capture_specialization_reads = capture_image_reads || !plan.control_flow.empty();
// Writable descriptor addresses must come from clean backing for the alias proof.
for (const auto& buffer: plan.info.buffers) {
if (buffer.written) MarkCleanFlatSlots(plan, Source(plan, buffer.source), plan.clean_flat_slots);
@@ -957,7 +937,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
for (const auto& image: plan.info.images) {
if (image.written) MarkCleanFlatSlots(plan, Source(plan, image.source), plan.clean_flat_slots);
}
if (masked_image) plan.resource_tracking_complete &= !program.has_address_writes;
if (capture_image_reads) plan.resource_tracking_complete &= !program.has_address_writes;
return plan;
}
@@ -1008,11 +988,39 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
return true;
};
snapshot.buffers.resize(program.info.buffers.size());
specialization.buffers.clear();
specialization.buffers.reserve(program.info.buffers.size());
for (uint32_t i = 0; i < program.info.buffers.size(); ++i) {
if (!evaluate(program.info.buffers[i].source, snapshot.buffers[i],
program.info.buffers[i].written)) {
return false;
}
auto& descriptor_value = snapshot.buffers[i];
ShaderBufferResource descriptor;
if (!DecodeBufferDescriptor(descriptor_value, descriptor)) {
return SpecializationFail(fmt::format("buffer descriptor {} has invalid width", i));
}
if (descriptor.Type() != 0) {
descriptor_value.dwords.fill(0);
descriptor = {};
}
auto packed_stride = descriptor.PackedStride();
const auto stride = packed_stride & 0x3fffu;
const bool swizzle = stride != 0u && ((packed_stride >> 14u) & 1u) != 0u;
if (stride == 0u) {
packed_stride &= ~((1u << 14u) | (3u << 16u));
} else if (!swizzle) {
packed_stride &= ~(3u << 16u);
}
specialization.buffers.push_back({
.packed_stride = packed_stride,
.descriptor_format = program.info.buffers[i].formatted
? descriptor.Format()
: 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,
});
}
snapshot.images.resize(program.info.images.size());
specialization.images.resize(program.info.images.size());
@@ -1039,13 +1047,7 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
continue;
}
const auto& indirect = *source->indirect_image;
DescriptorValue material;
DescriptorValue table;
if ((indirect.material_source != UINT32_MAX &&
!clean.EvaluateDescriptor(indirect.material_source, material)) ||
!clean.EvaluateDescriptor(indirect.table_source, table) ||
!MaterializeIndirectImage(program, indirect, material, table, i, observed, clean, snapshot,
specialization)) {
if (!MaterializeIndirectImage(program, indirect, i, observed, clean, snapshot, specialization)) {
return false;
}
} else {
@@ -1062,6 +1064,16 @@ bool MaterializeResources(const ResourcePlan& program, const SrtRuntime& runtime
if (!evaluate(program.info.samplers[i].source, snapshot.samplers[i])) {
return false;
}
if (program.info.samplers[i].gather_lod) {
const auto control = snapshot.samplers[i].dwords[2];
const auto filter = (control >> 26u) & 3u;
// MipNone always selects the base level. Explicit point gathers currently require
// encoded-zero primary and secondary bias; linear primary-mip selection is unsupported.
if (filter > 1u || (filter == 1u && (control & 0xfffffu) != 0u)) {
return SpecializationFail(
"explicit-LOD gather requires mip filtering None or Point with zero LOD biases");
}
}
}
snapshot.user_data.assign(runtime.user_data.begin(), runtime.user_data.end());
return BuildResourceSpecialization(program, snapshot, specialization);
@@ -1073,19 +1085,20 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
EXIT_IF(program.info.buffers.size() != specialization.buffers.size() ||
program.info.images.size() > specialization.images.size());
auto buffers = program.info.buffers;
auto& buffers = program.info.buffers;
for (size_t index = 0; index < buffers.size(); index++) {
buffers[index].packed_stride = specialization.buffers[index].packed_stride;
buffers[index].descriptor_format = specialization.buffers[index].descriptor_format;
buffers[index].descriptor_swizzle = specialization.buffers[index].descriptor_swizzle;
}
auto images = program.info.images;
auto& images = program.info.images;
const auto original_image_count = images.size();
images.reserve(specialization.images.size());
for (uint32_t index = 0; index < specialization.images.size(); index++) {
const auto& source = specialization.images[index];
if (index >= images.size()) {
EXIT_IF(source.indirect_root >= program.info.images.size());
images.push_back(program.info.images[source.indirect_root]);
EXIT_IF(source.indirect_root >= original_image_count);
images.push_back(images[source.indirect_root]);
}
auto& image = images[index];
image.numeric_class = source.numeric_class;
@@ -1108,15 +1121,17 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
}
SamplerPlan sampler_plan;
EXIT_IF(!BuildSamplerPlan(program.info, images, sampler_plan));
auto samplers = program.info.samplers;
auto sampled_pairs = program.info.sampled_pairs;
EXIT_IF(!BuildSamplerPlan(program.info, sampler_plan));
auto& samplers = program.info.samplers;
auto& sampled_pairs = program.info.sampled_pairs;
const auto original_sampler_count = samplers.size();
samplers.reserve(sampler_plan.sampler_count);
for (uint32_t index = 0; index < sampler_plan.sampler_count; index++) {
const auto& binding = sampler_plan.bindings[index];
if (index >= program.info.samplers.size()) {
samplers.push_back(program.info.samplers[binding.source]);
if (index >= original_sampler_count) {
samplers.push_back(samplers[binding.source]);
}
samplers[index].snapshot_index = binding.source;
samplers[index].force_point_filtering = binding.type == SamplerClass::PointInteger;
samplers[index].integer_border = binding.type != SamplerClass::Float;
}
@@ -1127,11 +1142,15 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
samplers[pair.sampler].depth_compare |= images[pair.image].depth_compare;
}
auto memory_info = program.memory_info;
auto& memory_info = program.memory_info;
const ImageRemap image_remap(specialization);
for (auto* block: program.blocks) {
for (auto it = block->begin(); it != block->end(); ++it) {
auto& inst = *it;
if (inst.GetOpcode() == ValueOpcode::GetImageResource) {
inst.SetFlags(image_remap[inst.Flags<uint32_t>()]);
continue;
}
if (BufferAccessOf(inst.GetOpcode()) == BufferAccess::Read) {
const auto& memory = memory_info[inst.Flags<MemoryFlags>().index];
if (memory.kind == ResourceKind::Buffer &&
@@ -1202,7 +1221,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
continue;
}
if (image_opcode.needs_sampler &&
memory.sampler < program.info.samplers.size()) {
memory.sampler < original_sampler_count) {
const auto type = static_cast<uint32_t>(ClassifySampler(image));
memory.sampler = sampler_plan.mapping[memory.sampler][type];
EXIT_IF(memory.sampler == UINT32_MAX);
@@ -1211,13 +1230,6 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
inst.GetOpcode() != ValueOpcode::ImageSampleRaw);
}
}
for (auto* block: program.blocks) {
for (auto& inst: *block) {
if (inst.GetOpcode() == ValueOpcode::GetImageResource) {
inst.SetFlags(image_remap[inst.Flags<uint32_t>()]);
}
}
}
for (auto& memory: memory_info) {
if (memory.kind == ResourceKind::Image && !memory.planning_only) {
memory.resource = image_remap[memory.resource];
@@ -1240,11 +1252,6 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
}
}
image_remap.Apply(images);
program.info.buffers = std::move(buffers);
program.info.images = std::move(images);
program.info.samplers = std::move(samplers);
program.info.sampled_pairs = std::move(sampled_pairs);
program.memory_info = std::move(memory_info);
}
} // namespace Libs::Graphics::ShaderRecompiler::IR
@@ -321,8 +321,9 @@ public:
for (uint32_t dword = 5u; dword < plan.roots.size(); dword++) {
plan.handle->SetArg(dword, plan.key);
}
for (const auto index: plan.memory) {
m_program.memory_info[index].planning_only = true;
if (plan.reads[0] != nullptr) {
for (const auto index: plan.memory)
m_program.memory_info[index].planning_only = true;
}
}
m_program.descriptor_sources = std::move(m_sources);
@@ -762,7 +763,7 @@ private:
const auto& b = *descriptor.indirect_image;
if (a.material_source != b.material_source || a.table_source != b.table_source ||
a.selector_stride != b.selector_stride || a.selector_offset != b.selector_offset ||
a.table_offset != b.table_offset ||
a.table_offset != b.table_offset || a.sources != b.sources ||
!EquivalentValue(m_program, a.key_count, b.key_count) ||
a.selector_mask.IsEmpty() != b.selector_mask.IsEmpty() ||
(!a.selector_mask.IsEmpty() &&
@@ -1474,6 +1475,152 @@ private:
return true;
}
uint32_t SelectorMaximum(Value value) const {
value = value.Resolve();
if (value.IsImmediate() && value.GetType() == Type::U32) return value.U32();
const auto* inst = value.TryInstruction();
if (inst == nullptr) return UINT32_MAX;
if (inst->GetOpcode() == ValueOpcode::FindILsb32)
return NonzeroOnEntry(inst->Arg(0), inst->Parent()) ? 31u : UINT32_MAX;
if (inst->GetOpcode() == ValueOpcode::UMin32)
return std::min(SelectorMaximum(inst->Arg(0)), SelectorMaximum(inst->Arg(1)));
uint32_t shift;
if (inst->GetOpcode() == ValueOpcode::ShiftLeftLogical32 &&
ImmediateU32(inst->Arg(1), shift) && shift < 32u) {
const auto maximum = uint64_t {SelectorMaximum(inst->Arg(0))} << shift;
if (maximum <= UINT32_MAX) return static_cast<uint32_t>(maximum);
}
return UINT32_MAX;
}
bool ImpossibleSwitchEdge(const Block* from, const Block* to) const {
const auto from_it = std::ranges::find(m_program.blocks, from);
const auto to_it = std::ranges::find(m_program.blocks, to);
if (from_it == m_program.blocks.end() || to_it == m_program.blocks.end() ||
m_program.block_info.size() != m_program.blocks.size()) return false;
const auto& info = m_program.block_info[from_it - m_program.blocks.begin()];
const auto& term = info.terminator;
if (term.kind != CFG::TerminatorKind::IndirectBranch ||
term.indirect_selector_code == UINT32_MAX ||
term.indirect_selector_values.empty() ||
term.indirect_selector_values.size() != term.indirect_selector_targets.size()) return false;
const auto target = m_program.block_info[to_it - m_program.blocks.begin()].id;
const auto maximum = SelectorMaximum(info.indirect_target);
bool found = false;
for (size_t i = 0; i < term.indirect_selector_values.size(); ++i) {
if (term.indirect_selector_targets[i] != target) continue;
if (term.indirect_selector_values[i] <= maximum) return false;
found = true;
}
return found;
}
bool TryMakeFiniteImage(Inst& handle, IndirectImagePlan& plan) {
if (handle.GetOpcode() != ValueOpcode::GetImageResource || handle.NumArgs() != 8u)
return false;
if (std::ranges::any_of(handle.Uses(), [](const Use& use) {
const auto op = use.user->GetOpcode();
return op != ValueOpcode::ImageSampleRaw && op != ValueOpcode::ImageGatherRaw;
})) return false;
bool has_phi = false;
for (size_t word = 0; word < handle.NumArgs(); ++word) has_phi |= handle.Arg(word).Resolve().IsPhi();
if (!has_phi) return false;
// Normalize the eight synchronized descriptor words to one GPU ordinal.
// Build the complete graph before changing IR so an unsupported leaf is transactional.
struct Choice {
DescriptorSource descriptor;
const Inst* branch = nullptr;
std::vector<uint32_t> children;
Value key;
};
std::vector<Choice> choices;
const auto visit = [&](const auto& self, DescriptorSource descriptor) -> uint32_t {
for (auto& word: descriptor.dwords) word = word.Resolve();
for (uint32_t i = 0; i < choices.size(); ++i)
if (choices[i].descriptor.dwords == descriptor.dwords) return i;
const auto index = static_cast<uint32_t>(choices.size());
choices.push_back({descriptor});
uint32_t bad = 0;
if (ValidateSource(descriptor, bad)) return index;
const auto* branch = descriptor.dwords[bad].TryInstruction();
if (branch == nullptr || branch->Parent() == nullptr ||
branch->GetOpcode() != ValueOpcode::Phi ||
branch->NumPhiBlocks() != branch->NumArgs()) return UINT32_MAX;
choices[index].branch = branch;
const auto count = branch->NumArgs();
for (uint32_t arm = 0; arm < count; ++arm) {
// A nonzero bit scan cannot choose its zero-input switch sentinel.
// Preserve the Phi edge; its ordinal is never observed at runtime.
if (ImpossibleSwitchEdge(branch->PhiBlock(arm), branch->Parent())) {
choices[index].children.push_back(UINT32_MAX);
continue;
}
auto child = descriptor;
for (uint32_t word = 0; word < child.dword_count; ++word) {
const auto* selected = descriptor.dwords[word].TryInstruction();
if (selected == nullptr || selected->GetOpcode() != branch->GetOpcode() ||
selected->Parent() != branch->Parent()) {
if (!ValidateRuntimeValue(m_program, descriptor.dwords[word])) return UINT32_MAX;
continue;
}
if (selected->NumArgs() != count || selected->NumPhiBlocks() != count)
return UINT32_MAX;
uint32_t incoming = 0;
while (incoming < count && selected->PhiBlock(incoming) != branch->PhiBlock(arm))
++incoming;
if (incoming == count) return UINT32_MAX;
child.dwords[word] = selected->Arg(incoming);
}
const auto next = self(self, child);
if (next == UINT32_MAX) return UINT32_MAX;
choices[index].children.push_back(next);
}
return index;
};
DescriptorSource root;
root.dword_count = 8u;
for (uint32_t word = 0; word < root.dword_count; ++word) root.dwords[word] = handle.Arg(word);
if (visit(visit, root) == UINT32_MAX || choices.front().branch == nullptr) return false;
DescriptorSource image_source;
image_source.dword_count = 8u;
image_source.dwords.fill(Value(0u));
image_source.indirect_image.emplace(DescriptorSource::IndirectImage{});
auto& sources = image_source.indirect_image->sources;
for (auto& choice: choices) {
if (choice.branch != nullptr) continue;
const auto source = InternSource(choice.descriptor);
auto found = std::ranges::find(sources, source);
choice.key = Value(static_cast<uint32_t>(found - sources.begin()));
if (found == sources.end()) sources.push_back(source);
}
if (sources.empty()) return false;
for (auto& choice: choices) {
if (choice.branch == nullptr) continue;
auto* block = choice.branch->Parent();
const auto where = std::ranges::find_if(block->Instructions(), [&](const Inst& inst) {
return &inst == choice.branch;
});
auto& key = *block->PrependNewInst(where, choice.branch->GetOpcode());
key.SetFlags(Type::U32);
choice.key = Value(&key);
}
for (const auto& choice: choices) {
if (choice.branch == nullptr) continue;
auto* key = choice.key.Instruction();
for (uint32_t arm = 0; arm < choice.children.size(); ++arm) {
const auto child = choice.children[arm];
const auto value = child == UINT32_MAX ? Value(0u) : choices[child].key;
key->AddPhiOperand(choice.branch->PhiBlock(arm), value);
}
}
plan.handle = &handle;
plan.source = InternSource(image_source);
plan.key = choices.front().key;
plan.roots = image_source.dwords;
plan.reads.fill(nullptr);
return true;
}
const IndirectImagePlan* FindIndirectImage(const Inst& handle) const {
const auto found =
std::find_if(m_indirect_images.begin(), m_indirect_images.end(),
@@ -1486,7 +1633,8 @@ private:
bool IsIndirectPlanningMemory(uint32_t index) const {
return std::any_of(m_indirect_images.begin(), m_indirect_images.end(),
[&](const IndirectImagePlan& plan) {
return std::ranges::find(plan.memory, index) != plan.memory.end();
return plan.reads[0] != nullptr &&
std::ranges::find(plan.memory, index) != plan.memory.end();
});
}
@@ -1502,7 +1650,7 @@ private:
continue;
}
IndirectImagePlan plan;
if (TryMakeIndirectImage(*handle, plan)) {
if (TryMakeIndirectImage(*handle, plan) || TryMakeFiniteImage(*handle, plan)) {
m_indirect_images.push_back(std::move(plan));
}
}
@@ -1577,15 +1725,20 @@ private:
uint32_t AddImage(uint32_t source, const MemoryInfo& memory, ValueOpcode op, uint32_t pc) {
const auto resource_class = ImageOpcodeInfoOf(op).resource_class;
const auto mip = resource_class == ImageResourceClass::Storage && memory.image_has_mip
? ImageMipMode::DynamicStorage
: ImageMipMode::None;
const bool atomic64 = ImageOpcodeInfoOf(op).access == ImageAccess::Atomic &&
memory.data_bits == 64u;
const bool dynamic_mip =
(resource_class == ImageResourceClass::Storage && memory.image_has_mip) ||
(op == ValueOpcode::ImageGatherRaw &&
(memory.image_sample_flags & Decoder::ImageSampleFlagLod) != 0u);
const auto mip = dynamic_mip ? ImageMipMode::Dynamic : ImageMipMode::None;
const bool depth = (memory.image_sample_flags & Decoder::ImageSampleFlagCompare) != 0;
for (uint32_t i = 0; i < m_info.images.size(); i++) {
auto& image = m_info.images[i];
if (image.source == source && image.resource_class == resource_class &&
image.dimension == memory.image_dimension && image.mip_mode == mip &&
image.depth_compare == depth && image.r128 == memory.image_r128) {
image.depth_compare == depth && image.r128 == memory.image_r128 &&
image.atomic64 == atomic64) {
Merge(image, op, pc);
return i;
}
@@ -1601,6 +1754,7 @@ private:
image.mip_mode = mip;
image.depth_compare = depth;
image.r128 = memory.image_r128;
image.atomic64 = atomic64;
Merge(image, op, pc);
m_info.images.push_back(image);
return static_cast<uint32_t>(m_info.images.size() - 1);
@@ -1787,6 +1941,9 @@ private:
if (sampler == UINT32_MAX) {
Fail(flags.pc, "sampler resource limit exceeded");
}
m_info.samplers[sampler].gather_lod |=
op == ValueOpcode::ImageGatherRaw &&
(memory.image_sample_flags & Decoder::ImageSampleFlagLod) != 0u;
AddHandlePatch(sampler_handle, sampler, flags.pc);
AddSampledPair(resource, sampler, flags.pc);
}
@@ -412,7 +412,7 @@ void CollectShaderInfo(Program& program, ShaderStageInputInfo input_info) {
ValidateOptions(program, input_info);
ValidateValueReferences(program, input_info);
auto next = program.info;
auto& next = program.info;
next.inputs.clear();
next.outputs.clear();
next.has_bitwise_xor =
@@ -433,7 +433,6 @@ void CollectShaderInfo(Program& program, ShaderStageInputInfo input_info) {
}
CollectBuiltinInputs(program, next);
CollectOutputs(program, input_info, next);
program.info = std::move(next);
program.shader_info_complete = true;
}
+26 -12
View File
@@ -138,7 +138,13 @@ int KernelEqueuePrivate::GetTriggeredEvents(KernelEvent* ev, int num) {
void KernelEqueuePrivate::TriggerExpiredTimers(uint64_t now_ns) {
for (auto& event: m_events) {
if (!event.triggered && event.deadline_ns != 0 && event.deadline_ns <= now_ns) {
if (event.deadline_ns != 0 && event.deadline_ns <= now_ns) {
if (event.event.filter == KERNEL_EVFILT_TIMER) {
const auto count = event.interval_ns == 0 ? !event.triggered :
1 + (now_ns - event.deadline_ns) / event.interval_ns;
event.event.data += static_cast<intptr_t>(count);
event.deadline_ns += count * event.interval_ns;
}
event.triggered = true;
}
}
@@ -209,7 +215,9 @@ int KernelEqueuePrivate::AddEvent(const KernelEqueueEvent& event) {
return e.event.ident == ident && e.event.filter == filter;
});
if (it != m_events.end()) {
TriggerExpiredTimers(MonotonicTimeNs());
it->deadline_ns = event.deadline_ns;
it->interval_ns = event.interval_ns;
it->event.udata = event.event.udata;
for (auto& pending: it->pending_events) {
pending.udata = event.event.udata;
@@ -511,6 +519,22 @@ int KYTY_SYSV_ABI KernelDeleteUserEvent(KernelEqueue eq, int id) {
return KernelDeleteEvent(eq, static_cast<uintptr_t>(id), KERNEL_EVFILT_USER);
}
static int AddTimerEvent(KernelEqueue eq, int id, uint64_t delay_ns, bool periodic, void* udata) {
const auto now_ns = MonotonicTimeNs();
KernelEqueueEvent event {};
event.deadline_ns = delay_ns <= UINT64_MAX - now_ns ? now_ns + delay_ns : UINT64_MAX;
event.interval_ns = periodic ? delay_ns : 0;
event.event.ident = static_cast<uintptr_t>(id);
event.event.filter = periodic ? KERNEL_EVFILT_TIMER : KERNEL_EVFILT_HRTIMER;
event.event.flags = EV_ADD | (periodic ? EV_CLEAR : EV_ONESHOT);
event.event.udata = udata;
return KernelAddEvent(eq, event);
}
int KYTY_SYSV_ABI KernelAddTimerEvent(KernelEqueue eq, int id, KernelUseconds usec, void* udata) {
return AddTimerEvent(eq, id, static_cast<uint64_t>(usec) * 1000, true, udata);
}
int KYTY_SYSV_ABI KernelAddHRTimerEvent(KernelEqueue eq, int id, const KernelTimespec* ts,
void* udata) {
if (ts == nullptr) {
@@ -524,17 +548,7 @@ int KYTY_SYSV_ABI KernelAddHRTimerEvent(KernelEqueue eq, int id, const KernelTim
const auto delay_ns =
static_cast<uint64_t>(ts->tv_sec) * 1000000000ull + static_cast<uint64_t>(ts->tv_nsec);
const auto now_ns = MonotonicTimeNs();
KernelEqueueEvent event {};
event.deadline_ns = delay_ns <= UINT64_MAX - now_ns ? now_ns + delay_ns : UINT64_MAX;
event.event.ident = static_cast<uintptr_t>(id);
event.event.filter = KERNEL_EVFILT_HRTIMER;
event.event.flags = EV_ADD | EV_ONESHOT;
event.event.fflags = 0;
event.event.data = 0;
event.event.udata = udata;
return KernelAddEvent(eq, event);
return AddTimerEvent(eq, id, delay_ns, false, udata);
}
int KYTY_SYSV_ABI KernelDeleteHRTimerEvent(KernelEqueue eq, int id) {
+3
View File
@@ -51,6 +51,7 @@ struct KernelFilter {
struct KernelEqueueEvent {
bool triggered = false;
uint64_t deadline_ns = 0;
uint64_t interval_ns = 0;
KernelEvent event;
KernelFilter filter;
std::deque<KernelEvent> pending_events;
@@ -72,6 +73,8 @@ int KYTY_SYSV_ABI KernelAddUserEventEdge(KernelEqueue eq, int id);
int KYTY_SYSV_ABI KernelTriggerUserEvent(KernelEqueue eq, int id, void* udata);
int KYTY_SYSV_ABI KernelTriggerUserEventForAll(int id, void* udata);
int KYTY_SYSV_ABI KernelDeleteUserEvent(KernelEqueue eq, int id);
int KYTY_SYSV_ABI KernelAddTimerEvent(KernelEqueue eq, int id, KernelUseconds usec,
void* udata);
int KYTY_SYSV_ABI KernelAddHRTimerEvent(KernelEqueue eq, int id, const KernelTimespec* ts,
void* udata);
int KYTY_SYSV_ABI KernelDeleteHRTimerEvent(KernelEqueue eq, int id);
+4
View File
@@ -4208,6 +4208,10 @@ int KYTY_SYSV_ABI pthread_attr_setschedpolicy(LibKernel::PthreadAttr* attr, int
return POSIX_PTHREAD_CALL(LibKernel::PthreadAttrSetschedpolicy(attr, policy));
}
int KYTY_SYSV_ABI pthread_attr_setstack(LibKernel::PthreadAttr* attr, void* addr, size_t size) {
return POSIX_PTHREAD_CALL(LibKernel::PthreadAttrSetstack(attr, addr, size));
}
int KYTY_SYSV_ABI pthread_attr_setstacksize(LibKernel::PthreadAttr* attr, size_t stack_size) {
// PRINT_NAME();
+1
View File
@@ -249,6 +249,7 @@ int KYTY_SYSV_ABI pthread_attr_setinheritsched(LibKernel::PthreadAttr* attr, int
int KYTY_SYSV_ABI pthread_attr_setschedparam(LibKernel::PthreadAttr* attr,
const LibKernel::KernelSchedParam* param);
int KYTY_SYSV_ABI pthread_attr_setschedpolicy(LibKernel::PthreadAttr* attr, int policy);
int KYTY_SYSV_ABI pthread_attr_setstack(LibKernel::PthreadAttr* attr, void* addr, size_t size);
int KYTY_SYSV_ABI pthread_attr_setstacksize(LibKernel::PthreadAttr* attr, size_t stack_size);
int KYTY_SYSV_ABI pthread_cond_broadcast(LibKernel::PthreadCond* cond);
int KYTY_SYSV_ABI pthread_cond_signal(LibKernel::PthreadCond* cond);
+3
View File
@@ -53,6 +53,9 @@ constexpr PadControl PAD_CONTROLS[] = {
{"Options", "Options", "Return"},
{"TouchPad", "Touch pad left (SELECT)", "Backspace"},
{"TouchPadRight", "Touch pad right (START)", "Tab"},
{"SpeakerVolume", "Speaker volume (cycle)", "1"},
{"VibrationIntensity", "Vibration intensity (cycle)", "2"},
{"TriggerEffectIntensity", "Trigger effect intensity (cycle)", "3"},
};
QString KeypadName(int key) {
+18 -10
View File
@@ -159,8 +159,8 @@ private:
static void OpenSdlDevice(PortIn* port, Format format);
static void CloseSdlDevice(PortIn* port);
static const void* PrepareOutputBuffer(const PortOut& port, const void* data,
std::vector<uint8_t>* buffer);
static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking);
std::vector<uint8_t>* buffer, float gain);
static bool QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain);
};
static Audio* g_audio = nullptr;
@@ -302,7 +302,7 @@ void Audio::CloseSdlDevice(PortOut* port) {
}
const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
std::vector<uint8_t>* buffer) {
std::vector<uint8_t>* buffer, float gain) {
EXIT_IF(data == nullptr);
EXIT_IF(buffer == nullptr);
@@ -312,7 +312,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
const auto bytes_per_sample = BytesPerSample(port.format);
const bool reorder = channels >= 8 && !FormatIsStd(port.format);
bool volume_changed = false;
bool volume_changed = gain != 1.0f;
for (uint32_t ch = 0; ch < channels; ch++) {
if (port.volume[ch] != 32768) {
volume_changed = true;
@@ -338,7 +338,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch;
dst[frame * output_channels + ch] =
src[frame * channels + src_ch] *
(static_cast<float>(port.volume[src_ch]) / 32768.0f);
(static_cast<float>(port.volume[src_ch]) / 32768.0f) * gain;
}
if (channels == 12) {
// Add the four top channels to their front/back channels, turning 12 into 8.
@@ -347,7 +347,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
for (uint32_t ch = 0; ch < 4; ch++) {
dst[frame * output_channels + HEIGHT_DST[ch]] +=
src[frame * channels + 8 + ch] *
(static_cast<float>(port.volume[8 + ch]) / 32768.0f);
(static_cast<float>(port.volume[8 + ch]) / 32768.0f) * gain;
}
}
}
@@ -360,6 +360,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
const auto src_ch = reorder ? SDL_8CH_MAP[ch] : ch;
int64_t sample =
static_cast<int64_t>(src[frame * channels + src_ch]) * port.volume[src_ch] / 32768;
sample = static_cast<int64_t>(sample * static_cast<double>(gain));
if (sample > std::numeric_limits<int16_t>::max()) {
sample = std::numeric_limits<int16_t>::max();
} else if (sample < std::numeric_limits<int16_t>::min()) {
@@ -373,7 +374,7 @@ const void* Audio::PrepareOutputBuffer(const PortOut& port, const void* data,
return buffer->data();
}
bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking, float gain) {
EXIT_IF(port == nullptr);
if (port->stream == nullptr || data == nullptr) {
@@ -381,7 +382,7 @@ bool Audio::QueueSdlAudio(PortOut* port, const void* data, bool blocking) {
}
std::vector<uint8_t> prepared_buffer;
const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer);
const void* prepared_data = PrepareOutputBuffer(*port, data, &prepared_buffer, gain);
const auto output_channels = OutputChannels(*port);
const auto prepared_size =
BytesPerSample(port->format) * output_channels * port->samples_num;
@@ -554,6 +555,12 @@ 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()];
const float gain =
port.type == AUDIO_OUT_PORT_TYPE_PADSPK
? Controller::GetSettingScale(Controller::Setting::SpeakerVolume)
: port.type == AUDIO_OUT_PORT_TYPE_VIBRATION
? Controller::GetSettingScale(Controller::Setting::VibrationIntensity)
: 1.0f;
uint64_t controller_queued_us = 0;
bool controller_uses_bluetooth = false;
@@ -562,14 +569,15 @@ uint32_t Audio::AudioOutOutputs(OutputParam* params, uint32_t num, bool blocking
Common::LockGuard lock(m_mutex);
controller_queued_us = Controller::DualSenseHaptics::Queue(
port.haptics, Controller::GetActiveControllerId(), params[i].data, port.samples_num,
static_cast<uint32_t>(port.channels_num), FormatIsFloat(port.format), port.volume);
static_cast<uint32_t>(port.channels_num), FormatIsFloat(port.format), port.volume,
gain);
controller_uses_bluetooth =
Controller::DualSenseHaptics::UsesBluetooth(port.haptics);
}
if (controller_queued_us == 0) {
// No DualSense took it (e.g. it was unplugged); a pad speaker port plays on the main
// output instead, and a vibration port has none.
if (QueueSdlAudio(&port, params[i].data, blocking)) {
if (QueueSdlAudio(&port, params[i].data, blocking, gain)) {
any_device = true;
paced[i] = port.queue_primed;
}
+100 -17
View File
@@ -15,6 +15,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <cstring>
#include <vector>
@@ -117,6 +118,8 @@ public:
int GetActiveControllerId();
void SetLightBar(uint8_t r, uint8_t g, uint8_t b);
bool SetTriggerEffect(const PadTriggerEffectParam& param);
void CycleSetting(Setting setting);
float GetSettingScale(Setting setting) const;
void ReadState(ControllerState* state, bool* flag, int* count);
int ReadStates(ControllerState* states, int states_num, bool* flag, int* count);
@@ -125,6 +128,9 @@ private:
void CheckActive();
void AddState();
// Apply cached output without changing its game-requested lifetime; caller holds m_mutex.
void ApplyVibration();
bool SendTriggerEffect(const PadTriggerEffectParam& param);
Common::Mutex m_mutex;
std::vector<int> m_connected_ids;
@@ -142,6 +148,12 @@ private:
uint32_t m_states_num = 0;
uint32_t m_first_state = 0;
uint8_t m_next_touch_id = 1;
// The game's last requests, re-sent when their intensity setting changes.
std::array<uint8_t, 2> m_vibration {};
uint64_t m_vibration_until = 0;
PadTriggerEffectParam m_trigger_effect {};
// Setting changes share the output lock; audio only needs an atomic scale snapshot.
std::array<std::atomic<uint32_t>, 3> m_setting_steps {};
};
static GameController* g_controller = nullptr;
@@ -183,17 +195,56 @@ static void pad_fill_data(PadData* data, const ControllerState& state, bool conn
data->device_unique_data_len = 0;
}
static bool trigger_effect_zones(uint8_t* effect, const uint8_t* strengths, uint8_t type,
uint8_t frequency = 0) {
// In cycle order, starting at the PS5 default.
constexpr std::array SPEAKER_VOLUME = {1.0f, 0.0f, 0.02f, 0.12f, 0.42f};
constexpr std::array INTENSITY = {1.0f, 0.0f, 0.33f, 0.66f};
void CycleSetting(Setting setting) {
if (g_controller != nullptr) {
g_controller->CycleSetting(setting);
}
}
float GetSettingScale(Setting setting) {
return g_controller != nullptr ? g_controller->GetSettingScale(setting) : 1.0f;
}
void GameController::CycleSetting(Setting setting) {
Common::LockGuard lock(m_mutex);
auto& step = m_setting_steps[static_cast<size_t>(setting)];
const auto count = setting == Setting::SpeakerVolume ? SPEAKER_VOLUME.size() : INTENSITY.size();
step.store((step.load(std::memory_order_relaxed) + 1) % count, std::memory_order_relaxed);
if (setting == Setting::VibrationIntensity) {
ApplyVibration();
} else if (setting == Setting::TriggerEffectIntensity) {
(void)SendTriggerEffect(m_trigger_effect);
}
}
float GameController::GetSettingScale(Setting setting) const {
const auto step = m_setting_steps[static_cast<size_t>(setting)].load(std::memory_order_relaxed);
return setting == Setting::SpeakerVolume ? SPEAKER_VOLUME[step] : INTENSITY[step];
}
static uint8_t Scale(uint8_t value, float scale) {
if (value == 0 || scale == 0.0f) {
return 0;
}
return static_cast<uint8_t>(std::max(1L, std::lround(value * scale)));
}
static bool trigger_effect_zones(uint8_t* effect, const uint8_t* strengths, float scale,
uint8_t type, uint8_t frequency = 0) {
uint16_t active = 0;
uint32_t packed = 0;
for (int i = 0; i < 10; i++) {
if (strengths[i] > 8) {
return false;
}
if (strengths[i] != 0) {
const auto strength = Scale(strengths[i], scale);
if (strength != 0) {
active |= static_cast<uint16_t>(1u << i);
packed |= static_cast<uint32_t>(strengths[i] - 1u) << (3 * i);
packed |= static_cast<uint32_t>(strength - 1u) << (3 * i);
}
}
@@ -208,7 +259,8 @@ static bool trigger_effect_zones(uint8_t* effect, const uint8_t* strengths, uint
return true;
}
static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command, uint8_t* effect) {
static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command, uint8_t* effect,
float scale) {
std::memset(effect, 0, 11);
effect[0] = 0x05;
@@ -220,7 +272,7 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command,
return false;
}
std::fill(strengths + command.data[0], strengths + 10, command.data[1]);
return trigger_effect_zones(effect, strengths, 0x21);
return trigger_effect_zones(effect, strengths, scale, 0x21);
case 2: {
const auto start = command.data[0];
const auto end = command.data[1];
@@ -228,14 +280,15 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command,
if (start < 2 || start > 7 || end <= start || end > 8 || strength > 8) {
return false;
}
if (strength == 0) {
const auto scaled = Scale(strength, scale);
if (scaled == 0) {
return true;
}
const uint16_t zones = static_cast<uint16_t>((1u << start) | (1u << end));
effect[0] = 0x25;
effect[1] = static_cast<uint8_t>(zones);
effect[2] = static_cast<uint8_t>(zones >> 8u);
effect[3] = strength - 1u;
effect[3] = scaled - 1u;
return true;
}
case 3:
@@ -243,8 +296,8 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command,
return false;
}
std::fill(strengths + command.data[0], strengths + 10, command.data[1]);
return trigger_effect_zones(effect, strengths, 0x26, command.data[2]);
case 4: return trigger_effect_zones(effect, command.data, 0x21);
return trigger_effect_zones(effect, strengths, scale, 0x26, command.data[2]);
case 4: return trigger_effect_zones(effect, command.data, scale, 0x21);
case 5: {
const int start = command.data[0];
const int end = command.data[1];
@@ -262,9 +315,9 @@ static bool trigger_effect_to_dualsense(const PadTriggerEffectCommand& command,
: start_strength +
(delta + (delta < 0 ? -length / 2 : length / 2)) / length);
}
return trigger_effect_zones(effect, strengths, 0x21);
return trigger_effect_zones(effect, strengths, scale, 0x21);
}
case 6: return trigger_effect_zones(effect, command.data + 1, 0x26, command.data[0]);
case 6: return trigger_effect_zones(effect, command.data + 1, scale, 0x26, command.data[0]);
default: return false;
}
}
@@ -355,6 +408,9 @@ void GameController::CheckActive() {
m_states_num = 0;
m_first_state = 0;
m_next_touch_id = 1;
m_vibration = {};
m_vibration_until = 0;
m_trigger_effect = {};
}
void GameController::AddState() {
@@ -600,11 +656,24 @@ void GameController::ReleaseHostPads() {
}
m_connected_ids.clear();
CheckActive();
}
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)) {
m_vibration = {large_motor, small_motor};
m_vibration_until = SDL_GetTicks() + RUMBLE_DURATION_MS;
ApplyVibration();
}
void GameController::ApplyVibration() {
const auto now = SDL_GetTicks();
const auto duration =
m_vibration_until > now ? static_cast<uint32_t>(m_vibration_until - now) : 0;
const auto scale = duration != 0 ? GetSettingScale(Setting::VibrationIntensity) : 0.0f;
const auto large_motor = Scale(m_vibration[0], scale);
const auto small_motor = Scale(m_vibration[1], scale);
if (DualSenseHaptics::SetVibration(m_active_id, large_motor, small_motor, duration)) {
return;
}
@@ -619,7 +688,7 @@ void GameController::SetVibration(uint8_t large_motor, uint8_t small_motor) {
const auto large = static_cast<uint16_t>(large_motor * 0x101U);
const auto small = static_cast<uint16_t>(small_motor * 0x101U);
if (!SDL_RumbleGamepad(pad, large, small, RUMBLE_DURATION_MS)) {
if (!SDL_RumbleGamepad(pad, large, small, duration)) {
LOGF("\t rumble failed: %s\n", SDL_GetError());
}
}
@@ -643,20 +712,35 @@ void GameController::SetLightBar(uint8_t r, uint8_t g, uint8_t b) {
}
bool GameController::SetTriggerEffect(const PadTriggerEffectParam& param) {
Common::LockGuard lock(m_mutex);
if (!SendTriggerEffect(param)) {
return false;
}
for (int i = 0; i < 2; i++) {
if ((param.trigger_mask & (1u << i)) != 0) {
m_trigger_effect.trigger_mask |= static_cast<uint8_t>(1u << i);
m_trigger_effect.command[i] = param.command[i];
}
}
return true;
}
bool GameController::SendTriggerEffect(const PadTriggerEffectParam& param) {
if ((param.trigger_mask & ~0x03u) != 0) {
return false;
}
DualSenseEffects effect {};
const auto scale = GetSettingScale(Setting::TriggerEffectIntensity);
if ((param.trigger_mask & 0x01u) != 0) {
effect.enable_bits |= 0x08;
if (!trigger_effect_to_dualsense(param.command[0], effect.left_trigger)) {
if (!trigger_effect_to_dualsense(param.command[0], effect.left_trigger, scale)) {
return false;
}
}
if ((param.trigger_mask & 0x02u) != 0) {
effect.enable_bits |= 0x04;
if (!trigger_effect_to_dualsense(param.command[1], effect.right_trigger)) {
if (!trigger_effect_to_dualsense(param.command[1], effect.right_trigger, scale)) {
return false;
}
}
@@ -664,7 +748,6 @@ bool GameController::SetTriggerEffect(const PadTriggerEffectParam& param) {
return true;
}
Common::LockGuard lock(m_mutex);
auto* pad = SDL_GetGamepadFromID(static_cast<SDL_JoystickID>(m_active_id));
if (pad != nullptr && SDL_GetGamepadType(pad) == SDL_GAMEPAD_TYPE_PS5) {
(void)SDL_SendGamepadEffect(pad, &effect, sizeof(effect));
+5
View File
@@ -75,6 +75,11 @@ void SetSensor(int id, Sensor sensor, const float* data, uint64_t time_us);
void ResetInputState();
int GetActiveControllerId();
enum class Setting { SpeakerVolume, VibrationIntensity, TriggerEffectIntensity };
void CycleSetting(Setting setting);
float GetSettingScale(Setting setting);
int KYTY_SYSV_ABI PadInit();
int KYTY_SYSV_ABI PadOpen(int user_id, int type, int index, const void* param);
int KYTY_SYSV_ABI PadGetHandle(int user_id, int type, int index);
+122 -88
View File
@@ -14,6 +14,81 @@
// NOLINTNEXTLINE(modernize-concat-nested-namespaces)
namespace Libs::Dialog {
namespace {
constexpr int DIALOG_STATUS_RUNNING = 2;
constexpr int DIALOG_STATUS_FINISHED = 3;
struct DialogState {
int status = 0;
uint64_t generation = 0;
};
std::mutex g_dialog_mutex;
DialogState g_error;
DialogState g_signin;
uint64_t g_dialog_generation = 0;
uint64_t g_dialog_revision = 0;
int32_t g_error_code = 0;
void (*g_dialog_visibility_callback)() = nullptr;
void SetDialogStatus(DialogState& dialog, int status, std::unique_lock<std::mutex>& lock) {
const bool visibility_changed =
(dialog.status == DIALOG_STATUS_RUNNING) != (status == DIALOG_STATUS_RUNNING);
dialog.status = status;
if (visibility_changed) {
g_dialog_revision++;
if (status == DIALOG_STATUS_RUNNING) {
dialog.generation = ++g_dialog_generation;
}
}
const auto callback = visibility_changed ? g_dialog_visibility_callback : nullptr;
lock.unlock();
if (callback != nullptr) {
callback();
}
}
} // namespace
namespace SystemDialog {
bool GetHostSnapshot(HostSnapshot* snapshot) {
std::scoped_lock lock(g_dialog_mutex);
if (g_signin.status == DIALOG_STATUS_RUNNING) {
*snapshot = {Kind::Signin, g_signin.generation, 0};
return true;
}
if (g_error.status == DIALOG_STATUS_RUNNING) {
*snapshot = {Kind::Error, g_error.generation, g_error_code};
return true;
}
return false;
}
VisualState GetVisualState() noexcept {
std::scoped_lock lock(g_dialog_mutex);
const bool signin = g_signin.status == DIALOG_STATUS_RUNNING;
return {g_error.status == DIALOG_STATUS_RUNNING || signin, signin, g_dialog_revision};
}
void SetVisibilityCallback(void (*callback)()) {
std::scoped_lock lock(g_dialog_mutex);
g_dialog_visibility_callback = callback;
}
bool HostClose(uint64_t generation) {
std::unique_lock lock(g_dialog_mutex);
auto& dialog = g_signin.status == DIALOG_STATUS_RUNNING ? g_signin : g_error;
if (dialog.status != DIALOG_STATUS_RUNNING || generation != dialog.generation) {
return false;
}
SetDialogStatus(dialog, DIALOG_STATUS_FINISHED, lock);
return true;
}
} // namespace SystemDialog
namespace CommonDialog {
LIB_NAME("CommonDialog", "CommonDialog");
@@ -207,6 +282,7 @@ constexpr int SIGNIN_ERROR_NOT_INITIALIZED = static_cast<int>(0x81350001u);
constexpr int SIGNIN_ERROR_ALREADY_INITIALIZED = static_cast<int>(0x81350002u);
constexpr int SIGNIN_ERROR_PARAM_INVALID = static_cast<int>(0x81350003u);
constexpr int SIGNIN_ERROR_INVALID_STATE = static_cast<int>(0x81350005u);
constexpr int SIGNIN_ERROR_INVALID_USER_ID = static_cast<int>(0x81350007u);
struct SigninDialogResult {
int32_t result;
@@ -222,70 +298,78 @@ struct SigninDialogParam {
static_assert(sizeof(SigninDialogResult) == 16);
static_assert(sizeof(SigninDialogParam) == 16);
static int g_signin_status = SIGNIN_STATUS_NONE;
int KYTY_SYSV_ABI SigninDialogInitialize() {
PRINT_NAME();
if (g_signin_status != SIGNIN_STATUS_NONE) {
std::scoped_lock lock(g_dialog_mutex);
if (g_signin.status != SIGNIN_STATUS_NONE) {
return SIGNIN_ERROR_ALREADY_INITIALIZED;
}
g_signin_status = SIGNIN_STATUS_INITIALIZED;
g_signin.status = SIGNIN_STATUS_INITIALIZED;
return OK;
}
int KYTY_SYSV_ABI SigninDialogTerminate() {
PRINT_NAME();
if (g_signin_status == SIGNIN_STATUS_NONE) {
std::unique_lock lock(g_dialog_mutex);
if (g_signin.status == SIGNIN_STATUS_NONE) {
return SIGNIN_ERROR_NOT_INITIALIZED;
}
g_signin_status = SIGNIN_STATUS_NONE;
SetDialogStatus(g_signin, SIGNIN_STATUS_NONE, lock);
return OK;
}
int KYTY_SYSV_ABI SigninDialogOpen(const void* param) {
PRINT_NAME();
if (g_signin_status != SIGNIN_STATUS_INITIALIZED && g_signin_status != SIGNIN_STATUS_FINISHED) {
std::unique_lock lock(g_dialog_mutex);
if (g_signin.status == SIGNIN_STATUS_NONE) {
return SIGNIN_ERROR_NOT_INITIALIZED;
}
if (g_signin.status != SIGNIN_STATUS_INITIALIZED && g_signin.status != SIGNIN_STATUS_FINISHED) {
return SIGNIN_ERROR_INVALID_STATE;
}
if (param == nullptr ||
static_cast<const SigninDialogParam*>(param)->size != sizeof(SigninDialogParam)) {
if (param == nullptr) {
return SIGNIN_ERROR_PARAM_INVALID;
}
g_signin_status = SIGNIN_STATUS_RUNNING;
const auto* p = static_cast<const SigninDialogParam*>(param);
if (p->size != sizeof(SigninDialogParam) || p->reserved[0] != 0 || p->reserved[1] != 0) {
return SIGNIN_ERROR_PARAM_INVALID;
}
if (p->user_id != Config::GetUserId()) {
return SIGNIN_ERROR_INVALID_USER_ID;
}
SetDialogStatus(g_signin, SIGNIN_STATUS_RUNNING, lock);
return OK;
}
int KYTY_SYSV_ABI SigninDialogClose() {
PRINT_NAME();
if (g_signin_status == SIGNIN_STATUS_NONE) {
std::unique_lock lock(g_dialog_mutex);
if (g_signin.status == SIGNIN_STATUS_NONE) {
return SIGNIN_ERROR_NOT_INITIALIZED;
}
if (g_signin_status != SIGNIN_STATUS_RUNNING && g_signin_status != SIGNIN_STATUS_FINISHED) {
return SIGNIN_ERROR_INVALID_STATE;
}
g_signin_status = SIGNIN_STATUS_FINISHED;
SetDialogStatus(g_signin, SIGNIN_STATUS_FINISHED, lock);
return OK;
}
int KYTY_SYSV_ABI SigninDialogUpdateStatus() {
PRINT_NAME();
if (g_signin_status == SIGNIN_STATUS_RUNNING) {
g_signin_status = SIGNIN_STATUS_FINISHED;
}
return g_signin_status;
std::scoped_lock lock(g_dialog_mutex);
return g_signin.status;
}
int KYTY_SYSV_ABI SigninDialogGetStatus() {
PRINT_NAME();
return g_signin_status;
std::scoped_lock lock(g_dialog_mutex);
return g_signin.status;
}
int KYTY_SYSV_ABI SigninDialogGetResult(void* result) {
PRINT_NAME();
if (g_signin_status == SIGNIN_STATUS_NONE) {
std::scoped_lock lock(g_dialog_mutex);
if (g_signin.status == SIGNIN_STATUS_NONE) {
return SIGNIN_ERROR_NOT_INITIALIZED;
}
if (g_signin_status != SIGNIN_STATUS_FINISHED) {
if (g_signin.status != SIGNIN_STATUS_FINISHED) {
return SIGNIN_ERROR_INVALID_STATE;
}
if (result == nullptr) {
@@ -661,64 +745,15 @@ struct ErrorDialogParam {
static_assert(sizeof(ErrorDialogParam) == 16);
static std::mutex g_error_mutex;
static int g_error_status = STATUS_NONE;
static uint64_t g_error_generation = 0;
static uint64_t g_error_revision = 0;
static int32_t g_error_code = 0;
static void (*g_error_visibility_callback)() = nullptr;
static void SetStatus(int status, std::unique_lock<std::mutex>& lock) {
const bool visibility_changed =
(g_error_status == STATUS_RUNNING) != (status == STATUS_RUNNING);
g_error_status = status;
if (visibility_changed) {
g_error_revision++;
}
const auto callback = visibility_changed ? g_error_visibility_callback : nullptr;
lock.unlock();
if (callback != nullptr) {
callback();
}
}
bool GetHostSnapshot(HostSnapshot* snapshot) {
std::scoped_lock lock(g_error_mutex);
if (g_error_status != STATUS_RUNNING) {
return false;
}
*snapshot = {g_error_generation, g_error_code};
return true;
}
VisualState GetVisualState() noexcept {
std::scoped_lock lock(g_error_mutex);
return {g_error_status == STATUS_RUNNING, g_error_revision};
}
void SetVisibilityCallback(void (*callback)()) {
std::scoped_lock lock(g_error_mutex);
g_error_visibility_callback = callback;
}
bool HostAccept(uint64_t generation) {
std::unique_lock lock(g_error_mutex);
if (g_error_status != STATUS_RUNNING || generation != g_error_generation) {
return false;
}
SetStatus(STATUS_FINISHED, lock);
return true;
}
int KYTY_SYSV_ABI ErrorDialogInitialize() {
PRINT_NAME();
std::scoped_lock lock(g_error_mutex);
if (g_error_status != STATUS_NONE) {
std::scoped_lock lock(g_dialog_mutex);
if (g_error.status != STATUS_NONE) {
return ERROR_ALREADY_INITIALIZED;
}
g_error_status = STATUS_INITIALIZED;
g_error.status = STATUS_INITIALIZED;
return OK;
}
@@ -726,8 +761,8 @@ int KYTY_SYSV_ABI ErrorDialogInitialize() {
int KYTY_SYSV_ABI ErrorDialogOpen(const void* param) {
PRINT_NAME();
std::unique_lock lock(g_error_mutex);
if (g_error_status != STATUS_INITIALIZED && g_error_status != STATUS_FINISHED) {
std::unique_lock lock(g_dialog_mutex);
if (g_error.status != STATUS_INITIALIZED && g_error.status != STATUS_FINISHED) {
return ERROR_INVALID_STATE;
}
if (param == nullptr) {
@@ -745,8 +780,7 @@ int KYTY_SYSV_ABI ErrorDialogOpen(const void* param) {
p->size, static_cast<uint32_t>(p->error_code), p->user_id);
g_error_code = p->error_code;
g_error_generation++;
SetStatus(STATUS_RUNNING, lock);
SetDialogStatus(g_error, STATUS_RUNNING, lock);
return OK;
}
@@ -754,12 +788,12 @@ int KYTY_SYSV_ABI ErrorDialogOpen(const void* param) {
int KYTY_SYSV_ABI ErrorDialogClose() {
PRINT_NAME();
std::unique_lock lock(g_error_mutex);
if (g_error_status != STATUS_RUNNING) {
std::unique_lock lock(g_dialog_mutex);
if (g_error.status != STATUS_RUNNING) {
return ERROR_INVALID_STATE;
}
SetStatus(STATUS_FINISHED, lock);
SetDialogStatus(g_error, STATUS_FINISHED, lock);
return OK;
}
@@ -767,12 +801,12 @@ int KYTY_SYSV_ABI ErrorDialogClose() {
int KYTY_SYSV_ABI ErrorDialogTerminate() {
PRINT_NAME();
std::unique_lock lock(g_error_mutex);
if (g_error_status == STATUS_NONE) {
std::unique_lock lock(g_dialog_mutex);
if (g_error.status == STATUS_NONE) {
return ERROR_NOT_INITIALIZED;
}
SetStatus(STATUS_NONE, lock);
SetDialogStatus(g_error, STATUS_NONE, lock);
return OK;
}
@@ -780,15 +814,15 @@ int KYTY_SYSV_ABI ErrorDialogTerminate() {
int KYTY_SYSV_ABI ErrorDialogUpdateStatus() {
PRINT_NAME();
std::scoped_lock lock(g_error_mutex);
return g_error_status;
std::scoped_lock lock(g_dialog_mutex);
return g_error.status;
}
int KYTY_SYSV_ABI ErrorDialogGetStatus() {
PRINT_NAME();
std::scoped_lock lock(g_error_mutex);
return g_error_status;
std::scoped_lock lock(g_dialog_mutex);
return g_error.status;
}
} // namespace ErrorDialog
+10 -2
View File
@@ -68,22 +68,30 @@ int KYTY_SYSV_ABI MsgDialogProgressBarSetMsg(int target, const char* msg);
} // namespace MsgDialog
namespace ErrorDialog {
namespace SystemDialog {
enum class Kind { Error, Signin };
struct HostSnapshot {
Kind kind;
uint64_t generation;
int32_t error_code;
};
struct VisualState {
bool active;
bool background;
uint64_t revision;
};
bool GetHostSnapshot(HostSnapshot* snapshot);
VisualState GetVisualState() noexcept;
void SetVisibilityCallback(void (*callback)());
bool HostAccept(uint64_t generation);
bool HostClose(uint64_t generation);
} // namespace SystemDialog
namespace ErrorDialog {
int KYTY_SYSV_ABI ErrorDialogInitialize();
int KYTY_SYSV_ABI ErrorDialogOpen(const void* param);
+4 -4
View File
@@ -311,7 +311,7 @@ bool UsesBluetooth(const Stream* stream) {
}
uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume) {
bool is_float, const int* volume, float gain) {
if (stream == nullptr || data == nullptr || frames == 0 || channels == 0 || volume == nullptr) {
return 0;
}
@@ -353,7 +353,7 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames
const auto index = (static_cast<size_t>(first_frame) + 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;
value *= volume[src_ch] / 32768.0f * gain;
// USB places the speaker in front and the actuators in back; Bluetooth takes stereo.
stream->buffer[static_cast<size_t>(frame) * output_channels + ch + channel_offset] =
value;
@@ -382,7 +382,7 @@ uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames
return queued_us;
}
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) {
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor, uint32_t duration_ms) {
if (SDL_GetGamepadTypeForID(static_cast<SDL_JoystickID>(controller)) != SDL_GAMEPAD_TYPE_PS5) {
return false;
}
@@ -390,7 +390,7 @@ bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor) {
SelectController(controller);
g_large_motor = large_motor;
g_small_motor = small_motor;
g_rumble_until = SDL_GetTicks() + 0xffff;
g_rumble_until = SDL_GetTicks() + duration_ms;
ApplyRumble();
return true;
}
+3 -2
View File
@@ -17,9 +17,10 @@ bool UsesBluetooth(const Stream* stream);
// (the active pad has no usable controller audio device or its speaker cannot be routed).
// Ambiguous USB or Bluetooth endpoints are not selected.
uint64_t Queue(Stream* stream, int controller, const void* data, uint32_t frames, uint32_t channels,
bool is_float, const int* volume);
bool is_float, const int* volume, float gain = 1.0f);
// Returns false for other gamepad types, which retain the normal rumble path.
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor);
bool SetVibration(int controller, uint8_t large_motor, uint8_t small_motor,
uint32_t duration_ms = 0xffff);
// Also restores the headphone routing.
void Shutdown();
+56 -8
View File
@@ -2189,6 +2189,7 @@ LIB_DEFINE(InitLibKernel_1_Posix) {
LIB_FUNC("wtkt-teR1so", Posix::pthread_attr_init);
LIB_FUNC("zHchY8ft5pk", Posix::pthread_attr_destroy);
LIB_FUNC("vQm4fDEsWi8", Posix::pthread_attr_getstack);
LIB_FUNC("-SrbXpGR1f0", Posix::pthread_attr_setstack);
LIB_FUNC("2Q0z6rnBrTE", Posix::pthread_attr_setstacksize);
LIB_FUNC("Ucsu-OK+els", Posix::pthread_attr_get_np);
LIB_FUNC("RtLRV-pBTTY", Posix::pthread_attr_getschedpolicy);
@@ -2827,6 +2828,7 @@ constexpr int32_t KERNEL_AIO_STATE_SUBMITTED = 1;
constexpr int32_t KERNEL_AIO_STATE_PROCESSING = 2;
constexpr int32_t KERNEL_AIO_STATE_COMPLETED = 3;
constexpr int32_t KERNEL_AIO_STATE_ABORTED = 4;
constexpr int32_t KERNEL_AIO_STATE_NOTIFIED = 0x10000;
constexpr int32_t KERNEL_AIO_MAX_QUEUE = 512;
constexpr int32_t KERNEL_AIO_MAX_REQUESTS = 128;
@@ -2939,10 +2941,31 @@ int KYTY_SYSV_ABI KernelAioPollRequest(int32_t id, int32_t* state) {
}
if (!kernel_aio_is_valid_id(id)) {
return LibKernel::KERNEL_ERROR_EINVAL;
*state = LibKernel::KERNEL_ERROR_ESRCH;
return OK;
}
*state = g_kernel_aio_state[id].load(std::memory_order_acquire);
auto current = g_kernel_aio_state[id].load(std::memory_order_acquire);
while (current == KERNEL_AIO_STATE_COMPLETED || current == KERNEL_AIO_STATE_ABORTED) {
if (g_kernel_aio_state[id].compare_exchange_weak(current, current | KERNEL_AIO_STATE_NOTIFIED,
std::memory_order_acq_rel)) {
break;
}
}
*state = current == 0 ? LibKernel::KERNEL_ERROR_ESRCH : current;
return OK;
}
int KYTY_SYSV_ABI KernelAioPollRequests(int32_t* ids, int32_t num, int32_t* states) {
if (ids == nullptr || states == nullptr) {
return LibKernel::KERNEL_ERROR_EFAULT;
}
if (num <= 0 || num > KERNEL_AIO_MAX_REQUESTS) {
return LibKernel::KERNEL_ERROR_EINVAL;
}
for (int32_t i = 0; i < num; i++) {
KernelAioPollRequest(ids[i], &states[i]);
}
return OK;
}
@@ -2954,7 +2977,7 @@ int KYTY_SYSV_ABI KernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* use
}
if (!kernel_aio_is_valid_id(id)) {
return LibKernel::KERNEL_ERROR_EINVAL;
return KernelAioPollRequest(id, state);
}
uint32_t waited = 0;
@@ -2970,8 +2993,7 @@ int KYTY_SYSV_ABI KernelAioWaitRequest(int32_t id, int32_t* state, uint32_t* use
current = g_kernel_aio_state[id].load(std::memory_order_acquire);
}
*state = current;
return OK;
return KernelAioPollRequest(id, state);
}
int KYTY_SYSV_ABI KernelAioDeleteRequest(int32_t id, int32_t* ret) {
@@ -2982,12 +3004,35 @@ int KYTY_SYSV_ABI KernelAioDeleteRequest(int32_t id, int32_t* ret) {
}
if (!kernel_aio_is_valid_id(id)) {
return LibKernel::KERNEL_ERROR_EINVAL;
*ret = LibKernel::KERNEL_ERROR_ESRCH;
return OK;
}
g_kernel_aio_state[id].store(KERNEL_AIO_STATE_ABORTED, std::memory_order_release);
*ret = OK;
auto current = g_kernel_aio_state[id].load(std::memory_order_acquire);
for (;;) {
const auto state = current & ~KERNEL_AIO_STATE_NOTIFIED;
if (state != KERNEL_AIO_STATE_COMPLETED && state != KERNEL_AIO_STATE_ABORTED) {
*ret = current == 0 ? LibKernel::KERNEL_ERROR_ESRCH : LibKernel::KERNEL_ERROR_EBUSY;
break;
}
if (g_kernel_aio_state[id].compare_exchange_weak(current, 0, std::memory_order_acq_rel)) {
*ret = OK;
break;
}
}
return OK;
}
int KYTY_SYSV_ABI KernelAioDeleteRequests(int32_t* ids, int32_t num, int32_t* rets) {
if (ids == nullptr || rets == nullptr) {
return LibKernel::KERNEL_ERROR_EFAULT;
}
if (num <= 0 || num > KERNEL_AIO_MAX_REQUESTS) {
return LibKernel::KERNEL_ERROR_EINVAL;
}
for (int32_t i = 0; i < num; i++) {
KernelAioDeleteRequest(ids[i], &rets[i]);
}
return OK;
}
@@ -3103,6 +3148,7 @@ LIB_DEFINE(InitLibKernel_1_Equeue) {
LIB_FUNC("WDszmSbWuDk", EventQueue::KernelAddUserEventEdge);
LIB_FUNC("F6e0kwo4cnk", EventQueue::KernelTriggerUserEvent);
LIB_FUNC("LJDwdSNTnDg", EventQueue::KernelDeleteUserEvent);
LIB_FUNC("57ZK+ODEXWY", EventQueue::KernelAddTimerEvent);
LIB_FUNC("R74tt43xP6k", EventQueue::KernelAddHRTimerEvent);
LIB_FUNC("J+LF6LwObXU", EventQueue::KernelDeleteHRTimerEvent);
LIB_FUNC("bBfz7kMF2Ho", EventQueue::KernelAddAmprEvent);
@@ -3325,8 +3371,10 @@ LIB_DEFINE(InitLibKernel_1) {
LIB_FUNC("kbw4UHHSYy0", LibKernel::pthread_cxa_finalize);
LIB_FUNC("lLMT9vJAck0", LibKernel::clock_gettime);
LIB_FUNC("5TgME6AYty4", KernelAioDeleteRequest);
LIB_FUNC("Ft3EtsZzAoY", KernelAioDeleteRequests);
LIB_FUNC("HgX7+AORI58", KernelAioSubmitReadCommands);
LIB_FUNC("2pOuoWoCxdk", KernelAioPollRequest);
LIB_FUNC("o7O4z3jwKzo", KernelAioPollRequests);
LIB_FUNC("KOF-oJbQVvc", KernelAioWaitRequest);
LIB_FUNC("XQ8C8y+de+E", KernelAioSubmitWriteCommands);
LIB_FUNC("nu4a0-arQis", KernelAioInitializeParam);
+50 -5
View File
@@ -111,6 +111,10 @@ int KYTY_SYSV_ABI NetResolverDestroy(int rid) {
return FinishNetCall(Net::NetResolverDestroy(rid));
}
int KYTY_SYSV_ABI NetResolverAbort(int rid, int flags) {
return FinishNetCall(Net::NetResolverAbort(rid, flags));
}
int KYTY_SYSV_ABI NetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout,
int retry, int flags) {
return FinishNetCall(Net::NetResolverStartNtoa(rid, hostname, addr, timeout, retry, flags));
@@ -203,6 +207,7 @@ LIB_DEFINE(InitNet_1_Net) {
LIB_FUNC("K7RlrTkI-mw", LibNet::NetPoolDestroy);
LIB_FUNC("C4UgDHHPvdw", LibNet::NetResolverCreate);
LIB_FUNC("kJlYH5uMAWI", LibNet::NetResolverDestroy);
LIB_FUNC("AzqoBha7js4", LibNet::NetResolverAbort);
LIB_FUNC("Nd91WaWmG2w", LibNet::NetResolverStartNtoa);
LIB_FUNC("8Kcp5d-q1Uo", LibNet::NetInetPton);
LIB_FUNC("9vA2aW+CHuA", LibNet::NetInetNtop);
@@ -3133,9 +3138,21 @@ namespace LibNpWebApi2 {
LIB_VERSION("NpWebApi2", 1, "NpWebApi2", 1, 1);
constexpr int NP_WEBAPI2_ERROR_INVALID_ARGUMENT = -2141899774; /* 0x80553402 */
constexpr int NP_WEBAPI2_ERROR_REQUEST_NOT_FOUND = -2141899770; /* 0x80553406 */
constexpr int NP_WEBAPI2_ERROR_NOT_SIGNED_IN = -2141899769; /* 0x80553407 */
constexpr int NP_WEBAPI2_ERROR_INVALID_ARGUMENT = -2141899774; /* 0x80553402 */
constexpr int NP_WEBAPI2_ERROR_INVALID_LIB_CONTEXT_ID = -2141899773; /* 0x80553403 */
constexpr int NP_WEBAPI2_ERROR_LIB_CONTEXT_NOT_FOUND = -2141899772; /* 0x80553404 */
constexpr int NP_WEBAPI2_ERROR_REQUEST_NOT_FOUND = -2141899770; /* 0x80553406 */
constexpr int NP_WEBAPI2_ERROR_NOT_SIGNED_IN = -2141899769; /* 0x80553407 */
struct NpWebApi2MemoryPoolStats {
size_t pool_size;
size_t max_inuse_size;
size_t current_inuse_size;
int32_t reserved;
};
static std::mutex g_np_webapi2_context_mutex;
static std::map<int, size_t> g_np_webapi2_contexts;
struct NpWebApi2ResponseInformationOption {
int32_t http_status;
@@ -3166,7 +3183,30 @@ static int KYTY_SYSV_ABI NpWebApi2Initialize(int lib_http_ctx_id, size_t pool_si
static int id = 0;
return ++id;
if (pool_size > std::numeric_limits<size_t>::max() - 0x3fff) {
return NP_WEBAPI2_ERROR_INVALID_ARGUMENT;
}
std::lock_guard lock(g_np_webapi2_context_mutex);
g_np_webapi2_contexts[++id] = (pool_size + 0x3fff) & ~size_t {0x3fff};
return id;
}
static int KYTY_SYSV_ABI NpWebApi2GetMemoryPoolStats(int lib_ctx_id,
NpWebApi2MemoryPoolStats* stats) {
if (stats == nullptr) {
return NP_WEBAPI2_ERROR_INVALID_ARGUMENT;
}
if (lib_ctx_id <= 0) {
return NP_WEBAPI2_ERROR_INVALID_LIB_CONTEXT_ID;
}
std::lock_guard lock(g_np_webapi2_context_mutex);
const auto context = g_np_webapi2_contexts.find(lib_ctx_id);
if (context == g_np_webapi2_contexts.end()) {
return NP_WEBAPI2_ERROR_LIB_CONTEXT_NOT_FOUND;
}
// Requests currently use host storage, not allocations from the library pool.
*stats = {context->second, 0, 0, 0};
return OK;
}
static int KYTY_SYSV_ABI NpWebApi2PushEventCreateHandle(int lib_ctx_id) {
@@ -3433,11 +3473,16 @@ static int KYTY_SYSV_ABI NpWebApi2Terminate(int lib_ctx_id) {
LOGF("\t lib_ctx_id = %d\n", lib_ctx_id);
return 0;
if (lib_ctx_id <= 0) {
return NP_WEBAPI2_ERROR_INVALID_LIB_CONTEXT_ID;
}
std::lock_guard lock(g_np_webapi2_context_mutex);
return g_np_webapi2_contexts.erase(lib_ctx_id) != 0 ? OK : NP_WEBAPI2_ERROR_LIB_CONTEXT_NOT_FOUND;
}
LIB_DEFINE(InitNet_1_NpWebApi2) {
LIB_FUNC("+o9816YQhqQ", LibNpWebApi2::NpWebApi2Initialize);
LIB_FUNC("Xweb+naPZ8Y", LibNpWebApi2::NpWebApi2GetMemoryPoolStats);
LIB_FUNC("WV1GwM32NgY", LibNpWebApi2::NpWebApi2PushEventCreateHandle);
LIB_FUNC("sk54bi6FtYM", LibNpWebApi2::NpWebApi2CreateUserContext);
LIB_FUNC("9X9+cneTGUU", LibNpWebApi2::NpWebApi2DeleteUserContext);
+4 -2
View File
@@ -163,8 +163,10 @@ static int KYTY_SYSV_ABI SystemServiceGetStatus(SystemServiceStatus* status) {
return SYSTEM_SERVICE_ERROR_PARAMETER;
}
*status = SystemServiceStatus();
status->is_system_ui_overlaid = Dialog::ErrorDialog::GetVisualState().active;
const auto dialog = Dialog::SystemDialog::GetVisualState();
*status = SystemServiceStatus();
status->is_system_ui_overlaid = dialog.active;
status->is_in_background_execution = dialog.background;
return OK;
}
+6
View File
@@ -1343,6 +1343,12 @@ int KYTY_SYSV_ABI NetResolverDestroy(int rid) {
return OK;
}
int KYTY_SYSV_ABI NetResolverAbort(int rid, int flags) {
PRINT_NAME();
LOGF("\t rid = %d\n\t flags = 0x%08x\n", rid, flags);
return OK;
}
int KYTY_SYSV_ABI NetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout,
int retry, int flags) {
PRINT_NAME();
+1
View File
@@ -41,6 +41,7 @@ int KYTY_SYSV_ABI NetPoolCreate(const char* name, int size, int flags);
int KYTY_SYSV_ABI NetPoolDestroy(int memid);
int KYTY_SYSV_ABI NetResolverCreate(const char* name, int memid, int flags);
int KYTY_SYSV_ABI NetResolverDestroy(int rid);
int KYTY_SYSV_ABI NetResolverAbort(int rid, int flags);
int KYTY_SYSV_ABI NetResolverStartNtoa(int rid, const char* hostname, void* addr, int timeout,
int retry, int flags);
int KYTY_SYSV_ABI NetInetPton(int af, const char* src, void* dst);
+4 -1
View File
@@ -135,6 +135,9 @@ namespace Libs::Controller {
int GetActiveControllerId() {
return 0;
}
float GetSettingScale(Setting) {
return 1.0f;
}
namespace DualSenseHaptics {
Stream* Open(uint32_t, bool) {
@@ -145,7 +148,7 @@ void Close(Stream*) {}
bool UsesBluetooth(const Stream* stream) {
return stream != nullptr && pad_bluetooth;
}
uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*) {
uint64_t Queue(Stream* stream, int, const void*, uint32_t, uint32_t, bool, const int*, float) {
return stream != nullptr ? pad_queue_us : 0;
}
} // namespace DualSenseHaptics
+296
View File
@@ -0,0 +1,296 @@
// Exercise the production controller with deterministic host output and time.
#include <SDL3/SDL.h>
#include <array>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <vector>
namespace {
void Check(bool condition, const char* text) {
if (!condition) {
std::fprintf(stderr, "ControllerSettingsTests: %s\n", text);
std::abort();
}
}
struct Rumble {
Uint16 large, small;
Uint32 duration;
};
Uint64 now = 1000;
std::vector<Rumble> rumble;
std::vector<Rumble> haptics;
std::vector<std::array<Uint8, 32>> effects;
bool haptics_handles_rumble = false;
} // namespace
namespace Fake {
Uint64 GetTicks() {
return now;
}
SDL_Gamepad* GetGamepadFromID(SDL_JoystickID id) {
return id == 1 || id == 2 ? reinterpret_cast<SDL_Gamepad*>(static_cast<uintptr_t>(id))
: nullptr;
}
SDL_GamepadType GetGamepadType(SDL_Gamepad*) {
return SDL_GAMEPAD_TYPE_PS5;
}
bool RumbleGamepad(SDL_Gamepad*, Uint16 large, Uint16 small, Uint32 duration) {
rumble.push_back({large, small, duration});
return true;
}
bool SendGamepadEffect(SDL_Gamepad*, const void* data, int size) {
Check(size == 32, "unexpected DualSense effect size");
std::array<Uint8, 32> effect {};
std::memcpy(effect.data(), data, effect.size());
effects.push_back(effect);
return true;
}
bool SetGamepadLED(SDL_Gamepad*, Uint8, Uint8, Uint8) {
return true;
}
bool GamepadHasSensor(SDL_Gamepad*, SDL_SensorType) {
return false;
}
bool SetGamepadSensorEnabled(SDL_Gamepad*, SDL_SensorType, bool) {
return true;
}
void CloseGamepad(SDL_Gamepad*) {}
void Delay(Uint32) {}
} // namespace Fake
#define SDL_GetTicks Fake::GetTicks
#define SDL_GetGamepadFromID Fake::GetGamepadFromID
#define SDL_GetGamepadType Fake::GetGamepadType
#define SDL_RumbleGamepad Fake::RumbleGamepad
#define SDL_SendGamepadEffect Fake::SendGamepadEffect
#define SDL_SetGamepadLED Fake::SetGamepadLED
#define SDL_GamepadHasSensor Fake::GamepadHasSensor
#define SDL_SetGamepadSensorEnabled Fake::SetGamepadSensorEnabled
#define SDL_CloseGamepad Fake::CloseGamepad
#define SDL_Delay Fake::Delay
#include "libs/controller.cpp"
#undef SDL_GetTicks
#undef SDL_GetGamepadFromID
#undef SDL_GetGamepadType
#undef SDL_RumbleGamepad
#undef SDL_SendGamepadEffect
#undef SDL_SetGamepadLED
#undef SDL_GamepadHasSensor
#undef SDL_SetGamepadSensorEnabled
#undef SDL_CloseGamepad
#undef SDL_Delay
namespace Libs::Controller::DualSenseHaptics {
bool SetVibration(int, uint8_t large_motor, uint8_t small_motor, uint32_t duration_ms) {
haptics.push_back({large_motor, small_motor, duration_ms});
return haptics_handles_rumble;
}
void Shutdown() {}
} // namespace Libs::Controller::DualSenseHaptics
namespace Libs::LibKernel {
uint64_t KYTY_SYSV_ABI KernelGetProcessTime() {
return now * 1000;
}
} // namespace Libs::LibKernel
namespace Loader::Timer {
double GetTimeMs() {
return static_cast<double>(now);
}
} // namespace Loader::Timer
namespace {
using namespace Libs::Controller;
struct Controller {
Controller() {
now = 1000;
haptics_handles_rumble = false;
Initialize();
Connect(1);
Check(GetSettingScale(Setting::SpeakerVolume) == 1.0f &&
GetSettingScale(Setting::VibrationIntensity) == 1.0f &&
GetSettingScale(Setting::TriggerEffectIntensity) == 1.0f,
"controller initialization retained old settings");
rumble.clear();
haptics.clear();
effects.clear();
}
~Controller() { Shutdown(); }
};
DualSenseEffects LastEffect() {
Check(!effects.empty(), "missing trigger output");
DualSenseEffects effect {};
std::memcpy(&effect, effects.back().data(), sizeof(effect));
return effect;
}
int ZoneStrength(const uint8_t* effect, int zone) {
const unsigned active = effect[1] | (effect[2] << 8u);
const uint32_t packed = effect[3] | (effect[4] << 8u) | (effect[5] << 16u) |
(static_cast<uint32_t>(effect[6]) << 24u);
return (active & (1u << zone)) != 0 ? 1 + ((packed >> (3 * zone)) & 7u) : 0;
}
void SetRumble(uint8_t large, uint8_t small) {
const PadVibrationParam param {large, small};
Check(PadSetVibration(1, &param) == 0, "vibration request failed");
}
void TestSettingCycles() {
Controller controller;
for (auto setting:
{Setting::SpeakerVolume, Setting::VibrationIntensity, Setting::TriggerEffectIntensity}) {
Check(GetSettingScale(setting) == 1.0f, "initial setting is not strong");
const std::vector<float> levels = setting == Setting::SpeakerVolume
? std::vector<float> {0.0f, 0.02f, 0.12f, 0.42f, 1.0f}
: std::vector<float> {0.0f, 0.33f, 0.66f, 1.0f};
for (int cycle = 0; cycle < 2; ++cycle) {
for (float level: levels) {
CycleSetting(setting);
Check(GetSettingScale(setting) == level, "setting did not cycle or wrap");
}
}
}
CycleSetting(Setting::SpeakerVolume);
CycleSetting(Setting::VibrationIntensity);
CycleSetting(Setting::TriggerEffectIntensity);
}
void TestVibrationLifetime() {
Controller controller;
SetRumble(255, 1);
Check(rumble.size() == 1 && rumble.back().large == 65535 && rumble.back().small == 257 &&
rumble.back().duration == 65535,
"full vibration changed");
now += 100;
CycleSetting(Setting::VibrationIntensity);
Check(rumble.back().large == 0 && rumble.back().small == 0, "muting did not stop vibration");
now += 100;
CycleSetting(Setting::VibrationIntensity);
Check(rumble.back().large == 84 * 257 && rumble.back().small == 257 &&
rumble.back().duration == 65335,
"changing intensity lost a small motor or extended the vibration deadline");
Check(effects.empty(), "vibration intensity resent triggers");
now = 1000 + 65535;
const auto calls = rumble.size();
CycleSetting(Setting::VibrationIntensity);
CycleSetting(Setting::VibrationIntensity);
Check(rumble.size() == calls || (rumble.back().large == 0 && rumble.back().small == 0),
"expired vibration was resurrected");
SetRumble(0, 0);
const auto stopped = rumble.size();
CycleSetting(Setting::VibrationIntensity);
CycleSetting(Setting::VibrationIntensity);
Check(rumble.size() == stopped || (rumble.back().large == 0 && rumble.back().small == 0),
"explicitly stopped vibration was resurrected");
}
void TestMaskedTriggersAndValidation() {
Controller controller;
PadTriggerEffectParam param {};
param.trigger_mask = 3;
param.command[0].mode = 1;
param.command[0].data[0] = 2;
param.command[0].data[1] = 8;
param.command[1].mode = 2;
param.command[1].data[0] = 2;
param.command[1].data[1] = 7;
param.command[1].data[2] = 6;
Check(PadSetTriggerEffect(1, &param) == 0, "initial trigger request failed");
CycleSetting(Setting::TriggerEffectIntensity);
Check(LastEffect().left_trigger[0] == 5 && LastEffect().right_trigger[0] == 5,
"muting did not disable both triggers");
for (uint32_t mode = 1; mode <= 7; ++mode) {
auto invalid = param;
invalid.command[1] = {};
invalid.command[1].mode = mode;
std::memset(invalid.command[1].data, 255, sizeof(invalid.command[1].data));
const auto calls = effects.size();
Check(PadSetTriggerEffect(1, &invalid) == PAD_ERROR_INVALID_ARG && effects.size() == calls,
"muting bypassed trigger validation or sent a partial invalid request");
}
param.trigger_mask = 4;
Check(PadSetTriggerEffect(1, &param) == PAD_ERROR_INVALID_ARG, "invalid trigger mask accepted");
param.trigger_mask = 1;
param.command[0].data[1] = 6;
Check(PadSetTriggerEffect(1, &param) == 0 && LastEffect().enable_bits == 8,
"left-only update touched the right trigger");
CycleSetting(Setting::TriggerEffectIntensity);
auto effect = LastEffect();
Check(effect.enable_bits == 12 && effect.left_trigger[0] == 0x21 &&
ZoneStrength(effect.left_trigger, 1) == 0 &&
ZoneStrength(effect.left_trigger, 2) == 2 && effect.right_trigger[0] == 0x25 &&
effect.right_trigger[1] == 0x84 && effect.right_trigger[3] == 1,
"masked updates lost a cached trigger or failed to scale its strength");
CycleSetting(Setting::TriggerEffectIntensity);
CycleSetting(Setting::TriggerEffectIntensity);
effect = LastEffect();
Check(ZoneStrength(effect.left_trigger, 2) == 6 && effect.right_trigger[3] == 5,
"restoring strong intensity did not recover original trigger strengths");
Check(rumble.empty() && haptics.empty(), "trigger intensity resent vibration");
}
void TestIndependentOutputsAndPadSwitch() {
Controller controller;
haptics_handles_rumble = true;
SetRumble(200, 100);
now += 100;
CycleSetting(Setting::VibrationIntensity);
CycleSetting(Setting::VibrationIntensity);
Check(rumble.empty() && haptics.back().large == 66 && haptics.back().small == 33 &&
haptics.back().duration == 65435,
"haptics rumble did not receive scaled motors and remaining duration");
PadTriggerEffectParam param {};
param.trigger_mask = 1;
param.command[0].mode = 1;
param.command[0].data[1] = 8;
Check(PadSetTriggerEffect(1, &param) == 0, "trigger request failed");
const auto vibration_calls = haptics.size();
const auto trigger_calls = effects.size();
CycleSetting(Setting::SpeakerVolume);
Check(haptics.size() == vibration_calls && effects.size() == trigger_calls,
"speaker volume resent controller effects");
Connect(2);
Disconnect(1);
Check(GetActiveControllerId() == 2, "active controller did not change");
haptics.clear();
effects.clear();
CycleSetting(Setting::VibrationIntensity);
CycleSetting(Setting::TriggerEffectIntensity);
CycleSetting(Setting::TriggerEffectIntensity);
Check(effects.empty(), "previous controller's trigger effects leaked to new pad");
for (const auto& request: haptics) {
Check(request.large == 0 && request.small == 0,
"previous controller's vibration leaked to new pad");
}
SetRumble(200, 100);
Check(PadSetTriggerEffect(1, &param) == 0, "replacement pad trigger request failed");
EmergencyShutdown();
Check(GetActiveControllerId() == -1, "released controller remained active");
haptics.clear();
effects.clear();
CycleSetting(Setting::VibrationIntensity);
CycleSetting(Setting::TriggerEffectIntensity);
Check(effects.empty(), "released controller received a trigger request");
for (const auto& request: haptics) {
Check(request.large == 0 && request.small == 0,
"released controller retained a cached vibration");
}
}
} // namespace
int main() {
Config::Initialize();
TestSettingCycles();
// Each following test initializes another controller and requires strong defaults.
TestVibrationLifetime();
TestMaskedTriggersAndValidation();
TestIndependentOutputsAndPadSwitch();
Config::Shutdown();
return 0;
}
+65
View File
@@ -3,6 +3,7 @@
#include <algorithm>
#include <atomic>
#include <chrono>
#include <cstdio>
#include <cstdlib>
#include <memory>
@@ -27,6 +28,69 @@ void CheckConcurrentResult(int result, const char* text) {
Check(result == OK || result == KERNEL_ERROR_EBADF || result == KERNEL_ERROR_ENOENT, text);
}
void TestPeriodicTimerEvents() {
using namespace std::chrono_literals;
using Libs::LibKernel::KERNEL_ERROR_ETIMEDOUT;
Check(EventQueue::KernelAddTimerEvent(EventQueue::KERNEL_EQUEUE_INVALID, 10, 1000, nullptr) ==
KERNEL_ERROR_EBADF,
"timer rejects invalid queue");
EventQueue::KernelEqueue queue = EventQueue::KERNEL_EQUEUE_INVALID;
Check(EventQueue::KernelCreateEqueue(&queue, "periodic-timer") == OK, "create timer queue");
auto* user_data = reinterpret_cast<void*>(0x1234);
Check(EventQueue::KernelAddTimerEvent(queue, 10, 1000, user_data) == OK, "add periodic timer");
std::this_thread::sleep_for(5ms);
EventQueue::KernelEvent events[2] {};
int out = 0;
Libs::LibKernel::KernelUseconds timeout = 0;
Check(EventQueue::KernelWaitEqueue(queue, events, 2, &out, &timeout) == OK && out == 1,
"periodic occurrences are aggregated into one event");
Check(events[0].ident == 10 && events[0].filter == EventQueue::KERNEL_EVFILT_TIMER &&
events[0].data >= 5 && events[0].udata == user_data,
"timer reports identity, userdata and elapsed occurrence count");
timeout = 100000;
Check(EventQueue::KernelWaitEqueue(queue, events, 2, &out, &timeout) == OK && out == 1 &&
events[0].data >= 1,
"periodic timer remains registered after delivery");
std::this_thread::sleep_for(5ms);
user_data = reinterpret_cast<void*>(0x5678);
Check(EventQueue::KernelAddTimerEvent(queue, 10, 3000000000u, user_data) == OK,
"update periodic timer interval");
timeout = 0;
Check(EventQueue::KernelWaitEqueue(queue, events, 2, &out, &timeout) == OK && out == 1 &&
events[0].data >= 5 && events[0].udata == user_data,
"timer update preserves accumulated occurrences and updates userdata");
Check(EventQueue::KernelWaitEqueue(queue, events, 2, &out, &timeout) == KERNEL_ERROR_ETIMEDOUT,
"timer update replaces the repeating interval");
Check(EventQueue::KernelDeleteEvent(queue, 10, EventQueue::KERNEL_EVFILT_TIMER) == OK,
"delete periodic timer");
Check(EventQueue::KernelAddTimerEvent(queue, 10, 0, user_data) == OK,
"add immediate periodic timer");
for (int i = 0; i < 2; ++i) {
Check(EventQueue::KernelAddTimerEvent(queue, 10, 0, user_data) == OK,
"update pending immediate timer");
}
for (int i = 0; i < 2; ++i) {
Check(EventQueue::KernelWaitEqueue(queue, events, 2, &out, &timeout) == OK && out == 1 &&
events[0].data == 1,
"zero interval remains immediately ready after delivery");
}
Check(EventQueue::KernelDeleteEvent(queue, 10, EventQueue::KERNEL_EVFILT_TIMER) == OK,
"delete immediate timer");
const Libs::LibKernel::KernelTimespec delay {0, 1000000};
Check(EventQueue::KernelAddHRTimerEvent(queue, 10, &delay, user_data) == OK,
"add high resolution timer");
timeout = 100000;
Check(EventQueue::KernelWaitEqueue(queue, events, 2, &out, &timeout) == OK && out == 1 &&
events[0].filter == EventQueue::KERNEL_EVFILT_HRTIMER && events[0].udata == user_data,
"shared timer scheduling delivers high resolution event");
Check(EventQueue::KernelDeleteHRTimerEvent(queue, 10) == KERNEL_ERROR_ENOENT,
"high resolution timer is removed after one delivery");
Check(EventQueue::KernelDeleteEqueue(queue) == OK, "delete timer queue");
}
void CountDeletedEvent(EventQueue::KernelEqueue, EventQueue::KernelEqueueEvent* event) {
auto* count = static_cast<std::atomic_uint32_t*>(event->filter.data);
count->fetch_add(1, std::memory_order_relaxed);
@@ -431,6 +495,7 @@ void TestConcurrentDelete() {
} // namespace
int main() {
TestPeriodicTimerEvents();
TestDuplicateAddPreservesEventState();
TestCallbackStateOutlivesPort();
TestCallbackOwnsPayload();
+123
View File
@@ -35,6 +35,10 @@ namespace Libs::LibKernelApr {
void InitLibKernel_1_Apr(Loader::SymbolDatabase *symbols);
}
namespace Libs {
void InitLibKernel_1(Loader::SymbolDatabase *symbols);
}
namespace Libs::LibAmpr {
void InitAmpr_1(Loader::SymbolDatabase *symbols);
}
@@ -43,6 +47,10 @@ namespace Libs::LibNet {
void InitNet_1_Net(Loader::SymbolDatabase *symbols);
}
namespace Libs::LibNpWebApi2 {
void InitNet_1_NpWebApi2(Loader::SymbolDatabase *symbols);
}
namespace {
namespace FileSystem = Libs::LibKernel::FileSystem;
@@ -132,6 +140,119 @@ void TestSaveOpenVisibility() {
Check(FileSystem::KernelClose(truncated) == OK, "close truncated save file");
}
void TestAioBatches() {
namespace Kernel = Libs::LibKernel;
Loader::SymbolDatabase symbols;
Libs::InitLibKernel_1(&symbols);
const auto find = [&](const char *nid) {
const auto *symbol = symbols.FindByNid(nid, Loader::SymbolType::Func);
Check(symbol != nullptr, "AIO exports resolve");
return symbol->vaddr;
};
struct Result { int64_t return_value; uint32_t state; };
struct Request {
int64_t offset;
size_t size;
void *buffer;
Result *result;
int32_t fd;
};
using Submit = int (KYTY_SYSV_ABI *)(Request *, int32_t, int32_t, int32_t *);
using Batch = int (KYTY_SYSV_ABI *)(int32_t *, int32_t, int32_t *);
using Single = int (KYTY_SYSV_ABI *)(int32_t, int32_t *);
using Wait = int (KYTY_SYSV_ABI *)(int32_t, int32_t *, uint32_t *);
const auto submit = reinterpret_cast<Submit>(find("HgX7+AORI58"));
const auto poll = reinterpret_cast<Batch>(find("o7O4z3jwKzo"));
const auto erase = reinterpret_cast<Batch>(find("Ft3EtsZzAoY"));
const auto poll_one = reinterpret_cast<Single>(find("2pOuoWoCxdk"));
const auto erase_one = reinterpret_cast<Single>(find("5TgME6AYty4"));
const auto wait = reinterpret_cast<Wait>(find("KOF-oJbQVvc"));
constexpr char Payload[] = "AIO payload";
const int fd = FileSystem::KernelOpen("/savedata0/aio.dat", 0x602, 0777);
Check(fd >= 3 && FileSystem::KernelWrite(fd, Payload, sizeof(Payload)) == sizeof(Payload),
"create AIO read fixture");
std::array<int32_t, 3> ids {};
std::array<char, 4> buffer {};
Result result {};
Request request {2, buffer.size(), buffer.data(), &result, fd};
for (int i = 0; i < 2; ++i) {
Check(submit(&request, 1, 2, &ids[i]) == OK &&
result.return_value == buffer.size() && result.state == 3 &&
std::memcmp(buffer.data(), Payload + 2, buffer.size()) == 0,
"AIO submission reads bytes at the requested offset");
}
ids[2] = -1;
std::array<int32_t, 3> states {-1, -1, -1};
Check(poll(ids.data(), ids.size(), states.data()) == OK &&
states[0] == 3 && states[1] == 3 && states[2] == Kernel::KERNEL_ERROR_ESRCH,
"batch poll writes every state and per-request invalid-ID error");
Check(poll_one(ids[0], &states[0]) == OK && states[0] == (3 | 0x10000) &&
poll(ids.data(), 2, states.data()) == OK &&
states[0] == (3 | 0x10000) && states[1] == (3 | 0x10000) &&
wait(ids[0], &states[0], nullptr) == OK && states[0] == (3 | 0x10000),
"single and batch polls and waits share completion notification state");
Check(erase(ids.data(), ids.size(), states.data()) == OK &&
states[0] == OK && states[1] == OK && states[2] == Kernel::KERNEL_ERROR_ESRCH,
"batch deletion writes per-request results");
Check(poll_one(ids[0], &states[0]) == OK && states[0] == Kernel::KERNEL_ERROR_ESRCH &&
erase_one(ids[1], &states[1]) == OK && states[1] == Kernel::KERNEL_ERROR_ESRCH,
"deleted AIO IDs are invalid for single-request APIs");
for (const auto batch : {poll, erase}) {
states.fill(42);
Check(batch(nullptr, 1, states.data()) == Kernel::KERNEL_ERROR_EFAULT &&
batch(ids.data(), 1, nullptr) == Kernel::KERNEL_ERROR_EFAULT &&
batch(ids.data(), 0, states.data()) == Kernel::KERNEL_ERROR_EINVAL &&
batch(ids.data(), 129, states.data()) == Kernel::KERNEL_ERROR_EINVAL &&
states == std::array<int32_t, 3> {42, 42, 42},
"invalid batch arguments do not modify outputs");
std::array<int32_t, 128> invalid_ids {};
std::array<int32_t, 128> errors {};
Check(batch(invalid_ids.data(), invalid_ids.size(), errors.data()) == OK &&
std::all_of(errors.begin(), errors.end(), [](int32_t error) {
return error == Kernel::KERNEL_ERROR_ESRCH;
}),
"maximum-sized batch returns all invalid-ID errors");
}
Check(FileSystem::KernelClose(fd) == OK, "close AIO read fixture");
}
void TestNpWebApi2Memory() {
Loader::SymbolDatabase symbols;
Libs::LibNpWebApi2::InitNet_1_NpWebApi2(&symbols);
const auto find = [&](const char *nid) {
const auto *symbol = symbols.FindByNid(nid, Loader::SymbolType::Func);
Check(symbol != nullptr, "NpWebApi2 library and memory exports resolve");
return symbol->vaddr;
};
struct Stats { size_t pool, maximum, current; int32_t reserved; };
static_assert(sizeof(Stats) == 32 && offsetof(Stats, reserved) == 24);
using Initialize = int (KYTY_SYSV_ABI *)(int, size_t);
using GetStats = int (KYTY_SYSV_ABI *)(int, Stats *);
using Terminate = int (KYTY_SYSV_ABI *)(int);
const auto initialize = reinterpret_cast<Initialize>(find("+o9816YQhqQ"));
const auto get_stats = reinterpret_cast<GetStats>(find("Xweb+naPZ8Y"));
const auto terminate = reinterpret_cast<Terminate>(find("bEvXpcEk200"));
const int first = initialize(1, 65537);
const int second = initialize(1, 16384);
Stats stats {1, 2, 3, 4};
Check(first > 0 && second > 0 && first != second &&
get_stats(first, &stats) == OK && stats.pool == 81920 &&
stats.maximum == 0 && stats.current == 0 && stats.reserved == 0 &&
get_stats(second, &stats) == OK && stats.pool == 16384,
"NpWebApi2 statistics retain each context's rounded pool capacity");
Check(get_stats(first, nullptr) == static_cast<int32_t>(0x80553402) &&
get_stats(0, &stats) == static_cast<int32_t>(0x80553403) &&
initialize(1, std::numeric_limits<size_t>::max()) == static_cast<int32_t>(0x80553402),
"NpWebApi2 statistics validate output, context ID, and pool rounding");
Check(terminate(first) == OK &&
get_stats(first, &stats) == static_cast<int32_t>(0x80553404) &&
terminate(first) == static_cast<int32_t>(0x80553404) &&
terminate(-1) == static_cast<int32_t>(0x80553403) &&
get_stats(second, &stats) == OK && stats.pool == 16384 &&
terminate(second) == OK,
"NpWebApi2 termination removes only the selected library context");
}
void CheckMountRoot(const std::filesystem::path &root) {
Common::File cache;
Check(cache.Create(root / "rpf.cache"), "create directory listing fixture");
@@ -836,10 +957,12 @@ int main(int, char**) {
CheckAprPaths(temporary.Path());
FileSystem::Mount(temporary.Path(), "/savedata0");
TestSaveOpenVisibility();
TestAioBatches();
CheckSaveRename(temporary.Path(), "first-save");
CheckSaveRename(temporary.Path(), "replacement-save");
FileSystem::Shutdown();
CheckSocketWakeup();
TestNpWebApi2Memory();
graphics.reset();
subsystems.Destroy();
+99
View File
@@ -402,6 +402,100 @@ void TestCpuDirtyUpload() {
Release(memory);
}
void TestCleanUploadPreservesOwnership() {
constexpr auto page_size = Libs::Graphics::TRACKER_PAGE_SIZE;
constexpr auto region_size = Libs::Graphics::TRACKER_REGION_SIZE;
TrackerHarness harness;
auto &tracker = harness.tracker;
auto *memory = AllocateFixedGuestRange(region_size * 2, region_size);
const auto address = reinterpret_cast<uint64_t>(memory);
tracker.ForEachUploadRange(address, region_size * 2, false,
[](uint64_t, uint64_t) noexcept {},
[]() noexcept {});
for (const auto start : {address + 63 * page_size,
address + region_size - page_size}) {
const auto before = start - page_size;
const auto after = start + 2 * page_size;
tracker.MarkRegionAsCpuModified(before, page_size);
tracker.MarkRegionAsCpuModified(after, page_size);
uint32_t ranges = 0;
uint32_t completions = 0;
ResetProtectionLog();
tracker.ForEachUploadRange(
start, 2 * page_size, false,
[&](uint64_t, uint64_t) noexcept { ranges++; },
[&]() noexcept { completions++; });
Check(ranges == 0 && completions == 1 && g_protection_calls == 0 &&
!tracker.IsRegionCpuModified(start, 2 * page_size),
"clean read-only upload changed dirty state or protection");
tracker.ForEachUploadRange(
start, 2 * page_size, true,
[&](uint64_t, uint64_t) noexcept { ranges++; },
[&]() noexcept { completions++; });
Check(ranges == 0 && completions == 2 &&
tracker.IsRegionGpuModified(start, page_size) &&
tracker.IsRegionGpuModified(start + page_size, page_size) &&
Protection(reinterpret_cast<void *>(start)) == PAGE_NOACCESS &&
Protection(reinterpret_cast<void *>(start + page_size)) ==
PAGE_NOACCESS &&
tracker.IsRegionCpuModified(before, page_size) &&
tracker.IsRegionCpuModified(after, page_size) &&
!tracker.IsRegionGpuModified(before, page_size) &&
!tracker.IsRegionGpuModified(after, page_size) &&
IsWritable(reinterpret_cast<void *>(before)) &&
IsWritable(reinterpret_cast<void *>(after)),
"clean written upload lost GPU ownership or changed dirty neighbors");
tracker.UnmarkRegionAsGpuModified(start, 2 * page_size);
}
tracker.UntrackMemory(address, region_size * 2);
Release(memory);
}
void BenchmarkCleanUploads() {
constexpr uint64_t size = 256ull * 1024 * 1024;
TrackerHarness harness;
auto &tracker = harness.tracker;
auto *memory = AllocateFixedGuestRange(size, 0);
const auto address = reinterpret_cast<uint64_t>(memory);
tracker.ForEachUploadRange(address, size, false,
[](uint64_t, uint64_t) noexcept {},
[]() noexcept {});
std::puts("chunk_bytes,sweeps,calls,elapsed_ns,ns_per_sweep,ns_per_call");
for (const uint64_t chunk : {size, Libs::Graphics::TRACKER_REGION_SIZE,
uint64_t{64 * 1024}}) {
uint64_t ranges = 0;
uint64_t completions = 0;
uint64_t sweeps = 0;
ResetProtectionLog();
const auto start = std::chrono::steady_clock::now();
std::chrono::nanoseconds elapsed{};
do {
for (uint64_t offset = 0; offset < size; offset += chunk) {
tracker.ForEachUploadRange(
address + offset, chunk, false,
[&](uint64_t, uint64_t) noexcept { ranges++; },
[&]() noexcept { completions++; });
}
sweeps++;
elapsed = std::chrono::steady_clock::now() - start;
} while (elapsed < std::chrono::milliseconds(250));
Check(ranges == 0 && completions == sweeps * (size / chunk) &&
g_protection_calls == 0,
"clean upload benchmark changed ranges, completion, or protection");
std::printf("%llu,%llu,%llu,%lld,%.2f,%.2f\n",
static_cast<unsigned long long>(chunk),
static_cast<unsigned long long>(sweeps),
static_cast<unsigned long long>(completions),
static_cast<long long>(elapsed.count()),
static_cast<double>(elapsed.count()) / sweeps,
static_cast<double>(elapsed.count()) / completions);
}
tracker.UntrackMemory(address, size);
Release(memory);
}
void TestRangeInvalidation() {
TrackerHarness harness;
auto &tracker = harness.tracker;
@@ -1027,11 +1121,16 @@ int main(int argc, char **argv) {
if (argc == 3 && std::strcmp(argv[1], "--death") == 0) {
RunDeathCase(argv[2]);
}
if (argc == 2 && std::strcmp(argv[1], "--benchmark-clean-upload") == 0) {
BenchmarkCleanUploads();
return 0;
}
TestGuestRange();
TestRangeSet();
TestQueriesDoNotRequireMappedOwnership();
TestConcurrentRegionPublication();
TestCpuDirtyUpload();
TestCleanUploadPreservesOwnership();
TestRangeInvalidation();
TestGpuReacquisitionAfterInvalidation();
TestGpuDirtyBits();
+122 -6
View File
@@ -56,6 +56,7 @@ std::binary_semaphore write_started {0}, allow_write {0};
SDL_AudioStream* default_stream = nullptr;
bool fail_default_resume = false;
int active_controller = 1;
float speaker_scale = 1.0f, vibration_scale = 1.0f;
} // namespace
namespace Fake {
@@ -325,6 +326,9 @@ namespace Libs::Controller {
int GetActiveControllerId() {
return active_controller;
}
float GetSettingScale(Setting setting) {
return setting == Setting::SpeakerVolume ? speaker_scale : vibration_scale;
}
} // namespace Libs::Controller
namespace Libs::LibKernel {
@@ -364,6 +368,7 @@ struct Fixture {
default_stream = nullptr;
fail_default_resume = false;
active_controller = 1;
speaker_scale = vibration_scale = 1.0f;
now = 1000;
opens = rumble_calls = 0;
fail_open = fail_resume = false;
@@ -381,8 +386,8 @@ Port Open(bool speaker = false, uint32_t freq = 48000) {
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);
bool is_float = true, const int* volume = unity.data(), float gain = 1.0f) {
Haptics::Queue(port.get(), 1, data, frames, channels, is_float, volume, gain);
}
void Pull() {
std::array<float, 64> buffer {};
@@ -412,6 +417,8 @@ void TestFormatsAndVolume() {
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});
Queue(port, mono.data(), 2, 1, false, volume.data() + 1, 0.5f);
ExpectPcm({0, 0, 0.125f, 0.125f, 0, 0, -0.25f, -0.25f});
std::array<float, 24> multichannel {};
multichannel.fill(0.9f);
multichannel[0] = 0.1f;
@@ -561,6 +568,33 @@ void TestRumbleLeaseAndDuration() {
Check(rumble_calls == calls, "audio pull touched rumble after shutdown");
}
void TestScaledRumbleDuration() {
for (bool bluetooth: {false, true}) {
Fixture f;
wireless = hid_available = bluetooth;
auto port = Open();
std::vector<float> sound(2048, 0.5f);
Haptics::SetVibration(1, 100, 50, 1000);
Queue(port, sound.data(), 1024, 2, true, unity.data(), 0.0f);
Check(rumble.large == 100 * 0x101, "muted haptics suppressed motor rumble");
Queue(port, sound.data(), 1024, 2, true, unity.data(), 0.5f);
Check(rumble.large == 0, "scaled haptics did not suppress motor rumble");
now += 100;
Haptics::SetVibration(1, 50, 25, 900);
now += 200;
if (bluetooth) {
Haptics::UpdateBluetoothRumble();
} else {
Pull();
}
Check(rumble.large == 50 * 0x101 && rumble.small == 25 * 0x101 && rumble.duration == 700,
"scaled rumble lost its strength or extended its original expiry");
Haptics::SetVibration(1, 50, 25, 0);
Check(rumble.large == 0 && rumble.small == 0 && rumble.duration == 0,
"expired rumble was restarted");
}
}
void TestSwitchStopsOldRumble() {
Fixture f;
auto port = Open();
@@ -732,12 +766,12 @@ void TestBluetoothFormatsAndRoutingFailure() {
now += 2001;
std::vector<int16_t> mono(1024, 16384);
const int volume = 16384;
Queue(speaker, mono.data(), 1024, 1, false, &volume);
Queue(speaker, mono.data(), 1024, 1, false, &volume, 0.5f);
auto pending = Bluetooth::Prepare(now * 1000000);
Check(pending.size() == 1 && hid_opens == 2 && effects[1][7] == 0x30 &&
std::abs(pending[0].audio[200] - 0.25f) < 1e-4f &&
std::abs(pending[0].audio[201] - 0.25f) < 1e-4f,
"Bluetooth mono int16 volume mapping or speaker routing recovery failed");
std::abs(pending[0].audio[200] - 0.125f) < 1e-4f &&
std::abs(pending[0].audio[201] - 0.125f) < 1e-4f,
"Bluetooth mono int16 volume/gain mapping or speaker routing recovery failed");
}
void TestBluetoothEncodingDoesNotBlockAudioQueue() {
@@ -1103,6 +1137,82 @@ void TestAudioSpeakerRouting() {
SDL_GetAudioStreamQueued(default_stream) == 0,
"successful DualSense playback also queued audio on the main output");
ExpectPcm({0.5f, 0.5f, 0, 0, 0.5f, 0.5f, 0, 0});
speaker_scale = 0.5f;
audio.AudioOutOutputs(&output, 1, false);
ExpectPcm({0.25f, 0.25f, 0, 0, 0.25f, 0.25f, 0, 0});
speaker_scale = 0.0f;
audio.AudioOutOutputs(&output, 1, false);
ExpectPcm({});
active_controller = 2;
audio.AudioOutOutputs(&output, 1, false);
std::array<float, 4> fallback {};
Check(SDL_GetAudioStreamData(default_stream, fallback.data(), sizeof(fallback)) ==
sizeof(fallback) &&
fallback == std::array<float, 4> {},
"unplugging the controller unmuted its speaker fallback");
}
void TestAudioFallbackGain() {
for (bool is_float: {false, true}) {
Fixture f;
Libs::Audio::Audio audio;
active_controller = 2;
speaker_scale = 0.5f;
const std::array<int16_t, 4> integer_pcm {16384, 16384, 16384, 16384};
const auto format = is_float ? Libs::Audio::Audio::Format::FloatStereo
: Libs::Audio::Audio::Format::Signed16bitStereo;
const void* data = is_float ? static_cast<const void*>(pcm.data()) : integer_pcm.data();
for (int type: {0, 4}) {
const auto port = audio.AudioOutOpen(type, 2, 48000, format);
const std::array<int, 2> volume {16384, 16384};
audio.AudioOutSetVolume(port, 3, volume.data());
Libs::Audio::Audio::OutputParam output {port, data};
audio.AudioOutOutputs(&output, 1, false);
const float expected = type == 4 ? 0.125f : 0.25f;
std::array<float, 4> actual {};
if (is_float) {
Check(SDL_GetAudioStreamData(default_stream, actual.data(), sizeof(actual)) ==
sizeof(actual),
"float fallback output is missing");
} else {
std::array<int16_t, 4> integers {};
Check(SDL_GetAudioStreamData(default_stream, integers.data(), sizeof(integers)) ==
sizeof(integers),
"integer fallback output is missing");
std::transform(integers.begin(), integers.end(), actual.begin(),
[](int16_t value) { return value / 32768.0f; });
}
Check(std::all_of(actual.begin(), actual.end(),
[expected](float value) { return value == expected; }),
"speaker setting gain was lost or applied to ordinary audio");
audio.AudioOutClose(port);
}
}
}
void TestAudioVibrationGain() {
for (bool bluetooth: {false, true}) {
Fixture f;
wireless = hid_available = bluetooth;
Libs::Audio::Audio audio;
vibration_scale = 0.5f;
speaker_scale = 0.0f;
const auto port =
audio.AudioOutOpen(10, 1024, 48000, Libs::Audio::Audio::Format::FloatStereo);
std::vector<float> sound(2048, 0.5f);
Libs::Audio::Audio::OutputParam output {port, sound.data()};
audio.AudioOutOutputs(&output, 1, false);
Check(default_stream == nullptr, "vibration port opened ordinary audio");
if (bluetooth) {
Check(Bluetooth::Prepare(now * 1000000).empty(),
"Bluetooth vibration skipped its coalescing window");
const auto pending = Bluetooth::Prepare((now + 1) * 1000000);
Check(pending.size() == 1 && pending[0].data[76] == 32,
"Bluetooth vibration did not apply its own setting gain");
} else {
ExpectPcm({0, 0, 0.25f, 0.25f, 0, 0, 0.25f, 0.25f});
}
}
}
void TestBluetoothAudioSpeakerRouting() {
@@ -1113,12 +1223,15 @@ void TestBluetoothAudioSpeakerRouting() {
const auto port = audio.AudioOutOpen(4, 1024, 48000, Libs::Audio::Audio::Format::FloatStereo);
Check(port.IsValid() && default_stream != nullptr, "Bluetooth pad speaker port did not open");
std::vector<float> sound(2048, 0.25f);
speaker_scale = 0.5f;
Libs::Audio::Audio::OutputParam output {port, sound.data()};
Check(audio.AudioOutOutputs(&output, 1, false) == 1024 && hid_opens == 1 &&
SDL_GetAudioStreamQueued(default_stream) == 0,
"Bluetooth pad speaker also played on the host output");
fail_hid_write = true;
auto pending = Libs::Controller::DualSenseBluetooth::Prepare(now * 1000000);
Check(pending.size() == 1 && std::abs(pending[0].audio[200] - 0.125f) < 1e-4f,
"Bluetooth speaker did not apply its setting gain");
Libs::Controller::DualSenseBluetooth::Send(pending);
Check(audio.AudioOutOutputs(&output, 1, false) == 1024 &&
SDL_GetAudioStreamQueued(default_stream) > 0,
@@ -1144,6 +1257,8 @@ int main() {
SDL_SetHint(SDL_HINT_AUDIO_DRIVER, "dummy");
TestAudioSpeakerFallback();
TestAudioSpeakerRouting();
TestAudioFallbackGain();
TestAudioVibrationGain();
TestBluetoothAudioSpeakerRouting();
TestAudioDefaultResumeFailure();
TestFormatsAndVolume();
@@ -1173,6 +1288,7 @@ int main() {
TestUsbAndBluetoothFeatureParity();
TestFailuresAndBoundedQueue();
TestRumbleLeaseAndDuration();
TestScaledRumbleDuration();
TestSwitchStopsOldRumble();
TestCloseRestoresRumble();
std::printf("PadHapticsTests: all cases passed\n");
+136 -19
View File
@@ -95,29 +95,33 @@ Libs::Graphics::ShaderRecompiler::IR::ResourcePlan UserDataBufferPlan() {
return ExtractResourcePlan(program);
}
Libs::Graphics::ShaderRecompiler::IR::ResourcePlan MixedSamplerPlan() {
Libs::Graphics::ShaderRecompiler::IR::Program MixedSamplerProgram() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
program.srt_plan_complete = true;
program.resource_tracking_complete = true;
AddValueBlock(program);
auto &block = AddValueBlock(program);
const auto AddSource = [&program](uint32_t dword_count, uint32_t first) {
const auto AddSource = [&program](uint32_t dword_count) {
DescriptorSource source;
source.dword_count = dword_count;
source.dwords[0] = Value(first);
for (uint32_t i = 1; i < dword_count; i++) {
for (uint32_t i = 0; i < dword_count; i++) {
source.dwords[i] = Value(0u);
}
program.descriptor_sources.push_back(source);
return static_cast<uint32_t>(program.descriptor_sources.size() - 1u);
};
const auto image0 = AddSource(8, 0);
const auto image1 = AddSource(8, 0);
const auto sampler0 = AddSource(4, 0x11111111u);
const auto sampler1 = AddSource(4, 0x22222222u);
const auto image0 = AddSource(8);
const auto image1 = AddSource(8);
const auto sampler0 = AddSource(4);
const auto sampler1 = AddSource(4);
for (uint32_t index = 0; index < 2; ++index) {
auto &value = block.AppendNewInst(
ValueOpcode::GetUserData, {Value(static_cast<ScalarReg>(index))});
program.descriptor_sources[sampler0 + index].dwords[0] = Value(&value);
}
program.info.images.push_back(
{.source = image0,
.resource_class = ImageResourceClass::Sampled,
@@ -137,7 +141,7 @@ Libs::Graphics::ShaderRecompiler::IR::ResourcePlan MixedSamplerPlan() {
program.info.sampled_pairs.push_back({.image = 0, .sampler = 0});
program.info.sampled_pairs.push_back({.image = 0, .sampler = 1});
program.info.sampled_pairs.push_back({.image = 1, .sampler = 1});
return ExtractResourcePlan(program);
return program;
}
void TestMappedSrtUsesDirectReaderByDefault() {
@@ -278,18 +282,130 @@ void TestFailedMaterializationRejectsStage() {
"missing runtime user data did not reject the cached stage");
}
void TestMixedSamplerDuplicatesTheCorrectSnapshot() {
void TestFiniteImageRefreshReusesScalarReads() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
auto plan = MixedSamplerPlan();
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
program.srt_plan_complete = true;
program.resource_tracking_complete = true;
auto &block = AddValueBlock(program);
program.memory_info.push_back({.kind = ResourceKind::ScalarAddress});
auto &handle = block.AppendNewInst(ValueOpcode::GetAddressResource,
{Value(0x1000u), Value(0u)});
auto &srt = block.AppendNewInst(ValueOpcode::GetSrtResource);
for (uint32_t index = 0; index < 3; ++index) {
auto &read = block.AppendNewInst(ValueOpcode::LoadAddressU32,
{Value(&handle), Value(index * 4u), Value(0u), Value(true)});
read.SetFlags(MemoryFlags{.index = 0});
program.srt_reads.push_back({Value(&read), index});
auto &flat = block.AppendNewInst(ValueOpcode::ReadConst,
{Value(&srt), Value(index)});
DescriptorSource source;
source.dword_count = 8;
source.dwords.fill(Value(0u));
source.dwords[0] = Value(&flat);
source.dwords[1] = Value(static_cast<uint32_t>(
Libs::Graphics::Prospero::BufferFormat::k32_32_32_32Float) << 20u);
source.dwords[3] = Value(Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u));
program.descriptor_sources.push_back(source);
}
DescriptorSource root;
root.dword_count = 8;
root.dwords.fill(Value(0u));
root.indirect_image.emplace(DescriptorSource::IndirectImage{}).sources = {0, 1, 2, 1};
program.descriptor_sources.push_back(root);
program.info.images.push_back({
.source = 3,
.resource_class = ImageResourceClass::Sampled,
.numeric_class = Libs::Graphics::Prospero::TextureNumericClass::Float,
.dimension = Libs::Graphics::ShaderRecompiler::Decoder::ImageDimension::Dim2D});
auto plan = ExtractResourcePlan(program);
struct Reads {
std::array<uint32_t, 3> words{0x100u, 0x200u, 0x200u};
std::array<uint32_t, 3> counts{};
uint32_t ordinary = 0;
} reads;
const SrtRuntime runtime{
.read_memory = +[](void *data, uint64_t, std::span<uint32_t>) {
++static_cast<Reads *>(data)->ordinary;
return false;
},
.userdata = &reads,
.read_specialization_memory = +[](void *data, uint64_t address,
std::span<uint32_t> words) {
if (words.size() != 1 || address < 0x1000u || address >= 0x100cu ||
(address & 3u) != 0) return false;
auto &reads = *static_cast<Reads *>(data);
const auto index = (address - 0x1000u) / 4u;
++reads.counts[index];
words[0] = reads.words[index];
return true;
}};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, {}, snapshot, specialization),
const auto capacities = [&] {
return std::array{snapshot.images.capacity(), snapshot.flattened_srt.capacity(),
snapshot.specialization_reads.capacity(), specialization.images.capacity()};
};
const auto check_mapping = [&](std::array<uint32_t, 4> ordinals) {
const auto offset = specialization.images[0].indirect_mapping_offset;
Check(snapshot.images.size() == 2 && snapshot.flattened_srt[offset] == 4,
"finite image candidates were not deduplicated");
for (uint32_t key = 0; key < ordinals.size(); ++key) {
Check(snapshot.flattened_srt[offset + 1u + key * 2u] == key &&
snapshot.flattened_srt[offset + 2u + key * 2u] == ordinals[key],
"finite image selector mapping is stale or incorrect");
}
};
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"finite image materialization failed");
Check(reads.ordinary == 0 && reads.counts == std::array<uint32_t, 3>{1, 1, 1} &&
snapshot.specialization_reads.size() == 3,
"finite image candidates repeated scalar reads or bypassed clean provenance");
check_mapping({0, 1, 1, 1});
const auto warm_capacities = capacities();
reads.words = {0x300u, 0x300u, 0x400u};
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"finite image refresh failed after descriptor changes");
Check(reads.ordinary == 0 && reads.counts == std::array<uint32_t, 3>{2, 2, 2} &&
snapshot.specialization_reads.size() == 3 &&
snapshot.images[0].dwords[0] == 0x300u && snapshot.images[1].dwords[0] == 0x400u,
"finite image refresh retained old scalar values or repeated reads");
check_mapping({0, 0, 1, 0});
Check(capacities() == warm_capacities,
"finite image refresh grew reusable resource storage after warmup");
}
void TestMixedSamplerVariantsShareRuntimeDescriptor() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
auto program = MixedSamplerProgram();
auto plan = ExtractResourcePlan(program);
std::array<uint32_t, 2> user_data{0x11111111u, 0x22222222u};
const SrtRuntime runtime{.user_data = user_data};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization),
"mixed sampler materialization failed");
Check(snapshot.samplers.size() == 3,
"mixed sampler materialization appended unrelated samplers");
Check(snapshot.samplers[2] == snapshot.samplers[1] &&
snapshot.samplers[2] != snapshot.samplers[0],
"point sampler variant duplicated the wrong runtime descriptor");
ApplyResourceSpecialization(program, specialization);
const auto &samplers = program.info.samplers;
Check(snapshot.samplers.size() == 2 && samplers.size() == 3 &&
samplers[0].snapshot_index == 0 && samplers[1].snapshot_index == 1 &&
samplers[2].snapshot_index == 1 &&
samplers[0].source == plan.info.samplers[0].source &&
samplers[1].source == plan.info.samplers[1].source &&
samplers[2].source == samplers[1].source &&
!samplers[1].force_point_filtering && samplers[2].force_point_filtering &&
program.info.sampled_pairs[2].sampler == 2,
"native sampler variants lost their source identity or binding order");
const auto capacity = snapshot.samplers.capacity();
user_data[1] = 0x33333333u;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.samplers.size() == 2 && snapshot.samplers.capacity() == capacity &&
snapshot.samplers[samplers[0].snapshot_index].dwords[0] == user_data[0] &&
snapshot.samplers[samplers[1].snapshot_index].dwords[0] == user_data[1] &&
snapshot.samplers[samplers[2].snapshot_index].dwords[0] == user_data[1],
"sampler variants retained stale or duplicated descriptors after refresh");
}
} // namespace
@@ -314,7 +430,8 @@ int main() {
TestUnbasedFlatCacheHitMaterializes();
TestWrittenDescriptorUsesStrictReaderOnce();
TestFailedMaterializationRejectsStage();
TestMixedSamplerDuplicatesTheCorrectSnapshot();
TestFiniteImageRefreshReusesScalarReads();
TestMixedSamplerVariantsShareRuntimeDescriptor();
std::puts("ResourceMaterializationTests: all cases passed");
return 0;
}
+196 -10
View File
@@ -2032,6 +2032,58 @@ void TestFmaskLoadSpecialization() {
"rebinding FMASK as a texture reused the metadata specialization");
}
void TestGatherLodSamplerValidation() {
for (const bool explicit_lod : {false, true}) {
for (const bool reverse : {false, true}) {
Fixture fixture;
std::array<Value, 8> image_words;
std::array<Value, 4> sampler_words;
for (uint32_t i = 0; i < image_words.size(); i++)
image_words[i] = fixture.UserData(i);
for (uint32_t i = 0; i < sampler_words.size(); i++)
sampler_words[i] = fixture.UserData(i + 8);
for (uint32_t i = 0; i < 2; i++) {
const bool lod = explicit_lod && i == (reverse ? 0u : 1u);
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_sample_flags = lod ? Decoder::ImageSampleFlagLod
: Decoder::ImageSampleFlagLevelZero;
fixture.Emit(ValueOpcode::ImageGatherRaw,
{fixture.Image(image_words), fixture.Sampler(sampler_words),
fixture.ImageAddress(), image_words[1], image_words[3],
sampler_words[1], sampler_words[2]},
fixture.AddMemory(memory, i * 4));
}
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
Check(plan.info.samplers.size() == 1 &&
plan.info.samplers[0].gather_lod == explicit_lod,
"shared sampler lost explicit gather validation or restricted LZ gather");
std::array<uint32_t, 12> user_data{};
user_data[0] = 0x1000u;
user_data[1] = static_cast<uint32_t>(
Libs::Graphics::Prospero::BufferFormat::k32Float) << 20u;
user_data[3] = Libs::Graphics::DstSel(4, 5, 6, 7) |
(static_cast<uint32_t>(Libs::Graphics::Prospero::ImageType::kColor2D) << 28u);
user_data[9] = 0xfff000u;
constexpr std::array sampler_controls{
0u, 0x4001u, 1u << 26u, (1u << 26u) | 256u,
(1u << 26u) | (1u << 14u), 2u << 26u, 3u << 26u};
for (const auto control : sampler_controls) {
user_data[10] = control;
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const bool supported = !explicit_lod || (control >> 26u) == 0 ||
control == (1u << 26u);
Check(MaterializeResources(plan, {.user_data = user_data}, snapshot,
specialization) == supported,
"explicit gather accepted an unsupported sampler or rejected a valid one");
}
}
}
}
void TestDynamicStorageMipTracking() {
Fixture fixture;
std::array<Value, 8> image_words;
@@ -2067,7 +2119,7 @@ void TestDynamicStorageMipTracking() {
const auto &images = fixture.program.info.images;
Check(images.size() == 2 && images[0].mip_mode == ImageMipMode::None &&
images[0].mip_count == 1 &&
images[1].mip_mode == ImageMipMode::DynamicStorage &&
images[1].mip_mode == ImageMipMode::Dynamic &&
images[1].mip_count == 1,
"storage mip writes did not share one dynamic logical resource");
Check(plain.first.Instruction()->Flags<uint32_t>() == 0 &&
@@ -2488,6 +2540,132 @@ void TestConditionalSamplerPhi() {
}
}
void TestFiniteImagePhiCycle() {
Fixture fixture;
auto *entry = fixture.block;
auto *header = fixture.AddBlock();
auto *reload = fixture.AddBlock();
auto *merge = fixture.AddBlock();
entry->AddBranch(header);
header->AddBranch(reload);
header->AddBranch(merge);
reload->AddBranch(merge);
merge->AddBranch(header);
std::array<Value, 8> words;
for (uint32_t word = 0; word < words.size(); ++word) {
auto &loop = header->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
auto &choice = merge->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
loop.AddPhiOperand(entry, fixture.UserData(word));
loop.AddPhiOperand(merge, Value(&choice));
// Incoming order is independent for each descriptor DWORD.
if (word & 1u) {
choice.AddPhiOperand(reload, fixture.UserData(word + 8));
choice.AddPhiOperand(header, Value(&loop));
} else {
choice.AddPhiOperand(header, Value(&loop));
choice.AddPhiOperand(reload, fixture.UserData(word + 8));
}
words[word] = Value(&choice);
}
fixture.block = merge;
const auto image = fixture.Image(words);
const auto sampler = fixture.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)});
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.AddMemory(memory, 4));
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
const auto &finite = fixture.program.descriptor_sources[source].indirect_image;
Check(finite && finite->sources.size() == 2,
"cyclic image selection did not retain two complete descriptor candidates");
const auto *key = image.Instruction()->Arg(0).Instruction();
Check(key->GetOpcode() == ValueOpcode::Phi && key->Parent() == merge,
"finite image key lost its original merge block");
const auto *loop = key->Arg(0).Instruction();
Check(loop->GetOpcode() == ValueOpcode::Phi && loop->Parent() == header &&
loop->Arg(1).Resolve() == Value(const_cast<Inst *>(key)) &&
loop->Arg(0).U32() == 0 && key->Arg(1).U32() == 1,
"finite image key did not preserve the loop-carried selection");
std::array<uint32_t, 16> data;
for (uint32_t i = 0; i < data.size(); ++i) data[i] = 100u + i;
SrtWalker walker(fixture.program, SrtRuntime{.user_data = data});
for (uint32_t candidate = 0; candidate < 2; ++candidate) {
DescriptorValue descriptor;
Check(walker.EvaluateDescriptor(finite->sources[candidate], descriptor),
"finite image candidate is not host-readable");
for (uint32_t word = 0; word < 8; ++word)
Check(descriptor.dwords[word] == data[candidate * 8 + word],
"finite image selection mixed descriptor DWORDs across predecessors");
}
}
void TestFiniteImageBitScanSentinel() {
namespace CFG = Libs::Graphics::ShaderRecompiler::CFG;
for (const bool nonzero : {false, true}) {
Fixture fixture;
auto *entry = fixture.block;
auto *dispatch = fixture.AddBlock();
auto *load = fixture.AddBlock();
auto *merge = fixture.AddBlock();
auto *exit = fixture.AddBlock();
entry->AddBranch(dispatch);
entry->AddBranch(exit);
dispatch->AddBranch(load);
dispatch->AddBranch(merge);
load->AddBranch(merge);
const auto mask = fixture.Emit(ValueOpcode::GetBuiltin,
{Value(uint32_t(StageInputKind::LocalInvocationIndex)), Value(0u)});
const auto condition = fixture.Emit(nonzero ? ValueOpcode::INotEqual32 : ValueOpcode::IEqual32,
{mask, Value(0u)});
auto &guard = fixture.program.block_info[0];
guard.condition = condition;
guard.terminator.kind = CFG::TerminatorKind::ConditionalBranch;
guard.terminator.true_block = 1;
guard.terminator.false_block = 4;
fixture.block = dispatch;
const auto first = fixture.Emit(ValueOpcode::FindILsb32, {mask});
const auto minimum = fixture.Emit(ValueOpcode::UMin32, {first, Value(32u)});
const auto selector = fixture.Emit(ValueOpcode::ShiftLeftLogical32, {minimum, Value(2u)});
fixture.Emit(ValueOpcode::ReferenceU32, {selector});
auto &table = fixture.program.block_info[1];
table.indirect_target = selector;
table.terminator.kind = CFG::TerminatorKind::IndirectBranch;
table.terminator.indirect_selector_code = 0;
table.terminator.indirect_selector_values = {0u, 128u};
table.terminator.indirect_selector_targets = {2u, 3u};
std::array<Value, 8> words;
for (uint32_t word = 0; word < words.size(); ++word) {
auto &phi = merge->AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
phi.AddPhiOperand(dispatch, mask);
phi.AddPhiOperand(load, Value(100u + word));
words[word] = Value(&phi);
}
fixture.block = merge;
const auto image = fixture.Image(words);
const auto sampler = fixture.Sampler({Value(0u), Value(0u), Value(0u), Value(0u)});
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.AddMemory(memory, 4));
if (!nonzero) {
CheckFatal([&] { fixture.PlanAndTrack(); }, "not a valid runtime value",
"zero bit scan silently accepted a GPU-valued descriptor sentinel");
Check(fixture.program.descriptor_sources.empty() && image.Instruction()->Arg(0) == words[0],
"failed finite descriptor analysis partially mutated the resource plan");
continue;
}
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
const auto &finite = fixture.program.descriptor_sources[source].indirect_image;
Check(finite && finite->sources.size() == 1 &&
image.Instruction()->Arg(0).Instruction()->NumPhiBlocks() == 2,
"nonzero bit scan retained its impossible sentinel or removed a Phi edge");
}
}
void TestLoopCycleEnteredThroughRuntimeValue() {
Fixture fixture;
auto *entry = fixture.block;
@@ -3013,15 +3191,15 @@ void TestShaderInfoAndBindingLayout() {
void TestImageBindingAbi() {
using NumericClass = Libs::Graphics::Prospero::TextureNumericClass;
Check(ImageBindingCount == 43u &&
Check(ImageBindingCount == 48u &&
static_cast<uint32_t>(DescriptorBindingKind::Buffers) == 0u &&
static_cast<uint32_t>(DescriptorBindingKind::Samplers) == 44u &&
static_cast<uint32_t>(DescriptorBindingKind::Gds) == 45u &&
static_cast<uint32_t>(DescriptorBindingKind::BdaPagetable) == 46u &&
static_cast<uint32_t>(DescriptorBindingKind::FaultBuffer) == 47u &&
static_cast<uint32_t>(DescriptorBindingKind::FlattenedSrt) == 48u &&
static_cast<uint32_t>(DescriptorBindingKind::ShaderData) == 49u &&
static_cast<uint32_t>(DescriptorBindingKind::Count) == 50u,
static_cast<uint32_t>(DescriptorBindingKind::Samplers) == 49u &&
static_cast<uint32_t>(DescriptorBindingKind::Gds) == 50u &&
static_cast<uint32_t>(DescriptorBindingKind::BdaPagetable) == 51u &&
static_cast<uint32_t>(DescriptorBindingKind::FaultBuffer) == 52u &&
static_cast<uint32_t>(DescriptorBindingKind::FlattenedSrt) == 53u &&
static_cast<uint32_t>(DescriptorBindingKind::ShaderData) == 54u &&
static_cast<uint32_t>(DescriptorBindingKind::Count) == 55u,
"native descriptor binding anchors changed");
const std::array sampled_dimensions{
@@ -3044,12 +3222,13 @@ void TestImageBindingAbi() {
uint32_t index = 0;
const auto CheckBinding =
[&](ImageResourceClass resource_class, NumericClass numeric_class,
Decoder::ImageDimension dimension, bool atomic, bool comparison = false) {
Decoder::ImageDimension dimension, bool atomic, bool comparison = false, bool atomic64 = false) {
ImageResource image;
image.resource_class = resource_class;
image.numeric_class = numeric_class;
image.dimension = dimension;
image.atomic = atomic;
image.atomic64 = atomic64;
image.depth_compare = comparison;
const auto kind = DescriptorBindingForImage(image);
Check(kind.has_value() &&
@@ -3084,6 +3263,10 @@ void TestImageBindingAbi() {
CheckBinding(ImageResourceClass::Storage, NumericClass::Uint, dimension,
true);
}
for (const auto dimension : storage_dimensions) {
CheckBinding(ImageResourceClass::Storage, NumericClass::Uint, dimension,
true, false, true);
}
Check(index == ImageBindingCount, "image descriptor ABI case count changed");
const auto Invalid = [](ImageResource image) {
@@ -3281,6 +3464,7 @@ int main() {
Run("images and samplers", TestImagesSamplersAndAliases);
Run("SampleAdjust sampler scratch", TestSampleAdjustSamplerScratch);
Run("FMASK load specialization", TestFmaskLoadSpecialization);
Run("gather LOD sampler validation", TestGatherLodSamplerValidation);
Run("dynamic storage mips", TestDynamicStorageMipTracking);
Run("invariant indirect images", TestInvariantIndirectImageMaterialization);
Run("guarded direct image table", TestGuardedDirectImageTable);
@@ -3292,6 +3476,8 @@ int main() {
Run("dynamic SRT", TestDynamicSrtReadRemainsExplicit);
Run("phi validation", TestPhiValidation);
Run("conditional sampler phi", TestConditionalSamplerPhi);
Run("finite image phi cycle", TestFiniteImagePhiCycle);
Run("finite image bit scan sentinel", TestFiniteImageBitScanSentinel);
Run("runtime-rooted loop", TestLoopCycleEnteredThroughRuntimeValue);
Run("invariant loop phi", TestInvariantLoopPhi);
Run("DMA address materialization", TestDmaAddressMaterialization);
+34
View File
@@ -650,6 +650,39 @@ void TestControlFlowValueSurvivesReadLaneFolding() {
ValidateProgram(fixture.program, true);
}
void TestDeadPhiCyclesAndPlanningRoots() {
Fixture fixture(2);
auto &entry = fixture.BlockAt(0);
auto &loop = fixture.BlockAt(1);
entry.AddBranch(&loop);
loop.AddBranch(&loop);
const auto source = fixture.Emit(ValueOpcode::GetUserData, {Value(static_cast<ScalarReg>(2))});
const auto identity = fixture.Emit(ValueOpcode::Identity, {source});
auto &live = loop.AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
live.AddPhiOperand(&entry, identity);
live.AddPhiOperand(&loop, Value(&live));
const auto reference = fixture.Emit(ValueOpcode::ReferenceU32, {Value(&live)}, 0, 1);
auto &dead = loop.AppendNewInst(ValueOpcode::Phi, {}, uint64_t(Type::U32));
const auto increment = fixture.Emit(ValueOpcode::IAdd32, {Value(&dead), Value(1u)}, 0, 1);
dead.AddPhiOperand(&entry, source);
dead.AddPhiOperand(&loop, increment);
const auto retained = fixture.Emit(ValueOpcode::GetUserData, {Value(static_cast<ScalarReg>(3))});
auto &planning = fixture.program.value_storage.emplace_back(ValueOpcode::Identity);
planning.SetArg(0, retained);
EliminateDeadCode(fixture.program.blocks);
Check(entry.Instructions().size() == 3 && loop.Instructions().size() == 2,
"dead Phi cycle survived or a live/planning dependency was removed");
Check(identity.Instruction()->Arg(0) == source && live.Arg(0) == identity &&
live.Arg(1) == Value(&live) && planning.Arg(0) == retained,
"DCE did not preserve direct Identity operands or live Phi recurrence");
// Dropping roots must clear prior marks and remove the newly dead SCC safely.
reference.Instruction()->Invalidate();
planning.Invalidate();
EliminateDeadCode(fixture.program.blocks);
Check(entry.empty() && loop.empty(), "DCE reused stale marks after removing its roots");
}
void TestUndefinedRuntimeValueFails() {
Fixture fixture;
const auto undef = fixture.Emit(ValueOpcode::UndefU32);
@@ -699,6 +732,7 @@ int main() {
TestReadLaneElimination();
TestOptimizationPipeline();
TestControlFlowValueSurvivesReadLaneFolding();
TestDeadPhiCyclesAndPlanningRoots();
TestUndefinedRuntimeValueFails();
std::cout << "TypedValuePlanningTests: all cases passed\n";
return 0;
File diff suppressed because it is too large Load Diff
+35 -1
View File
@@ -1309,7 +1309,8 @@ void TestSpirvRequirementsAnalysis() {
block->AppendNewInst(ValueOpcode::ImageQueryLod,
{Value(0u), Value(0u), Value(0u)});
block->AppendNewInst(ValueOpcode::ImageGatherRaw,
{Value(0u), Value(0u), Value(0u)});
{Value(0u), Value(0u), Value(0u), Value(0u),
Value(0u), Value(0u), Value(0u)});
auto &shared = block->AppendNewInst(ValueOpcode::LoadSharedU32,
{Value(0u), Value(true)});
shared.SetFlags(MemoryFlags{.index = 0});
@@ -4269,6 +4270,38 @@ void TestScalarAshrI64Decoder() {
"S_ASHR_I64 is missing from the decoded dump");
}
void TestImageAtomicWidthDecoder() {
using namespace ShaderRecompiler::Decoder;
// Captured PPSA29343 pixel shader: image_atomic_umax v[2:3], v[0:1], s[0:7].
const uint32_t captured[] = {0xf05c0308u, 0x00000200u};
Instruction decoded;
DecodeInstruction(captured, 0, decoded);
Check(decoded.opcode == Opcode::IMAGE_ATOMIC_UMAX &&
decoded.data_bits == 64u && decoded.data_dwords == 2u &&
decoded.dst.reg == 2u && decoded.dmask == 3u && !decoded.glc,
"captured image atomic lost its 64-bit data width");
for (const auto opcode : {0x10u, 0x17u}) {
for (uint32_t mask = 0; mask < 16u; ++mask) {
const uint32_t words[] = {EncodeMimg0(opcode, mask), captured[1]};
DecodeInstruction(words, 0, decoded);
const bool compare_swap = opcode == 0x10u;
const bool supported = mask == (compare_swap ? 3u : 1u) ||
(!compare_swap && mask == 3u);
Check((decoded.opcode != Opcode::UNSUPPORTED) == supported,
"image atomic accepted an invalid or unsupported width mask");
if (supported) {
Check(decoded.data_bits == (!compare_swap && mask == 3u ? 64u : 32u),
"image atomic DMASK selected the wrong data width");
}
}
}
const uint32_t unsupported[] = {EncodeMimg0(0x11u, 3u), captured[1]};
DecodeInstruction(unsupported, 0, decoded);
Check(decoded.opcode == Opcode::UNSUPPORTED && decoded.data_bits == 64u,
"unsupported 64-bit image atomic silently decoded as 32-bit");
}
void TestNewShaderDecoderArchitecture() {
using namespace ShaderRecompiler::Decoder;
@@ -14301,6 +14334,7 @@ int main() {
// ShaderRecompilerComputeTests; keep the distinct decoder contract checks
// here.
TestScalarAshrI64Decoder();
TestImageAtomicWidthDecoder();
TestNewShaderDecoderArchitecture();
TestImageAddressOperands();
TestSopkCompareImmediateExtension();