Compare commits

...

28 Commits

Author SHA1 Message Date
Aryan Sharma feeced81d2 window: auto-hide the cursor over the game window (#958)
* window: auto-hide the cursor over the game window

Hide the cursor when the game window is created and whenever the mouse
has been idle for two seconds; any mouse motion, button or wheel event
shows it again. Games that draw their own cursor (e.g. Blue Prince) no
longer show the host pointer on top of it.

Only real mouse input counts as activity: synthetic touch/pen mouse
events and zero-delta motion (sent by some platforms on focus or cursor
visibility changes) are ignored. The event loop only waits with a
timeout while the cursor is visible, so it still blocks indefinitely
once the cursor is hidden. The cursor is restored on shutdown.

Uses SDL_HideCursor/SDL_ShowCursor, which SDL implements for X11,
Wayland, Cocoa and Win32 and which only affect the cursor over the
emulator window.

Closes #282

* window: make cursor auto-hide optional in CLI and launcher

Co-authored-by: Anatolii Kucheruk <anatolii.kucheruk01@gmail.com>

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
Co-authored-by: Anatolii Kucheruk <anatolii.kucheruk01@gmail.com>
2026-10-03 18:15:15 +04:00
Pyxellator 820856b301 launcher: dualsense vibration/volume setting (#973)
* pad: send DualSense speaker and haptic audio over Bluetooth

* ci: fix Opus clang-cl build and macOS memory tracker test

* docs: remove Bluetooth notes from project README

* pad: keep Bluetooth audio work off emulation timing path

* pad: smooth DualSense speaker audio on USB and Bluetooth

* tests: find available macOS guest range for page manager

* pad: keep haptic waveform when Bluetooth queue overflows

* audio: reject zero output sample frequency

* Simplify DualSense audio routing and fix Bluetooth batching

* controller: add global DualSense volume and vibration controls

* Simplify global controller settings

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-03 18:02:59 +04:00
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
75 changed files with 3439 additions and 787 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
+13
View File
@@ -23,6 +23,7 @@ void Load(const ConfigOptions& cfg) {
EXIT_IF(g_config == nullptr);
EXIT_IF(cfg.user_name.empty() || cfg.user_name.size() > MAX_USER_NAME_LENGTH);
EXIT_IF(!IsConfiguredUserIdValid(cfg.user_id));
EXIT_IF(cfg.controller_speaker_volume > 100 || cfg.controller_vibration_intensity > 100);
*g_config = cfg;
}
@@ -51,6 +52,14 @@ const std::optional<ControllerColor>& GetControllerColor() {
return g_config->controller_color;
}
uint32_t GetControllerSpeakerVolume() {
return g_config->controller_speaker_volume;
}
uint32_t GetControllerVibrationIntensity() {
return g_config->controller_vibration_intensity;
}
PresentMode GetPresentMode() {
return g_config->present_mode;
}
@@ -63,6 +72,10 @@ bool FullscreenEnabled() {
return g_config->fullscreen_enabled;
}
bool HideCursorEnabled() {
return g_config->hide_cursor_enabled;
}
bool VrEnabled() {
return g_config->vr_enabled;
}
+6
View File
@@ -48,9 +48,12 @@ struct ConfigOptions {
int32_t user_id = DEFAULT_USER_ID;
std::string audio_input_device;
std::optional<ControllerColor> controller_color;
uint32_t controller_speaker_volume = 100;
uint32_t controller_vibration_intensity = 100;
PresentMode present_mode = PresentMode::Mailbox;
int32_t gpu_index = -1;
bool fullscreen_enabled = false;
bool hide_cursor_enabled = false;
bool vr_enabled = false;
bool amd_cpu_enabled = false;
uint32_t vblank_frequency = 60;
@@ -86,9 +89,12 @@ const std::string& GetUserName();
int32_t GetUserId();
const std::string& GetAudioInputDevice();
const std::optional<ControllerColor>& GetControllerColor();
uint32_t GetControllerSpeakerVolume();
uint32_t GetControllerVibrationIntensity();
PresentMode GetPresentMode();
int32_t GetGpuIndex();
bool FullscreenEnabled();
bool HideCursorEnabled();
bool VrEnabled();
bool AmdCpuEnabled();
uint32_t GetVblankFrequency();
+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);
}
+69 -8
View File
@@ -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();
@@ -66,6 +72,7 @@ struct Binding {
};
constexpr int MOUSE_POLL_INTERVAL_MS = 33;
constexpr uint64_t CURSOR_IDLE_HIDE_MS = 2000;
constexpr std::string_view MOUSE_SENSITIVITY = "MouseSensitivity=";
struct MouseJoystickState {
@@ -75,7 +82,8 @@ struct MouseJoystickState {
};
MouseJoystickState g_mouse;
SDL_Window* g_mouse_window = nullptr;
SDL_Window* g_mouse_window = nullptr;
uint64_t g_cursor_hide_at = 0;
std::size_t ControlFromName(std::string_view name) {
const auto info = std::find_if(CONTROL_INFO.begin(), CONTROL_INFO.end(),
@@ -242,6 +250,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 +270,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 +298,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);
@@ -361,11 +382,31 @@ int PollMouse(uint64_t now_ms) {
return MOUSE_POLL_INTERVAL_MS;
}
bool IsCursorActivity(const SDL_Event& event) {
SDL_MouseID which;
switch (event.type) {
case SDL_EVENT_MOUSE_MOTION:
if (event.motion.xrel == 0.0f && event.motion.yrel == 0.0f) {
return false;
}
which = event.motion.which;
break;
case SDL_EVENT_MOUSE_BUTTON_DOWN:
case SDL_EVENT_MOUSE_BUTTON_UP: which = event.button.which; break;
case SDL_EVENT_MOUSE_WHEEL: which = event.wheel.which; break;
default: return false;
}
return which != SDL_TOUCH_MOUSEID && which != SDL_PEN_MOUSEID;
}
} // namespace
void HostInputInit(SDL_Window* window) {
GetInputMap();
g_mouse_window = window;
if (Config::HideCursorEnabled()) {
g_cursor_hide_at = SDL_GetTicks() + CURSOR_IDLE_HIDE_MS;
}
}
void HostInputShutdown() {
@@ -374,7 +415,11 @@ void HostInputShutdown() {
CenterMouseStick();
g_mouse = {};
}
g_mouse_window = nullptr;
g_mouse_window = nullptr;
g_cursor_hide_at = 0;
if (Config::HideCursorEnabled()) {
SDL_ShowCursor();
}
}
void HostInputKey(int key_code, bool down) {
@@ -410,20 +455,36 @@ void HostInputToggleMouseToJoystick() {
}
bool HostInputWaitEvent(SDL_Event* event) {
bool has_event;
int timeout = -1;
if (!g_mouse.enabled || SDL_GetKeyboardFocus() != g_mouse_window) {
g_mouse.next_poll = 0;
CenterMouseStick();
has_event = SDL_WaitEvent(event);
if (!has_event) {
EXIT("%s\n", SDL_GetError());
}
} else {
if (g_mouse.next_poll == 0) {
SDL_GetRelativeMouseState(nullptr, nullptr);
g_mouse.next_poll = SDL_GetTicks() + MOUSE_POLL_INTERVAL_MS;
}
has_event = SDL_WaitEventTimeout(event, PollMouse(SDL_GetTicks()));
timeout = PollMouse(SDL_GetTicks());
}
if (g_cursor_hide_at != 0) {
const auto now_ms = SDL_GetTicks();
const int cursor_timeout =
now_ms < g_cursor_hide_at ? static_cast<int>(g_cursor_hide_at - now_ms) : 0;
timeout = timeout < 0 ? cursor_timeout : std::min(timeout, cursor_timeout);
}
const bool has_event = SDL_WaitEventTimeout(event, timeout);
if (Config::HideCursorEnabled()) {
const auto now_ms = SDL_GetTicks();
if (has_event && !g_mouse.enabled && IsCursorActivity(*event) &&
SDL_GetWindowFromEvent(event) == g_mouse_window) {
SDL_ShowCursor();
g_cursor_hide_at = now_ms + CURSOR_IDLE_HIDE_MS;
} else if (g_cursor_hide_at != 0 && now_ms >= g_cursor_hide_at) {
SDL_HideCursor();
g_cursor_hide_at = 0;
}
}
if (has_event && event->type == SDL_EVENT_WINDOW_FOCUS_LOST &&
@@ -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);
+83 -23
View File
@@ -23,7 +23,7 @@
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="0">
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="sectionsLayout" columnstretch="1,1">
<property name="horizontalSpacing">
<number>8</number>
@@ -72,27 +72,6 @@
<property name="toolTip"><string>Language</string></property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_controller_color">
<property name="text"><string>DualSense lightbar:</string></property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="controllerColorLayout">
<item>
<widget class="QPushButton" name="button_controller_color">
<property name="toolTip"><string>Choose and preview a fixed LED color. Bluetooth DualSense may take several seconds to update.</string></property>
<property name="text"><string>Custom</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_controller_color_reset">
<property name="toolTip"><string>Let the game control the lightbar</string></property>
<property name="text"><string>Default</string></property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
@@ -194,6 +173,12 @@
<property name="text"><string>Enable tessellation support (experimental)</string></property>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="checkBox_hide_cursor">
<property name="toolTip"><string>Hide the host cursor after 2 seconds without mouse activity</string></property>
<property name="text"><string>Auto-hide cursor</string></property>
</widget>
</item>
</layout>
</widget>
</item>
@@ -316,7 +301,82 @@
</item>
</layout>
</item>
<item row="2" column="0">
<item row="1" column="1">
<widget class="QGroupBox" name="controller_group">
<property name="title"><string>Controller</string></property>
<layout class="QFormLayout" name="controllerLayout">
<property name="fieldGrowthPolicy"><enum>QFormLayout::AllNonFixedFieldsGrow</enum></property>
<item row="0" column="0">
<widget class="QLabel" name="label_controller_color">
<property name="text"><string>DualSense lightbar:</string></property>
</widget>
</item>
<item row="0" column="1">
<layout class="QHBoxLayout" name="controllerColorLayout">
<item>
<widget class="QPushButton" name="button_controller_color">
<property name="toolTip"><string>Choose and preview a fixed LED color. Bluetooth DualSense may take several seconds to update.</string></property>
<property name="text"><string>Custom</string></property>
</widget>
</item>
<item>
<widget class="QPushButton" name="button_controller_color_reset">
<property name="toolTip"><string>Let the game control the lightbar</string></property>
<property name="text"><string>Default</string></property>
</widget>
</item>
</layout>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_controller_vibration">
<property name="text"><string>Vibration intensity:</string></property>
</widget>
</item>
<item row="1" column="1">
<layout class="QHBoxLayout" name="controllerVibrationLayout">
<item>
<widget class="QSlider" name="slider_controller_vibration">
<property name="toolTip"><string>Scale DualSense vibration over USB and Bluetooth</string></property>
<property name="maximum"><number>100</number></property>
<property name="value"><number>100</number></property>
<property name="orientation"><enum>Qt::Horizontal</enum></property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_controller_vibration_value">
<property name="minimumSize"><size><width>40</width><height>0</height></size></property>
<property name="text"><string>100%</string></property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_controller_volume">
<property name="text"><string>Speaker volume:</string></property>
</widget>
</item>
<item row="2" column="1">
<layout class="QHBoxLayout" name="controllerVolumeLayout">
<item>
<widget class="QSlider" name="slider_controller_volume">
<property name="toolTip"><string>Scale the DualSense controller speaker over USB and Bluetooth</string></property>
<property name="maximum"><number>100</number></property>
<property name="value"><number>100</number></property>
<property name="orientation"><enum>Qt::Horizontal</enum></property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_controller_volume_value">
<property name="minimumSize"><size><width>40</width><height>0</height></size></property>
<property name="text"><string>100%</string></property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
<item row="2" column="0" colspan="2">
<layout class="QHBoxLayout" name="buttonLayout">
<item>
<widget class="QPushButton" name="clear_button">
+31 -5
View File
@@ -46,6 +46,30 @@ QString EnumToText(T value) {
: key);
}
struct ControllerSettings {
QString color;
int speaker_volume = 100;
int vibration_intensity = 100;
void WriteSettings(QSettings* s) const {
s->setValue("controller_color", color);
s->setValue("controller_speaker_volume", speaker_volume);
s->setValue("controller_vibration_intensity", vibration_intensity);
}
void ReadSettings(QSettings* s) {
const QColor saved_color(s->value("controller_color").toString());
color = saved_color.isValid() ? saved_color.name(QColor::HexRgb) : QString {};
const auto read_percent = [s](const char* key) {
bool ok = false;
const int value = s->value(key, 100).toInt(&ok);
return ok ? qBound(0, value, 100) : 100;
};
speaker_volume = read_percent("controller_speaker_volume");
vibration_intensity = read_percent("controller_vibration_intensity");
}
};
class Configuration: public QObject {
Q_OBJECT
@@ -85,14 +109,17 @@ public:
GameStatus game_status = GameStatus::Unknown;
QString game_comment;
// Controller preferences always come from the global configuration.
ControllerSettings controller;
Resolution screen_resolution = Resolution::R1280X720;
QString user_name = "Kyty";
int user_id = Config::DEFAULT_USER_ID;
QString audio_input_device;
QString controller_color;
PresentMode present_mode = PresentMode::Mailbox;
int gpu_index = -1;
bool fullscreen_enabled = false;
bool hide_cursor_enabled = false;
bool readback_linear_images = false;
bool tessellation_enabled = false;
int vblank_frequency = 60;
@@ -121,10 +148,10 @@ public:
user_name = other.user_name;
user_id = other.user_id;
audio_input_device = other.audio_input_device;
controller_color = other.controller_color;
present_mode = other.present_mode;
gpu_index = other.gpu_index;
fullscreen_enabled = other.fullscreen_enabled;
hide_cursor_enabled = other.hide_cursor_enabled;
readback_linear_images = other.readback_linear_images;
tessellation_enabled = other.tessellation_enabled;
vblank_frequency = other.vblank_frequency;
@@ -168,10 +195,10 @@ public:
KYTY_CFG_SET(user_name);
KYTY_CFG_SET(user_id);
KYTY_CFG_SET(audio_input_device);
KYTY_CFG_SET(controller_color);
KYTY_CFG_SET(present_mode);
KYTY_CFG_SET(gpu_index);
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(hide_cursor_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(vblank_frequency);
@@ -208,14 +235,13 @@ public:
? saved_user_id
: Config::DEFAULT_USER_ID;
audio_input_device = s->value("audio_input_device", audio_input_device).toString();
const QColor color(s->value("controller_color", controller_color).toString());
controller_color = color.isValid() ? color.name(QColor::HexRgb) : QString {};
KYTY_CFG_GET(present_mode);
gpu_index = s->value("gpu_index", -1).toInt();
if (EnumToText(present_mode).isEmpty()) {
present_mode = PresentMode::Mailbox;
}
KYTY_CFG_GET(fullscreen_enabled);
KYTY_CFG_GET(hide_cursor_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(tessellation_enabled);
vblank_frequency = s->value("vblank_frequency", vblank_frequency).toInt();
@@ -28,7 +28,7 @@ public:
static void WriteSettings(QSettings& s);
static void ReadSettings(QSettings& s);
void SetGameDirectories(const QStringList& dirs);
void SetGlobalSettings(const QStringList& dirs);
[[nodiscard]] QStringList GetGameDirectories() const;
signals:
@@ -40,7 +40,7 @@ private:
QGroupBox* m_game_dirs_group = nullptr;
QListWidget* m_game_dirs_list = nullptr;
QToolButton* m_remove_game_dir_button = nullptr;
bool m_show_game_dirs = false;
bool m_global_settings = false;
protected:
void Init(const Configuration& info);
@@ -34,7 +34,7 @@ public:
[[nodiscard]] const QString& GetSettingsFile() const { return m_settings_file; }
[[nodiscard]] const QString& GetGlobalControllerColor() const {
return m_global_info.controller_color;
return m_global_info.controller.color;
}
[[nodiscard]] std::unique_ptr<Configuration>
CreateConfiguration(const ConfigurationItem& item) const;
+27 -10
View File
@@ -23,6 +23,7 @@
#include <QPushButton>
#include <QResizeEvent>
#include <QSettings>
#include <QSlider>
#include <QSpinBox>
#include <QStyle>
#include <QToolButton>
@@ -39,7 +40,7 @@
constexpr char SETTINGS_CFG_DIALOG[] = "ConfigurationEditDialog";
constexpr char SETTINGS_CFG_LAST_GEOMETRY[] = "geometry";
constexpr int GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH = 560;
constexpr int GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH = 400;
static void UpdateControllerColorButton(QPushButton* button, const QString& hex) {
const QColor color(hex);
@@ -123,6 +124,7 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
m_info(info) {
m_ui->setupUi(this);
InitGameDirectories();
m_ui->controller_group->setVisible(false);
connect(m_ui->ok_button, &QPushButton::clicked, this, &ConfigurationEditDialog::save);
connect(m_ui->cancel_button, &QPushButton::clicked, this, &QDialog::reject);
@@ -142,6 +144,12 @@ ConfigurationEditDialog::ConfigurationEditDialog(Configuration& info, QWidget* p
UpdateControllerColorButton(m_ui->button_controller_color, {});
emit PreviewControllerColor({});
});
connect(m_ui->slider_controller_vibration, &QSlider::valueChanged, this, [this](int value) {
m_ui->label_controller_vibration_value->setText(tr("%1%").arg(value));
});
connect(m_ui->slider_controller_volume, &QSlider::valueChanged, this, [this](int value) {
m_ui->label_controller_volume_value->setText(tr("%1%").arg(value));
});
connect(m_ui->comboBox_shader_log_direction, &QComboBox::currentTextChanged, this,
[this](const QString& text) {
auto log = TextToEnum<Configuration::LogDirection>(text);
@@ -201,7 +209,9 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
m_ui->lineEdit_user_name->setMaxLength(static_cast<int>(Config::MAX_USER_NAME_LENGTH));
m_ui->lineEdit_user_name->setText(info.user_name);
m_ui->spinBox_user_id->setValue(info.user_id);
UpdateControllerColorButton(m_ui->button_controller_color, info.controller_color);
UpdateControllerColorButton(m_ui->button_controller_color, info.controller.color);
m_ui->slider_controller_vibration->setValue(info.controller.vibration_intensity);
m_ui->slider_controller_volume->setValue(info.controller.speaker_volume);
auto* microphone = m_ui->comboBox_audio_input_device;
microphone->clear();
microphone->addItem(tr("None"), QString {});
@@ -263,6 +273,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
info.gpu_index >= 0 && info.gpu_index < m_ui->comboBox_gpu->count() - 1 ? info.gpu_index + 1
: 0);
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
@@ -342,8 +353,8 @@ void ConfigurationEditDialog::InitGameDirectories() {
update_game_directory_buttons();
}
void ConfigurationEditDialog::SetGameDirectories(const QStringList& dirs) {
m_show_game_dirs = true;
void ConfigurationEditDialog::SetGlobalSettings(const QStringList& dirs) {
m_global_settings = true;
m_game_dirs_list->clear();
m_game_dirs_group->setMinimumWidth(GLOBAL_SETTINGS_GAME_DIRS_MIN_WIDTH);
@@ -352,6 +363,7 @@ void ConfigurationEditDialog::SetGameDirectories(const QStringList& dirs) {
}
m_game_dirs_group->setVisible(true);
m_ui->controller_group->setVisible(true);
update_game_directory_buttons();
layout()->activate();
resize(size().expandedTo(minimumSizeHint()));
@@ -359,7 +371,7 @@ void ConfigurationEditDialog::SetGameDirectories(const QStringList& dirs) {
QStringList ConfigurationEditDialog::GetGameDirectories() const {
QStringList dirs;
if (!m_show_game_dirs) {
if (!m_global_settings) {
return dirs;
}
@@ -399,17 +411,22 @@ void ConfigurationEditDialog::resizeEvent(QResizeEvent* event) {
g_last_geometry = saveGeometry();
}
static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui) {
static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, bool global_settings) {
info.user_name = ui.lineEdit_user_name->text().trimmed();
info.user_id = ui.spinBox_user_id->value();
info.audio_input_device = ui.comboBox_audio_input_device->currentData().toString();
info.controller_color = ui.button_controller_color->property("controllerColor").toString();
if (global_settings) {
info.controller.color = ui.button_controller_color->property("controllerColor").toString();
info.controller.vibration_intensity = ui.slider_controller_vibration->value();
info.controller.speaker_volume = ui.slider_controller_volume->value();
}
info.screen_resolution =
TextToEnum<Configuration::Resolution>(ui.comboBox_screen_resolution->currentText());
info.present_mode =
TextToEnum<Configuration::PresentMode>(ui.comboBox_present_mode->currentText());
info.gpu_index = ui.comboBox_gpu->currentIndex() - 1;
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
@@ -453,7 +470,7 @@ void ConfigurationEditDialog::save() {
return;
}
UpdateInfo(m_info, *m_ui);
UpdateInfo(m_info, *m_ui, m_global_settings);
emit accept();
}
@@ -461,9 +478,9 @@ void ConfigurationEditDialog::save() {
void ConfigurationEditDialog::clear() {
Configuration default_info;
Init(default_info);
emit PreviewControllerColor({});
if (m_show_game_dirs) {
if (m_global_settings) {
emit PreviewControllerColor({});
m_game_dirs_list->clear();
update_game_directory_buttons();
}
+6 -1
View File
@@ -314,6 +314,7 @@ void ConfigurationListWidget::WriteSettings() {
s->remove(CONF_GLOBAL);
s->beginGroup(CONF_GLOBAL);
m_global_info.WriteSettings(s.get());
m_global_info.controller.WriteSettings(s.get());
s->endGroup();
s->remove(CONF_SECTION_NAME);
@@ -357,6 +358,7 @@ void ConfigurationListWidget::ReadSettings() {
if (!s->childKeys().isEmpty()) {
m_global_info.ReadSettings(s.get());
}
m_global_info.controller.ReadSettings(s.get());
s->endGroup();
qDeleteAll(m_custom_infos);
@@ -409,6 +411,7 @@ ConfigurationListWidget::CreateConfiguration(const ConfigurationItem& item) cons
const auto* custom = m_custom_infos.value(item.GetInfo().game_path);
info->CopyGameInfoFrom(item.GetInfo());
info->CopyEmulatorSettingsFrom(custom != nullptr ? *custom : m_global_info);
info->controller = m_global_info.controller;
if (custom != nullptr && !custom->elf.isEmpty()) {
info->elf = custom->elf;
}
@@ -787,16 +790,18 @@ void ConfigurationListWidget::delete_configuartion() {
void ConfigurationListWidget::edit_global_settings() {
Configuration info;
info.CopyEmulatorSettingsFrom(m_global_info);
info.controller = m_global_info.controller;
info.name = tr("Global settings");
ConfigurationEditDialog dlg(info, this);
dlg.setWindowTitle(tr("Global settings"));
dlg.SetGameDirectories(m_game_dirs);
dlg.SetGlobalSettings(m_game_dirs);
connect(&dlg, &ConfigurationEditDialog::PreviewControllerColor, this,
&ConfigurationListWidget::PreviewControllerColor);
if (dlg.exec() == QDialog::Accepted) {
m_global_info.CopyEmulatorSettingsFrom(info);
m_global_info.controller = info.controller;
const auto game_dirs = NormalizeGameDirectories(dlg.GetGameDirectories());
const bool game_dirs_changed = game_dirs != m_game_dirs;
m_game_dirs = game_dirs;
+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) {
+8 -4
View File
@@ -228,9 +228,11 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
if (!info.audio_input_device.isEmpty()) {
args << "--mic" << info.audio_input_device;
}
if (!info.controller_color.isEmpty()) {
args << "--controller-color" << info.controller_color;
if (!info.controller.color.isEmpty()) {
args << "--controller-color" << info.controller.color;
}
args << "--controller-volume" << QString::number(info.controller.speaker_volume);
args << "--controller-vibration" << QString::number(info.controller.vibration_intensity);
args << "--present-mode" << EnumToText(info.present_mode);
if (info.gpu_index >= 0) {
args << "--gpu" << QString::number(info.gpu_index);
@@ -238,6 +240,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
if (info.fullscreen_enabled) {
args << "--fullscreen";
}
if (info.hide_cursor_enabled) {
args << "--hide-cursor";
}
args << "--readback-linear-images" << BoolArg(info.readback_linear_images);
if (info.tessellation_enabled) {
args << "--tessellation";
@@ -529,8 +534,7 @@ void MainDialogPrivate::Update() {
m_lightbar.Stop();
return;
}
m_lightbar.SetColor(item != nullptr ? m_ui->widget->CreateConfiguration(*item)->controller_color
: m_ui->widget->GetGlobalControllerColor());
m_lightbar.SetColor(m_ui->widget->GetGlobalControllerColor());
}
#include "mainDialog.moc"
+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;
}
+106 -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,62 @@ 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);
if (setting == Setting::SpeakerVolume) {
return SPEAKER_VOLUME[step] * (Config::GetControllerSpeakerVolume() / 100.0f);
}
if (setting == Setting::VibrationIntensity) {
return INTENSITY[step] * (Config::GetControllerVibrationIntensity() / 100.0f);
}
return 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 +265,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 +278,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 +286,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 +302,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 +321,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 +414,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 +662,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 +694,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 +718,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 +754,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);
+21
View File
@@ -54,11 +54,15 @@ static void PrintUsage() {
Config::DEFAULT_USER_ID);
::printf(" --mic <name> Capture from this microphone; omit for silence.\n");
::printf(" --controller-color <#RRGGBB> Override the controller lightbar color.\n");
::printf(" --controller-volume <0-100> DualSense speaker volume. Default: 100.\n");
::printf(" --controller-vibration <0-100> DualSense vibration intensity. Default: 100.\n");
::printf(
" --present-mode <value> Fifo, Mailbox, or Immediate. Default: Mailbox.\n");
::printf(
" --gpu <index> Vulkan physical device index. Default: auto.\n");
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
::printf(
" --hide-cursor Hide the cursor after 2 s idle. Default: off.\n");
::printf(" --vr Enable the virtual VR headset.\n");
::printf(" --amd-cpu Apply AMD CPU instruction patches.\n");
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
@@ -203,6 +207,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
continue;
}
if (arg == "--hide-cursor") {
options.config.hide_cursor_enabled = true;
continue;
}
if (arg == "--vr") {
options.config.vr_enabled = true;
continue;
@@ -324,6 +333,18 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
return false;
}
options.config.controller_color = color;
} else if (arg == "--controller-volume") {
if (!ParseUint32(value, options.config.controller_speaker_volume) ||
options.config.controller_speaker_volume > 100) {
::printf("invalid controller volume: %s\n", value.c_str());
return false;
}
} else if (arg == "--controller-vibration") {
if (!ParseUint32(value, options.config.controller_vibration_intensity) ||
options.config.controller_vibration_intensity > 100) {
::printf("invalid controller vibration intensity: %s\n", value.c_str());
return false;
}
} else if (arg == "--present-mode") {
if (!ParseEnum(value, options.config.present_mode)) {
::printf("invalid present mode: %s\n", value.c_str());
+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
+341
View File
@@ -0,0 +1,341 @@
// 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) ==
Config::GetControllerSpeakerVolume() / 100.0f &&
GetSettingScale(Setting::VibrationIntensity) ==
Config::GetControllerVibrationIntensity() / 100.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 TestGlobalControllerLevels() {
Config::ConfigOptions options;
options.controller_speaker_volume = 50;
options.controller_vibration_intensity = 25;
Config::Load(options);
{
Controller controller;
Check(GetSettingScale(Setting::SpeakerVolume) == 0.5f &&
GetSettingScale(Setting::VibrationIntensity) == 0.25f &&
GetSettingScale(Setting::TriggerEffectIntensity) == 1.0f,
"global controller levels did not scale their outputs independently");
SetRumble(200, 100);
Check(rumble.back().large == 50 * 257 && rumble.back().small == 25 * 257,
"global vibration level did not scale rumble");
CycleSetting(Setting::SpeakerVolume);
CycleSetting(Setting::VibrationIntensity);
Check(GetSettingScale(Setting::SpeakerVolume) == 0.0f &&
GetSettingScale(Setting::VibrationIntensity) == 0.0f,
"cycle hotkeys did not mute globally scaled outputs");
CycleSetting(Setting::SpeakerVolume);
CycleSetting(Setting::VibrationIntensity);
Check(GetSettingScale(Setting::SpeakerVolume) == 0.01f &&
GetSettingScale(Setting::VibrationIntensity) == 0.0825f,
"cycle hotkeys did not multiply the global controller levels");
Check(rumble.back().large == 17 * 257 && rumble.back().small == 8 * 257,
"cycling vibration did not apply the combined intensity to cached rumble");
}
options.controller_speaker_volume = 0;
options.controller_vibration_intensity = 0;
Config::Load(options);
{
Controller controller;
Check(GetSettingScale(Setting::SpeakerVolume) == 0.0f &&
GetSettingScale(Setting::VibrationIntensity) == 0.0f,
"zero global controller levels did not mute outputs");
SetRumble(200, 100);
Check(rumble.back().large == 0 && rumble.back().small == 0,
"zero global vibration level did not mute motors");
}
Config::Load(Config::ConfigOptions {});
}
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();
TestGlobalControllerLevels();
// 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();