Compare commits

...

31 Commits

Author SHA1 Message Date
nmzik dad900c61d Fix BC alias writes and GC 2026-10-12 04:51:02 +02:00
nmzik 5b730d4df0 simplify guest gpu 2026-10-12 02:52:49 +02:00
nmzik e699a35f20 netctl: add opt-in host IPv4 connectivity and launcher toggle 2026-10-12 01:20:43 +02:00
nmzik 41beda4367 ampr: fix APR completion loss in 768-byte read batches
Co-authored-by: Chai T <ct@talnikar.in>
2026-10-12 01:20:43 +02:00
Chai T 048fe9f413 kernel: export sceKernelFtruncate through existing native handler 2026-10-12 01:20:43 +02:00
nmzik e51de54ee4 add hdr display support 2026-10-11 20:29:32 +02:00
nmzik 984cfcebd1 shader: emit native texel offsets using sample execution context
Co-authored-by: netwarex <mail@pety.me>
2026-10-11 18:53:01 +02:00
nmzik df0412afb5 shader: preserve shared predicates during resource plan extraction
Co-authored-by: netwarex <mail@pety.me>
2026-10-11 18:53:01 +02:00
nmzik f28c0fccb2 texture: preserve guest DCC metadata for comparison depth aliases
Co-authored-by: netwarex <mail@pety.me>
2026-10-11 18:53:01 +02:00
netwarex 4c87dbebb2 shader: select indirect texture descriptors for image gathers 2026-10-11 18:53:01 +02:00
nmzik 16a7fe689a fix windows compilation (2) (#1411)
* fix windows compialtion (2)

* loader: guard LoadExec close_range path with __linux__ for macOS

---------

Co-authored-by: ReverendOzmund <catmilker@proton.me>
2026-10-11 11:11:02 +02:00
thenotsoquietninja44-collab 6f0cddd440 implement LoadExec (windows) 2026-10-11 08:24:33 +02:00
nmzik 4084a008ea fix windows compilation 2026-10-11 07:53:59 +02:00
HamsterCL c0ebf22476 tests: rename a local that collides with the Windows small macro (#1400)
windows.h pulls in rpcndr.h, which defines `small` as `char`. The R64 BC1
array case added in b7b335ac declared `const auto small`, so the Windows
build of shader_recompiler_compute_tests failed to compile:

  ShaderRecompilerComputeTests.cpp(17928,24): error: expected unqualified-id

Rename the local to `small_array`.
2026-10-11 09:35:18 +02:00
nmzik cb375ec35a add load exec
Co-authored-by: thenotsoquietninja44-collab <thenotsoquietninja44@gmail.com>
2026-10-11 09:31:31 +02:00
nmzik 6f3bebcb50 playgo: read plgx chunks 2026-10-11 09:31:31 +02:00
nmzik 1029511bbc add depth to color copy
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-10-11 09:31:31 +02:00
nmzik 65144ce4ac gpu: centralize depth copy state validation in target resolution 2026-10-11 09:31:31 +02:00
nmzik f848d2761b add srt walker 2026-10-11 09:31:31 +02:00
ReverendOzmund 730e1a3cf3 docs: add Fedora and Fedora Atomic (Bazzite) build instructions (#1372)
* docs: add Fedora and Fedora Atomic (Bazzite) build instructions

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SscrMzrvwrj17PJsfzqog9

* docs: note --nvidia for Distrobox on NVIDIA hosts

* docs: streamline Fedora build instructions

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-11 03:24:15 +02:00
nmzik d4bb7fd786 agc: remove command buffer debug dump 2026-10-11 03:03:26 +02:00
popcoder565 fbea51c684 agc: implement sceAgcSetNop (#1343)
* agc: implement sceAgcSetNop

Star Wars Outlaws calls sceAgcSetNop (K2mciNVxUCE) and crashes right after
the unresolved stub returns, without the packet being disabled.

The call turns a packet into a NOP of the same length. Clear the emulator's
custom NOP code as well, so packets that the HLE encodes as NOPs, such as
end-of-pipe actions, stop executing too.

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

* agc: keep packets disabled by sceAgcSetNop from reading as markers

A disabled packet whose first payload word carries the 0x6875 marker
signature still reached the marker handler, which can flip or stop on an
unknown marker ID. Clear the signature so the packet is skipped like any
other NOP.

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

* agc: tidy SetNop logging and comments

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-11 03:00:09 +02:00
Devran Cosmo Uenal a6f4e8e7f4 AvPlayer: recycle output buffers so the decoder can run ahead 2026-10-11 02:08:27 +02:00
nmzik 448a4c8a6a launcher: reorganize panels 2026-10-11 01:44:04 +02:00
nmzik ece4e28740 gpu: store private format byte sizes in uint8_t 2026-10-11 01:25:47 +02:00
nmzik 58ba1e4cb4 docs: update README wording and grammar 2026-10-11 01:13:14 +02:00
nmzik cf77fb4353 shader: initialize NGG vertex count from active native invocations 2026-10-11 01:05:56 +02:00
nmzik 7e4e142aae gpu: enable native R8 SNORM texture sampling 2026-10-11 01:05:56 +02:00
nmzik b7b335ac63 gpu: admit block-compressed textures in PS5 R64 tiling 2026-10-11 01:05:56 +02:00
nmzik 9a5dd9faff agc: reserve driver backing before guest direct-memory allocation 2026-10-11 01:05:56 +02:00
nmzik 947627d10d docs: clarify pull request scope 2026-10-10 22:47:36 +02:00
61 changed files with 2500 additions and 1174 deletions
+17 -13
View File
@@ -40,6 +40,11 @@ if (NOT (WIN32 OR LINUX OR APPLE))
message(FATAL_ERROR "only Windows, Linux, and macOS builds are supported")
endif()
if(WIN32)
# Prevent Windows headers from defining min/max macros in all targets.
add_compile_definitions(NOMINMAX)
endif()
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
@@ -298,28 +303,27 @@ list(APPEND kyty_emulator_src "${fault_buffer_shader_header}")
list(APPEND kyty_emulator_src ${gpu_tiler_shader_headers})
set(gpu_blit_generated_dir "${PROJECT_BINARY_DIR}/gpu_blit_shaders")
set(gpu_blit_shader_sources
"${gpu_tiler_shader_dir}/gpu_blit_fs_triangle.vert"
"${gpu_tiler_shader_dir}/gpu_blit_color_to_ms_depth.frag"
"${gpu_tiler_shader_dir}/gpu_video_out_overlay.frag"
)
foreach(shader_source IN LISTS gpu_blit_shader_sources)
get_filename_component(shader_name "${shader_source}" NAME_WE)
function(add_gpu_blit_shader shader_name shader_file)
set(shader_source "${gpu_tiler_shader_dir}/${shader_file}")
set(shader_spv "${gpu_blit_generated_dir}/${shader_name}.spv")
set(shader_header "${gpu_blit_generated_dir}/${shader_name}_spv.h")
string(TOUPPER "${shader_name}_SPV" shader_symbol)
add_custom_command(
OUTPUT "${shader_header}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${gpu_blit_generated_dir}"
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os ${ARGN}
"-I${gpu_tiler_shader_dir}" -o "${shader_spv}" "${shader_source}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${shader_spv} -DOUTPUT=${shader_header}
-DSYMBOL=${shader_symbol} -P "${KYTY_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${shader_source}"
VERBATIM
)
list(APPEND gpu_blit_shader_headers "${shader_header}")
endforeach()
set(gpu_blit_shader_headers ${gpu_blit_shader_headers} "${shader_header}" PARENT_SCOPE)
endfunction()
add_gpu_blit_shader(gpu_blit_fs_triangle gpu_blit_fs_triangle.vert)
add_gpu_blit_shader(gpu_blit_color_to_ms_depth gpu_blit_color_to_ms_depth.frag)
add_gpu_blit_shader(gpu_video_out_overlay gpu_video_out_overlay.frag)
add_gpu_blit_shader(gpu_imgui_hdr gpu_video_out_overlay.frag -DIMGUI_HDR=1)
list(APPEND kyty_emulator_src ${gpu_blit_shader_headers})
list(APPEND kyty_emulator_src
@@ -399,7 +403,7 @@ add_executable(scalar_provenance_tests EXCLUDE_FROM_ALL
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/SrtWalker.cpp"
)
target_link_libraries(scalar_provenance_tests fmt::fmt)
target_link_libraries(scalar_provenance_tests fmt::fmt xbyak::xbyak)
target_include_directories(scalar_provenance_tests PRIVATE ${inc_headers})
add_executable(page_manager_tests EXCLUDE_FROM_ALL
@@ -467,7 +471,7 @@ add_executable(resource_materialization_tests EXCLUDE_FROM_ALL
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/SrtWalker.cpp"
)
target_link_libraries(resource_materialization_tests fmt::fmt)
target_link_libraries(resource_materialization_tests fmt::fmt xbyak::xbyak)
target_include_directories(resource_materialization_tests PRIVATE ${inc_headers})
add_executable(resource_tracking_tests EXCLUDE_FROM_ALL
@@ -480,7 +484,7 @@ add_executable(resource_tracking_tests EXCLUDE_FROM_ALL
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp"
"${KYTY_SOURCE_DIR}/graphics/shader/recompiler/ir/passes/ShaderInfoCollection.cpp"
)
target_link_libraries(resource_tracking_tests fmt::fmt)
target_link_libraries(resource_tracking_tests fmt::fmt xbyak::xbyak)
target_include_directories(resource_tracking_tests PRIVATE ${inc_headers})
add_executable(audio_out2_port_tests EXCLUDE_FROM_ALL
+64 -21
View File
@@ -7,12 +7,12 @@
[![Status](https://img.shields.io/badge/status-active%20development-orange.svg)](#current-status)
[![License](https://img.shields.io/badge/license-GPL--2.0-blue.svg)](LICENSE)
**[Weekly updates](https://github.com/KytyPS5/KytyPS5/discussions/862)** — game progress, recent fixes and ongoing development.
**[Weekly updates](https://github.com/KytyPS5/KytyPS5/discussions/862)** — game progress, recent fixes, and ongoing development.
**[Development on Discord](https://discord.gg/UNrkMqGaBg)** — KytyPS5 development.
KytyPS5 is a free and open-source PlayStation 5 emulator written in C++ for Windows and Linux,
with experimental macOS support. It is based on a heavily modified version of
KytyPS5 is a free and open-source PlayStation 5 translation layer written in C++ for Windows
and Linux, with experimental macOS support. It is based on a heavily modified version of
[Kyty](https://github.com/InoriRus/Kyty). The project is in active development, and behavior
can change significantly between builds.
@@ -23,13 +23,13 @@ can change significantly between builds.
## Current Status
KytyPS5 can boot 2D games and a selection of 3D games, including titles built with Unreal Engine
KytyPS5 can boot 2D and 3D games, including titles built with Unreal Engine
4/5, Unity, and custom engines. External low-level emulation modules are neither required nor
planned.
Development is currently focused on expanding game compatibility and improving boot reliability.
Windows and Linux are the primary platforms and receive the most testing.
Windows and Linux are the primary target platforms and receive the most testing.
macOS support is experimental. The emulator is built for x86-64 and runs on Apple Silicon under
Rosetta 2, with Vulkan provided by MoltenVK. A small number of titles have been verified in-game
@@ -85,14 +85,20 @@ or graphical glitches, so please include the version you tested when reporting a
Testing games and submitting detailed bug reports are useful ways to contribute. Search existing
issues first, then use the **Game Emulation Status Report** template and attach the complete log file.
Code contributions should be focused, build successfully on the platforms they touch, and include
relevant tests where practical. Windows is the primary target, so a change that alters shared code
should not regress it; changes confined to a platform's own code paths only need to build there. Because KytyPS5 is still evolving quickly, consider opening an issue before
starting a large change.
**Pull requests for general fixes, architectural changes, or refactoring that do not address a concrete bug or launch problem in a specific game will be automatically closed.**
**Large, unfocused commits or pull requests that bundle unrelated changes without a clear purpose or game-related benefit will also be automatically closed.**
Keep each PR focused. Name the affected game, describe the exact problem, and provide evidence showing what the change fixes.
Windows and Linux are the primary target platforms, so changes to shared code should not
introduce regressions on either platform. Changes confined to a platform's own code paths
only need to build on that platform. Because KytyPS5 is still evolving quickly, consider
opening an issue before starting a large change.
### Formatting
Set up the clang-format hook after cloning:
Set up the clang-format hook after cloning.
Install `pre-commit` using the method appropriate for your platform:
@@ -171,9 +177,9 @@ The finished application and its runtime dependencies will be placed in
### Building on Linux
Install the toolchain and the libraries the bundled SDL3 needs. Without the audio, Wayland and
udev development packages SDL3 quietly configures itself without those backends, and the resulting
build has no working sound and no gamepad hotplug:
Install the toolchain and the libraries the bundled SDL3 needs. Without the audio, Wayland, and
udev development packages, SDL3 quietly configures itself without those backends, and the resulting
build has no working audio or gamepad hotplug support:
```bash
sudo apt-get install --no-install-recommends \
@@ -183,8 +189,8 @@ sudo apt-get install --no-install-recommends \
libasound2-dev libpulse-dev libudev-dev libdbus-1-dev libwayland-dev wayland-protocols
```
Qt 6 (Concurrent, Network, Widgets) is required for the launcher — either the distribution packages
(`qt6-base-dev`) or an official Qt installation.
Qt 6 (Concurrent, Network, and Widgets) is required for the launcher. Use your distribution's
packages (`qt6-base-dev`) or an official Qt installation.
```bash
git submodule update --init --recursive
@@ -198,7 +204,7 @@ cmake --install _Build/linux --prefix _Build/linux/install
```
The install step copies the Qt libraries and plugins next to the binaries, so
`_Build/linux/install` runs without a matching system Qt. FFmpeg is linked statically
`_Build/linux/install` runs without a matching system Qt installation. FFmpeg is linked statically
from the pinned [KytyPS5 FFmpeg core](https://github.com/KytyPS5/ext-ffmpeg-core)
release, including VP9 and WebM support. System FFmpeg packages are not required.
@@ -242,6 +248,43 @@ sources, so it needs network access; a fully sandboxed `nix build` would require
inputs. A Vulkan 1.3 driver must be available at runtime (on NixOS,
`hardware.graphics.enable = true`).
### Building on Fedora and Fedora Atomic (Bazzite, Bluefin, Silverblue)
On regular Fedora, install dependencies on the host. On Fedora Atomic variants such as Bazzite,
Bluefin and Silverblue, use [Distrobox](https://distrobox.it/) and run the install and build steps
inside the container. Keep the checkout in your home directory so it is shared with the container.
Add `--nvidia` to the create command for hosts using the proprietary NVIDIA driver:
```bash
distrobox create --name kyty-build --image registry.fedoraproject.org/fedora:latest
distrobox enter kyty-build
```
Install the toolchain, Qt 6 and SDL3 dependencies:
```bash
sudo dnf install -y \
clang lld ninja-build cmake git glslang python3 pkgconf-pkg-config \
mesa-libGL-devel libX11-devel libXcursor-devel libXext-devel libXfixes-devel \
libXi-devel libXrandr-devel libXScrnSaver-devel libXtst-devel libxkbcommon-devel \
alsa-lib-devel pulseaudio-libs-devel systemd-devel dbus-devel \
wayland-devel wayland-protocols-devel qt6-qtbase-devel
```
From the repository root, follow the launcher configure, build and install steps in
[Building on Linux](#building-on-linux), omitting `-DCMAKE_PREFIX_PATH="$Qt6_DIR"`; CMake finds
Fedora's packaged Qt 6 automatically.
Run `./_Build/linux/install/launcher` on regular Fedora or inside the container. To launch a
container build from the host, leave the container with `exit`, then run from the repository root:
```bash
distrobox enter kyty-build -- ./_Build/linux/install/launcher
```
Qt libraries and plugins are bundled. You can also launch directly on a Fedora Atomic host
if its other system libraries are compatible with the container build.
### Building on macOS
macOS builds target x86-64 and run under Rosetta 2 on Apple Silicon, so the PS5's x86-64 game
@@ -253,7 +296,7 @@ Requirements:
- An Apple Silicon Mac with Rosetta 2 installed (`softwareupdate --install-rosetta`)
- Xcode (or the Command Line Tools)
- Homebrew packages: `brew install cmake ninja glslang python`
- Qt 6 (Concurrent, Network, Widgets) with x86-64 support. The official Qt installation is
- Qt 6 (Concurrent, Network, and Widgets) with x86-64 support. The official Qt installation is
universal and works; Homebrew's Qt is arm64-only and will not link
```bash
@@ -269,14 +312,14 @@ cmake --install _Build/macos --prefix _Build/macos/install
```
The build re-signs `kyty_emulator` with the JIT entitlements it needs to execute translated
guest code; no manual signing step is required. When the launcher is built, the install
guest code; no manual signing step is required. When the launcher is built, the installation
also produces `_Build/macos/install/KytyPS5.app` — double-click to launch the GUI.
A flat `kyty_emulator` is kept for CLI usage.
Vulkan comes from MoltenVK. Download `MoltenVK-macos.tar` from the
[MoltenVK releases](https://github.com/KhronosGroup/MoltenVK/releases), then copy
`MoltenVK/dynamic/dylib/macOS/libMoltenVK.dylib` next to the flat `kyty_emulator`
(and, for the bundle, into `KytyPS5.app/Contents/Frameworks/`) and ad-hoc sign it:
(and, for the bundle, into `KytyPS5.app/Contents/Frameworks/`) and sign it with an ad hoc signature:
```bash
codesign --force --sign - _Build/macos/install/libMoltenVK.dylib
@@ -344,7 +387,7 @@ for your platform from GitHub, checks its SHA-256 checksum, and restarts after i
any running games first. Settings, saves, patches, and other files outside the release package are
preserved; replacement failures restore the previous files. The installation folder must be writable.
If a package cannot be installed automatically, the updater offers the GitHub release page.
Source and fork builds do not replace themselves with official releases.
Source builds and builds from forks do not replace themselves with official releases.
The emulator can also be started directly with a legally obtained game directory, ELF file, or
ZArchive dump:
@@ -401,5 +444,5 @@ licenses included with those components.
- [InoriRus/Kyty](https://github.com/InoriRus/Kyty) — KytyPS5 is based on a heavily modified version
of the original Kyty project.
- [shadps4-emu/shadPS4](https://github.com/shadps4-emu/shadPS4) — reference for understanding PS4
memory behavior, GPU resource aliasing and cache coherency,
memory behavior, GPU resource aliasing, cache coherency,
and the AVPlayer implementation.
+12
View File
@@ -72,6 +72,10 @@ bool FullscreenEnabled() {
return g_config->fullscreen_enabled;
}
bool HdrEnabled() {
return g_config->hdr_enabled;
}
bool HideCursorEnabled() {
return g_config->hide_cursor_enabled;
}
@@ -96,6 +100,10 @@ bool VulkanValidationEnabled() {
return g_config->vulkan_validation_enabled;
}
bool IsNetworkConnected() {
return g_config->network_connected;
}
bool ShaderValidationEnabled() {
return g_config->shader_validation_enabled;
}
@@ -182,4 +190,8 @@ const Keymap& GetKeymap() {
return g_config->keymap;
}
const std::vector<std::string>& GetGuestArguments() {
return g_config->guest_arguments;
}
} // namespace Config
+7
View File
@@ -53,10 +53,12 @@ struct ConfigOptions {
PresentMode present_mode = PresentMode::Mailbox;
int32_t gpu_index = -1;
bool fullscreen_enabled = false;
bool hdr_enabled = false;
bool hide_cursor_enabled = false;
bool splash_enabled = true;
bool vr_enabled = false;
bool amd_cpu_enabled = false;
bool network_connected = false;
uint32_t vblank_frequency = 60;
uint32_t console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@@ -83,6 +85,8 @@ struct ConfigOptions {
bool red_zone_protection_enabled = false;
#endif
Keymap keymap;
// Ordinary boots need a non-null argv[0]; --guest-exec supplies the caller's arguments.
std::vector<std::string> guest_arguments {""};
};
void Load(const ConfigOptions& cfg);
@@ -98,9 +102,11 @@ uint32_t GetControllerVibrationIntensity();
PresentMode GetPresentMode();
int32_t GetGpuIndex();
bool FullscreenEnabled();
bool HdrEnabled();
bool HideCursorEnabled();
bool VrEnabled();
bool AmdCpuEnabled();
bool IsNetworkConnected();
uint32_t GetVblankFrequency();
uint32_t GetConsoleLanguage();
bool VulkanValidationEnabled();
@@ -136,6 +142,7 @@ bool RedZoneProtectionEnabled();
#endif
const Keymap& GetKeymap();
const std::vector<std::string>& GetGuestArguments();
} // namespace Config
+1
View File
@@ -16,6 +16,7 @@ struct RunOptions {
};
void Run(const RunOptions& options);
int LoadExec(const char* path, const char* const argv[]);
} // namespace Emulator
@@ -1934,13 +1934,7 @@ KYTY_CP_OP_PARSER(CpOpIndirectBuffer) {
auto* indirect_buffer =
reinterpret_cast<const uint32_t*>(buffer[0] | (static_cast<uint64_t>(buffer[1]) << 32u));
uint32_t indirect_num_dw = control & 0xfffffu;
static std::atomic<uint32_t> indirect_log_count {0};
if (indirect_log_count.fetch_add(1) < 128) {
LOGF("\t indirect buffer: addr=0x%016" PRIx64 ", num_dw=%" PRIu32 ", control=0x%08" PRIx32
"\n",
reinterpret_cast<uint64_t>(indirect_buffer), indirect_num_dw, control);
}
const uint32_t indirect_num_dw = control & 0xfffffu;
if (indirect_num_dw == 0) {
return 3;
+4 -4
View File
@@ -10,9 +10,9 @@ namespace {
struct FormatInfo {
BufferFormat format;
uint32_t bytes_per_element;
uint32_t block_compressed_bytes_per_block;
uint32_t render_target_bytes_per_element;
uint8_t bytes_per_element;
uint8_t block_compressed_bytes_per_block;
uint8_t render_target_bytes_per_element;
bool sampled_texture;
bool uint_texture;
bool sint_texture = false;
@@ -21,7 +21,7 @@ struct FormatInfo {
constexpr FormatInfo kFormatInfo[] = {
{BufferFormat::k8UNorm, 1, 0, 1, true, false},
{BufferFormat::k8UScaled, 1, 0, 0, true, false},
{BufferFormat::k8SNorm, 1, 0, 1, false, false},
{BufferFormat::k8SNorm, 1, 0, 1, true, false},
{BufferFormat::k8UInt, 1, 0, 1, true, true},
{BufferFormat::k16UNorm, 2, 0, 2, true, false},
{BufferFormat::k16SNorm, 2, 0, 2, true, false},
+40 -62
View File
@@ -576,12 +576,7 @@ bool GuestGpu::Process(Submission& submission) {
}
}
if (progressed) {
if (complete) {
m_renderer.RunGarbageCollector();
}
cp.BufferFlush();
} else if (complete) {
m_renderer.RunGarbageCollector();
}
break;
}
@@ -602,20 +597,16 @@ bool GuestGpu::Process(Submission& submission) {
complete = cp.Process(submission.command_execution, submission.commands) ==
Pm4ProcessResult::Complete;
if (submission.command_execution.MadeProgress()) {
if (complete) {
m_renderer.RunGarbageCollector();
}
cp.BufferFlush();
} else if (complete) {
m_renderer.RunGarbageCollector();
}
break;
}
case SubmissionType::FlipPreparation:
m_renderer.RunGarbageCollector();
cp.PrepareCpuFlip(submission.flip_request_id);
break;
case SubmissionType::SuspendPoint:
// Age caches at the frame boundary, not once per command list.
m_renderer.RunGarbageCollector();
cp.EmitGlobalBarrier();
m_renderer.GetCommandScheduler().DeferPriorityOperation(
[ready = m_suspend_point_ready] { ready->release(); });
@@ -1072,8 +1063,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
case 0x03: with_interrupt = false; break;
case 0x01:
if (!IsAsyncComputeQueue()) {
Sync::TriggerEopEventAtEndOfPipe(command, m_interrupt_event_id,
interrupt_context_id);
TriggerEopEventAtEndOfPipe(interrupt_context_id);
return;
}
with_interrupt = true;
@@ -1082,24 +1072,15 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
default: EXIT("unknown interrupt_selector\n");
}
auto write32 = [&](bool with_writeback) {
auto write32 = [&] {
auto* dst = static_cast<uint32_t*>(dst_gpu_addr);
auto data = static_cast<uint32_t>(value);
EXIT_IF(dst == nullptr);
(void)command.Handle();
std::memcpy(dst, &data, sizeof(data));
if (with_interrupt) {
if (with_writeback) {
Sync::WriteAtEndOfPipeWithInterruptWriteBack32(m_submit_id, command, dst,
data, m_interrupt_event_id,
interrupt_context_id);
} else {
Sync::WriteAtEndOfPipeWithInterrupt32(m_submit_id, command, dst, data,
m_interrupt_event_id, interrupt_context_id);
}
} else if (with_writeback) {
Sync::WriteAtEndOfPipeWithWriteBack32(m_submit_id, command, dst, data);
} else {
Sync::WriteAtEndOfPipe32(m_submit_id, command, dst, data);
TriggerEopEventAtEndOfPipe(interrupt_context_id);
}
};
@@ -1111,15 +1092,13 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
SynchronizeGpu();
Sync::ReadGds(*m_renderer.GetBufferCache().GetGdsBuffer(), dst, value & 0xffffu,
value >> 16u);
Sync::WriteAtEndOfPipeGds32(m_submit_id, command, dst, value & 0xffffu,
value >> 16u);
if (with_interrupt) {
m_renderer.TriggerInterrupt(m_interrupt_event_id, interrupt_context_id);
}
return;
}
} else if (eop_event_type == 0x04 && cache_action == 0x00 && event_index == 0x05) {
write32(false);
write32();
return;
}
break;
@@ -1128,8 +1107,8 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
if constexpr (sizeof(T) == sizeof(uint32_t)) {
if (event_write_source == 0x02 && eop_event_type == 0x2f && event_index == 0x06) {
switch (cache_action) {
case 0x00: write32(false); return;
case 0x38: write32(true); return;
case 0x00:
case 0x38: write32(); return;
default: break;
}
}
@@ -1137,25 +1116,14 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
if (event_write_source == 0x04) {
value = Sync::ReadReferenceClock();
}
auto write64 = [&](bool with_writeback) {
auto write64 = [&] {
auto* dst = static_cast<uint64_t*>(dst_gpu_addr);
EXIT_IF(dst == nullptr);
(void)command.Handle();
std::memcpy(dst, &value, sizeof(value));
if (with_interrupt) {
if (with_writeback) {
Sync::WriteAtEndOfPipeWithInterruptWriteBack64(
m_submit_id, command, dst, value, m_interrupt_event_id,
interrupt_context_id);
} else {
Sync::WriteAtEndOfPipeWithInterrupt64(m_submit_id, command, dst,
value, m_interrupt_event_id,
interrupt_context_id);
}
} else if (with_writeback) {
Sync::WriteAtEndOfPipeWithWriteBack64(m_submit_id, command, dst,
value);
} else {
Sync::WriteAtEndOfPipe64(m_submit_id, command, dst, value);
TriggerEopEventAtEndOfPipe(interrupt_context_id);
}
};
@@ -1164,7 +1132,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
switch (eop_event_type) {
case 0x04:
if (event_index == 0x05) {
write64(false);
write64();
return;
}
break;
@@ -1172,7 +1140,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
case 0x28:
case 0x2f:
if (event_index == 0x00) {
write64(false);
write64();
return;
}
break;
@@ -1180,7 +1148,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
case 0x2d:
case 0x30:
if (event_index == 0x00 && !with_interrupt) {
write64(false);
write64();
return;
}
break;
@@ -1195,20 +1163,20 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
if (((eop_event_type == 0x04 || eop_event_type == 0x28) &&
event_index == 0x05) ||
(event_index == 0x00)) {
write64(true);
write64();
return;
}
break;
case 0x2b:
case 0x2d:
if (event_index == 0x00 && !with_interrupt) {
write64(true);
write64();
return;
}
break;
case 0x2f:
if (event_index == 0x06 && !with_interrupt) {
write64(true);
write64();
return;
}
break;
@@ -1217,7 +1185,7 @@ void CommandProcessor::WriteAtEndOfPipe(uint32_t cache_policy, uint32_t event_wr
break;
case 0x3b:
if (eop_event_type == 0x04 && event_index == 0x05 && with_interrupt) {
write64(true);
write64();
return;
}
break;
@@ -1270,7 +1238,11 @@ void CommandProcessor::EmitGlobalBarrier() {
}
void CommandProcessor::TriggerEopEventAtEndOfPipe(uint32_t interrupt_context_id) {
Sync::TriggerEopEventAtEndOfPipe(CurrentBuffer(), m_interrupt_event_id, interrupt_context_id);
(void)CurrentBuffer().Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, event_id = m_interrupt_event_id, interrupt_context_id] {
renderer.TriggerInterrupt(event_id, interrupt_context_id);
});
}
void CommandProcessor::TriggerEvent(uint32_t event_type, uint32_t event_index,
@@ -1360,8 +1332,9 @@ void CommandProcessor::Flip() {
auto& command = CurrentBuffer();
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
Sync::WriteAtEndOfPipeOnlyFlip(m_submit_id, command, m_flip.handle, m_flip.index,
m_flip.flip_mode, m_flip.flip_arg, request);
(void)command.Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, request] { renderer.GetVideoOut().CompleteFlip(request); });
GetScheduler().Flush();
}
@@ -1375,12 +1348,13 @@ void CommandProcessor::Flip(void* dst_gpu_addr, uint32_t value) {
reinterpret_cast<uint64_t>(dst_gpu_addr), value);
}
EXIT_IF(dst_gpu_addr == nullptr);
std::memcpy(dst_gpu_addr, &value, sizeof(value));
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
Sync::WriteAtEndOfPipeWithFlip32(m_submit_id, command, static_cast<uint32_t*>(dst_gpu_addr),
value, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg, request);
(void)command.Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, request] { renderer.GetVideoOut().CompleteFlip(request); });
GetScheduler().Flush();
}
@@ -1400,12 +1374,16 @@ void CommandProcessor::FlipWithInterrupt(uint32_t eop_event_type, uint32_t cache
if (eop_event_type != 0x00000004 || cache_action != 0x00000038) {
EXIT("unknown event type\n");
}
EXIT_IF(dst_gpu_addr == nullptr);
std::memcpy(dst_gpu_addr, &value, sizeof(value));
auto request = Sync::PrepareVideoOutFlip(command, m_flip.handle, m_flip.index, m_flip.flip_mode,
m_flip.flip_arg);
Sync::WriteAtEndOfPipeWithInterruptWriteBackFlip32(
m_submit_id, command, static_cast<uint32_t*>(dst_gpu_addr), value, m_flip.handle,
m_flip.index, m_flip.flip_mode, m_flip.flip_arg, request, m_interrupt_event_id);
(void)command.Handle();
GetScheduler().DeferPriorityOperation(
[&renderer = m_renderer, event_id = m_interrupt_event_id, request] {
renderer.GetVideoOut().CompleteFlip(request);
renderer.TriggerInterrupt(event_id, 0);
});
GetScheduler().Flush();
}
+2 -2
View File
@@ -299,8 +299,8 @@ bool TileGetTextureBlockLayout(Prospero::BufferFormat format, Prospero::TileMode
if (!TileGetTextureElementLayout(format, element)) {
return false;
}
if ((family == TileBlockFamily::Depth64KB || family == TileBlockFamily::RenderTarget64KB) &&
Prospero::RenderTargetBytesPerElement(format) != element.bytes) {
if (family == TileBlockFamily::Depth64KB &&
(element.texel_width != 1 || element.texel_height != 1)) {
return false;
}
+1
View File
@@ -33,6 +33,7 @@ struct GraphicContext {
// bool fp64_denorm_preserve = false; // Temporarily disabled.
bool attachment_feedback_loop_enabled = false;
bool provoking_vertex_last_enabled = false;
bool depth_color_copy_enabled = false;
bool supports_block_texel_view = false;
bool mesh_shader_enabled = false;
vk::PhysicalDeviceMeshShaderPropertiesEXT mesh_shader_properties = {};
+84 -4
View File
@@ -36,7 +36,9 @@ constexpr uint64_t NumFramesBeforeRemoval = 32;
[[nodiscard]] bool DecodeColorClear(const TextureCache::ImageDesc& desc, uint8_t code,
vk::ClearColorValue& clear) {
const auto& metadata = desc.info.metadata;
const auto format = desc.view_info.format;
const auto format = desc.info.IsDepth()
? TextureGetSurfaceFormatInfo(desc.info.guest_format).vk_format
: desc.view_info.format;
const bool cmask = metadata.kind == ImageMetadataKind::Cmask;
if (cmask ? code == 0 : code == 0x20) {
// Register clears belong to the color buffer; the texture pipe cannot decode them.
@@ -631,6 +633,9 @@ void TextureCache::CopyImage(ImageId destination_id, ImageId source_id) {
void TextureCache::CopyImageMip(ImageId destination_id, ImageId source_id, uint32_t mip,
uint32_t layer) {
RefreshCopySource(source_id);
if (m_slot_images[destination_id].IsBufferModified()) {
RefreshImage(destination_id);
}
auto& destination = m_slot_images[destination_id];
auto& source = m_slot_images[source_id];
TrackImage(destination_id);
@@ -751,11 +756,63 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
return replacement_id;
}
namespace BcAlias {
bool Matches(const ImageInfo& requested, const ImageInfo& cached) {
return requested.guest_format != cached.guest_format &&
requested.IsBlock() != cached.IsBlock() &&
requested.tile_mode == cached.tile_mode &&
requested.bytes_per_block == cached.bytes_per_block &&
requested.samples == 1 && cached.samples == 1 &&
(requested.data != cached.data || requested.resources != cached.resources ||
requested.BlockExtent() != cached.BlockExtent()) &&
!TextureCache::SameBacking(cached, requested, false);
}
bool Publish(TextureCache& cache, Image& image) {
if (!cache.CanDownload(image)) {
return false;
}
// ObtainBuffer preserves padding and records ownership before the GPU-only transfer.
auto [buffer, offset] =
cache.m_buffer_cache.ObtainBuffer(image.info.data.address, image.info.data.size, true);
cache.DownloadImage(image, *buffer, offset, image.info.data.size, image.info.resources.levels);
image.ClearGpuModified();
image.MarkBufferModified();
return true;
}
void Resolve(TextureCache& cache, ImageId cached_id, ImageId merged_id) {
auto& cached = cache.m_slot_images[cached_id];
auto* merged = merged_id ? &cache.m_slot_images[merged_id] : nullptr;
// PS5 raw block images can span several packed BC mips. Transfer their complete
// guest bytes, including tail padding, before retiring the incompatible backing.
if (cached.SafeToDownload()) {
if (merged && merged->SafeToDownload() && !Publish(cache, *merged)) {
return;
}
if (!Publish(cache, cached)) {
return;
}
if (merged) {
merged->MarkBufferModified();
}
}
cached.binding.needs_rebind |= cached.binding.is_bound || cached.binding.is_target;
cache.FreeImage(cached_id);
}
} // namespace BcAlias
TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& requested,
BindingType binding, ImageId cached_id,
ImageId merged_id) {
auto owner = m_slot_images.try_get(cached_id);
if (owner == nullptr) {
if (owner == nullptr || !owner->registered) {
return {merged_id};
}
if (BcAlias::Matches(requested, owner->info) && !owner->IsCpuDirty()) {
BcAlias::Resolve(*this, cached_id, merged_id);
return {merged_id};
}
auto& cached = *owner;
@@ -1059,8 +1116,15 @@ void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
}
{
std::scoped_lock lock {m_lock};
ClearImage(m_scheduler.Current(), id, view.format,
{vk::ImageAspectFlagBits::eColor, view.base_level, view.level_count,
const auto& image = m_slot_images[id];
const bool depth = image.info.IsDepth();
if (depth) {
const auto value = clear.color.float32[0];
clear.depthStencil = {value, 0u};
}
ClearImage(m_scheduler.Current(), id, depth ? image.backing.format : view.format,
{depth ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor,
view.base_level, view.level_count,
image_first + slice, 1}, clear);
}
// Publish the conversion's expanded keys without treating them as guest writes
@@ -1166,6 +1230,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
std::scoped_lock lock {m_lock};
const auto candidates =
FindImagesInRegion(desc.info.data.address, desc.info.data.size, false);
bool block_alias = false;
for (const auto id: candidates) {
const auto& image = m_slot_images[id];
@@ -1184,6 +1249,9 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
}
if (SameBacking(image.info, desc.info, exact_format)) {
result = id;
} else if (image.SafeToDownload() &&
BcAlias::Matches(desc.info, image.info)) {
block_alias = true;
}
}
@@ -1201,6 +1269,15 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
view_layer = overlap.layer;
}
}
} else if (block_alias) {
for (const auto candidate: candidates) {
const auto* owner = m_slot_images.try_get(candidate);
if (candidate != result && owner != nullptr && owner->registered &&
!owner->IsCpuDirty() &&
BcAlias::Matches(m_slot_images[result].info, owner->info)) {
BcAlias::Resolve(*this, candidate, result);
}
}
}
if (result && !m_slot_images[result].depth_id) {
@@ -1923,6 +2000,9 @@ void TextureCache::RunGarbageCollector() {
}
void TextureCache::ProcessDownloadImages() {
if (!m_readback_linear_images) {
return;
}
std::scoped_lock lock {m_lock};
for (const auto id: m_download_images) {
const auto owner = m_slot_images.try_get(id);
+10
View File
@@ -26,8 +26,15 @@ class BufferCache;
class CommandBuffer;
class CommandScheduler;
class RenderExecutor;
class TextureCache;
struct TextureCacheTestAccess;
namespace BcAlias {
[[nodiscard]] bool Matches(const ImageInfo& requested, const ImageInfo& cached);
[[nodiscard]] bool Publish(TextureCache& cache, Image& image);
void Resolve(TextureCache& cache, ImageId cached, ImageId merged);
} // namespace BcAlias
class TextureCache {
public:
enum class BindingType : uint8_t { Texture, Storage, RenderTarget, DepthTarget, VideoOut };
@@ -181,6 +188,9 @@ private:
friend struct TextureCacheTestAccess;
friend class BufferCache;
friend class RenderExecutor;
friend bool BcAlias::Matches(const ImageInfo&, const ImageInfo&);
friend bool BcAlias::Publish(TextureCache&, Image&);
friend void BcAlias::Resolve(TextureCache&, ImageId, ImageId);
};
} // namespace Libs::Graphics
+3 -12
View File
@@ -394,26 +394,20 @@ static void RcPrint(const char* func, const HW::RenderControl& c) {
LOGF("\t depth_clear_enable = %s\n"
"\t stencil_clear_enable = %s\n"
"\t copy_depth_to_color = %s\n"
"\t copy_stencil_to_color = %s\n"
"\t resummarize_enable = %s\n"
"\t stencil_compress_disable = %s\n"
"\t depth_compress_disable = %s\n"
"\t copy_centroid = %s\n"
"\t copy_sample = %" PRIu8 "\n",
c.depth_clear_enable ? "true" : "false", c.stencil_clear_enable ? "true" : "false",
c.copy_depth_to_color ? "true" : "false", c.copy_stencil_to_color ? "true" : "false",
c.resummarize_enable ? "true" : "false", c.stencil_compress_disable ? "true" : "false",
c.depth_compress_disable ? "true" : "false", c.copy_centroid ? "true" : "false",
c.copy_sample);
}
static void RcCheck(const HW::RenderControl& c) {
// EXIT_NOT_IMPLEMENTED(c.depth_clear_enable != false);
// EXIT_NOT_IMPLEMENTED(c.stencil_clear_enable != false);
// EXIT_NOT_IMPLEMENTED(c.stencil_compress_disable != false);
// EXIT_NOT_IMPLEMENTED(c.depth_compress_disable != false);
EXIT_NOT_IMPLEMENTED(c.copy_centroid != false);
EXIT_NOT_IMPLEMENTED(c.copy_sample != 0);
}
static void McPrint(const char* func, const HW::ModeControl& c) {
LOGF("%s\n", func);
@@ -869,9 +863,6 @@ void hw_check(const CommandBuffer& buffer) {
VpCheck(vp, smc);
log_phase("clip");
ClipCheck(c);
log_phase("rc");
RcCheck(rc);
log_phase("depth");
log_phase("mode");
McCheck(mc);
log_phase("blend");
@@ -298,13 +298,20 @@ void RenderExecutor::ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepth
}
return;
}
if (rc.copy_depth_to_color || rc.copy_stencil_to_color || rc.copy_centroid ||
rc.copy_sample != 0 || dc.zfunc > static_cast<uint8_t>(vk::CompareOp::eAlways) ||
if (rc.copy_stencil_to_color ||
dc.zfunc > static_cast<uint8_t>(vk::CompareOp::eAlways) ||
(!z.depth_view.depth_write_disable && z.z_write_base_addr != z.z_read_base_addr) ||
(has_stencil && !z.depth_view.stencil_write_disable &&
z.stencil_write_base_addr != z.stencil_read_base_addr)) {
DepthFatal("unsupported depth register state");
}
if (rc.copy_depth_to_color &&
(z.z_info.num_samples != 0 || rc.copy_sample != 0 ||
z.z_info.format != Prospero::DepthFormat::kZ32F ||
dc.z_enable || dc.z_write_enable || dc.depth_bounds_enable || dc.stencil_enable ||
rc.depth_clear_enable || rc.stencil_clear_enable)) {
DepthFatal("unsupported depth-to-color copy state");
}
r.desc = MakeDepthTargetDesc(buffer, z);
r.depth_clear_enable = rc.depth_clear_enable;
r.depth_load_clear_enable = r.depth_clear_enable;
@@ -362,8 +362,17 @@ struct VideoOutFormatPolicy {
};
inline constexpr uint64_t VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ = 0x8100070422000000ull;
inline constexpr uint64_t VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ = 0x8100070400000000ull;
inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {{
[[nodiscard]] constexpr bool IsHdrVideoOutPixelFormat(uint64_t format) {
switch (format) {
case VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ:
case VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ: return true;
default: return false;
}
}
inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {{
{0x8000000022000000ull,
{vk::Format::eR8G8B8A8Srgb, Prospero::BufferFormat::k8_8_8_8Srgb, 4, false}},
{0x8000000000000000ull,
@@ -374,6 +383,8 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
{vk::Format::eA2R10G10B10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
{VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ,
{vk::Format::eA2B10G10R10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
{VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ,
{vk::Format::eA2R10G10B10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
{0xc001000622000000ull,
{vk::Format::eR16G16B16A16Sfloat, Prospero::BufferFormat::k16_16_16_16Float, 8, false}},
{0xc001000600000000ull,
@@ -179,7 +179,9 @@ static void ValidateSampledDepthBinding(const ShaderRecompiler::IR::ImageResourc
const ShaderTextureResource& descriptor, const Image& image,
vk::Format view_format, uint64_t size) {
const bool resource_ok = IsSupportedSampledDepthResource(resource);
const bool encoding_ok = IsSupportedDepthTextureEncoding(descriptor, resource.r128);
// Comparison sampling changes the host format, not the guest metadata encoding.
const bool encoding_ok = descriptor.TileMode() != Prospero::TileMode::kDepth ||
IsSupportedDepthTextureEncoding(descriptor, resource.r128);
const bool view_ok =
IsSupportedSampledDepthView(image.info.pixel_format, view_format, descriptor.DstSelXYZW());
if (resource_ok && encoding_ok && view_ok) {
@@ -622,8 +624,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
if (storage) {
ValidateStorageTexture(resource, descriptor, desc.info.data.size);
}
if (!resource.r128 && descriptor.MetaCompress() && tile != Prospero::TileMode::kDepth &&
!desc.info.IsDepth()) {
if (!resource.r128 && descriptor.MetaCompress() && tile != Prospero::TileMode::kDepth) {
TileSizeAlign metadata_size {};
(void)TileGetDccSize(width, height, volume ? depth : image_layers,
desc.info.bytes_per_block, physical_levels, tile, metadata_size,
+3 -7
View File
@@ -39,13 +39,7 @@ enum class CommandBufferDebugOp : uint32_t {
DispatchDirect,
DrawIndex,
DrawIndexAuto,
EopWrite,
EopInterrupt,
EopWriteBack,
EopFlip,
EopWriteBackFlip,
EopOnlyFlip,
DispatchIndirect,
DispatchIndirect = 9,
Unknown,
};
@@ -186,6 +180,8 @@ private:
bool ignore_target_mask = false, bool exact_format = false);
void ResolveRenderDepthTarget(CommandBuffer& buffer, RenderDepthInfo& target);
[[nodiscard]] bool DepthStencilCopy(CommandBuffer& buffer);
[[nodiscard]] bool CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo& draw,
const DrawEmitInfo& emit, DrawRenderState& state);
[[nodiscard]] bool PrepareDrawRenderState(CommandBuffer& buffer,
const DrawCallInfo& draw,
uint32_t render_target_slice_offset,
@@ -1029,12 +1029,71 @@ static void EmitDrawPrimitives(const HW::UserConfig& ucfg, vk::CommandBuffer vk_
}
}
bool RenderExecutor::CopyDepthToColor(CommandBuffer& buffer, const DrawCallInfo& draw,
const DrawEmitInfo& emit, DrawRenderState& state) {
if (!buffer.GetRegisters().GetRenderControl().copy_depth_to_color) {
return false;
}
auto& ucfg = buffer.GetUserConfig();
const auto& hw = buffer.GetRegisters();
auto& depth = state.depth_info;
auto& color = state.color_info[0];
EXIT_NOT_IMPLEMENTED(!m_context.GetGraphics().depth_color_copy_enabled ||
!Prospero::IsRectList(ucfg.GetPrimType()) ||
draw.IsIndexed() || draw.index_count != 3 || draw.instance_count != 1 ||
emit.first_vertex != 0 || emit.first_instance != 0 ||
state.color_count != 1 || color.target_slot != 0 || !depth.image_id ||
color.desc.info.samples != 1 ||
depth.desc.info.extent.width != color.Extent().width ||
depth.desc.info.extent.height != color.Extent().height ||
depth.desc.view_info.layer_count != 1 ||
color.desc.view_info.layer_count != 1 ||
hw.GetBlendControl(0).enable || state.ps_input_info.ps_pixel_kill_enable ||
state.ps_input_info.ps_depth_export_enable ||
state.ps_input_info.ps_sample_mask_export_enable);
vk::ImageAspectFlags feedback;
const auto rendering = AcquireRenderTargets(buffer, &color, 1, depth, feedback);
if (depth.depth_load_clear_enable) {
m_context.GetCommandScheduler().BeginRendering(rendering);
}
auto& cache = m_context.GetTextureCache();
auto& source = cache.GetImage(depth.image_id);
auto& destination = cache.GetImage(color.image_id);
EXIT_NOT_IMPLEMENTED((source.backing.format != vk::Format::eD32Sfloat &&
source.backing.format != vk::Format::eD32SfloatS8Uint) ||
destination.backing.format != vk::Format::eR32Sfloat);
const auto rect = calc_final_scissor(hw.GetScreenViewport(), hw.GetScanModeControl(),
{rendering.width, rendering.height}, 0);
if (rect.right > rect.left && rect.bottom > rect.top) {
const vk::ImageCopy region {
{vk::ImageAspectFlagBits::eDepth, depth.desc.view_info.base_level,
depth.desc.view_info.base_layer, 1}, {rect.left, rect.top, 0},
{vk::ImageAspectFlagBits::eColor, color.desc.view_info.base_level,
color.desc.view_info.base_layer, 1}, {rect.left, rect.top, 0},
{static_cast<uint32_t>(rect.right - rect.left),
static_cast<uint32_t>(rect.bottom - rect.top), 1}};
m_context.GetCommandScheduler().EndRendering();
const auto command = buffer.Handle();
source.Transit(vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead,
{}, command);
destination.Transit(vk::ImageLayout::eTransferDstOptimal,
vk::AccessFlagBits2::eTransferWrite, {}, command);
command.copyImage(source.backing.image, vk::ImageLayout::eTransferSrcOptimal,
destination.backing.image, vk::ImageLayout::eTransferDstOptimal,
1, &region);
}
return true;
}
void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buffer,
const DrawCallInfo& draw, DrawRenderState& state,
vk::PrimitiveTopology topology, const DrawEmitInfo& emit,
const DrawIndexBufferSource& index_source,
bool primitive_restart_enable) {
auto& ucfg = buffer.GetUserConfig();
if (CopyDepthToColor(buffer, draw, emit, state)) {
return;
}
const auto vertex_stages =
std::span {state.vertex_info.data(), state.programs.VertexStageCount()};
const bool mesh_active = state.vertex_info[0].stage.program->stage == ShaderType::Mesh;
-166
View File
@@ -1,10 +1,6 @@
#include "graphics/host_gpu/renderer/sync.h"
#include "common/assert.h"
#include "common/common.h"
#include "common/logging/log.h"
#include "common/threads.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/host_gpu/renderer/cache/bufferCache.h"
#include "graphics/host_gpu/renderer/render.h"
#include "graphics/host_gpu/renderer/renderContext.h"
@@ -53,109 +49,6 @@ uint64_t ReadReferenceClock() {
return value;
}
enum class EndOfPipeWriteSize : uint32_t { Dword = 4, Qword = 8 };
enum class EndOfPipeWriteAction { Write, WriteBack, Interrupt, InterruptWriteBack };
static CommandBufferDebugOp DebugOperation(EndOfPipeWriteAction action) {
switch (action) {
case EndOfPipeWriteAction::Write: return CommandBufferDebugOp::EopWrite;
case EndOfPipeWriteAction::WriteBack:
case EndOfPipeWriteAction::InterruptWriteBack: return CommandBufferDebugOp::EopWriteBack;
case EndOfPipeWriteAction::Interrupt: return CommandBufferDebugOp::EopInterrupt;
}
EXIT("unsupported end-of-pipe write action\n");
return CommandBufferDebugOp::Unknown;
}
static bool TriggersInterrupt(EndOfPipeWriteAction action) {
return action == EndOfPipeWriteAction::Interrupt ||
action == EndOfPipeWriteAction::InterruptWriteBack;
}
static void RecordEndOfPipeWrite(uint64_t submit_id, CommandBuffer& buffer, uint64_t destination,
uint64_t value, EndOfPipeWriteSize size,
EndOfPipeWriteAction action, int interrupt_event_id = 0,
uint32_t context_id = 0) {
EXIT_IF(destination == 0);
(void)buffer.Handle();
const auto width = static_cast<uint32_t>(size);
const auto value_low = static_cast<uint32_t>(value);
const auto value_high = static_cast<uint32_t>(value >> 32u);
const auto operation = static_cast<uint32_t>(DebugOperation(action));
if (TriggersInterrupt(action)) {
buffer.SetDebugInfo(operation, submit_id, width, context_id, value_low, value_high,
destination);
TriggerEopEventAtEndOfPipe(buffer, interrupt_event_id, context_id);
} else {
buffer.SetDebugInfo(operation, submit_id, width, value_low, value_high, 0, destination);
}
}
void WriteAtEndOfPipe32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
uint32_t value) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::Write);
}
void WriteAtEndOfPipeGds32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
uint32_t dw_offset, uint32_t dw_num) {
EXIT_IF(dst_gpu_addr == nullptr);
(void)buffer.Handle();
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopWrite), submit_id,
dw_offset, dw_num, 0, 0, reinterpret_cast<uint64_t>(dst_gpu_addr));
}
void WriteAtEndOfPipe64(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst_gpu_addr,
uint64_t value) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::Write);
}
void WriteAtEndOfPipeWithWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
uint64_t* dst_gpu_addr, uint64_t value) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::WriteBack);
}
void WriteAtEndOfPipeWithWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::WriteBack);
}
void WriteAtEndOfPipeWithInterruptWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
uint32_t context_id) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::InterruptWriteBack,
event_id, context_id);
}
void WriteAtEndOfPipeWithInterruptWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
uint32_t context_id) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::InterruptWriteBack,
event_id, context_id);
}
void WriteAtEndOfPipeWithInterrupt64(uint64_t submit_id, CommandBuffer& buffer,
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
uint32_t context_id) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Qword, EndOfPipeWriteAction::Interrupt, event_id,
context_id);
}
void WriteAtEndOfPipeWithInterrupt32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
uint32_t context_id) {
RecordEndOfPipeWrite(submit_id, buffer, reinterpret_cast<uint64_t>(dst_gpu_addr), value,
EndOfPipeWriteSize::Dword, EndOfPipeWriteAction::Interrupt, event_id,
context_id);
}
uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index, int flip_mode,
int64_t flip_arg) {
for (;;) {
@@ -176,65 +69,6 @@ uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index, int f
}
}
void WriteAtEndOfPipeWithInterruptWriteBackFlip32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value,
int handle, int index, int flip_mode,
int64_t flip_arg, uint64_t request_id,
int event_id) {
EXIT_IF(dst_gpu_addr == nullptr);
(void)buffer.Handle();
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopWriteBackFlip), submit_id,
static_cast<uint32_t>(handle), static_cast<uint32_t>(index),
static_cast<uint32_t>(flip_mode), value, static_cast<uint64_t>(flip_arg));
auto& renderer = buffer.GetContext();
auto& scheduler = renderer.GetCommandScheduler();
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
scheduler.DeferPriorityOperation([&renderer, event_id, request_id] {
renderer.GetVideoOut().CompleteFlip(request_id);
renderer.TriggerInterrupt(event_id, 0);
});
}
void WriteAtEndOfPipeWithFlip32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
uint32_t value, int handle, int index, int flip_mode,
int64_t flip_arg, uint64_t request_id) {
EXIT_IF(dst_gpu_addr == nullptr);
(void)buffer.Handle();
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopFlip), submit_id,
static_cast<uint32_t>(handle), static_cast<uint32_t>(index),
static_cast<uint32_t>(flip_mode), value, static_cast<uint64_t>(flip_arg));
auto& renderer = buffer.GetContext();
auto& scheduler = renderer.GetCommandScheduler();
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
scheduler.DeferPriorityOperation(
[&renderer, request_id] { renderer.GetVideoOut().CompleteFlip(request_id); });
}
void WriteAtEndOfPipeOnlyFlip(uint64_t submit_id, CommandBuffer& buffer, int handle, int index,
int flip_mode, int64_t flip_arg, uint64_t request_id) {
(void)buffer.Handle();
buffer.SetDebugInfo(static_cast<uint32_t>(CommandBufferDebugOp::EopOnlyFlip), submit_id,
static_cast<uint32_t>(handle), static_cast<uint32_t>(index),
static_cast<uint32_t>(flip_mode), 0, static_cast<uint64_t>(flip_arg));
auto& renderer = buffer.GetContext();
auto& scheduler = renderer.GetCommandScheduler();
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
scheduler.DeferPriorityOperation(
[&renderer, request_id] { renderer.GetVideoOut().CompleteFlip(request_id); });
}
void TriggerEopEventAtEndOfPipe(CommandBuffer& buffer, int event_id, uint32_t context_id) {
(void)buffer.Handle();
auto& renderer = buffer.GetContext();
auto& scheduler = renderer.GetCommandScheduler();
EXIT_IF(!scheduler.Active() || &buffer != &scheduler.Current());
scheduler.DeferPriorityOperation(
[&renderer, event_id, context_id] { renderer.TriggerInterrupt(event_id, context_id); });
}
static void InterruptEventResetFunc(LibKernel::EventQueue::KernelEqueueEvent* event) {
EXIT_IF(event == nullptr);
event->triggered = false;
-36
View File
@@ -17,44 +17,8 @@ namespace Sync {
uint64_t& value);
[[nodiscard]] uint64_t ReadReferenceClock();
void TriggerEopEventAtEndOfPipe(CommandBuffer& buffer, int event_id, uint32_t context_id);
void WriteAtEndOfPipe32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
uint32_t value);
void WriteAtEndOfPipe64(uint64_t submit_id, CommandBuffer& buffer, uint64_t* dst_gpu_addr,
uint64_t value);
void WriteAtEndOfPipeGds32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
uint32_t dw_offset, uint32_t dw_num);
void WriteAtEndOfPipeWithWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value);
void WriteAtEndOfPipeWithWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
uint64_t* dst_gpu_addr, uint64_t value);
void WriteAtEndOfPipeWithInterrupt32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
uint32_t context_id = 0);
void WriteAtEndOfPipeWithInterrupt64(uint64_t submit_id, CommandBuffer& buffer,
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
uint32_t context_id = 0);
void WriteAtEndOfPipeWithInterruptWriteBack32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value, int event_id,
uint32_t context_id = 0);
void WriteAtEndOfPipeWithInterruptWriteBack64(uint64_t submit_id, CommandBuffer& buffer,
uint64_t* dst_gpu_addr, uint64_t value, int event_id,
uint32_t context_id = 0);
[[nodiscard]] uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index,
int flip_mode, int64_t flip_arg);
void WriteAtEndOfPipeOnlyFlip(uint64_t submit_id, CommandBuffer& buffer, int handle, int index,
int flip_mode, int64_t flip_arg, uint64_t request_id);
void WriteAtEndOfPipeWithFlip32(uint64_t submit_id, CommandBuffer& buffer, uint32_t* dst_gpu_addr,
uint32_t value, int handle, int index, int flip_mode,
int64_t flip_arg, uint64_t request_id);
void WriteAtEndOfPipeWithInterruptWriteBackFlip32(uint64_t submit_id, CommandBuffer& buffer,
uint32_t* dst_gpu_addr, uint32_t value,
int handle, int index, int flip_mode,
int64_t flip_arg, uint64_t request_id,
int event_id);
int AddEqEvent(RenderContext& renderer, LibKernel::EventQueue::KernelEqueue eq, int id,
void* udata);
int DeleteEqEvent(RenderContext& renderer, LibKernel::EventQueue::KernelEqueue eq, int id);
@@ -1,15 +1,53 @@
#version 450
layout(location = 0) in vec2 uv;
layout(location = 0) out vec4 color;
vec3 SdrToHdr10(vec3 rgb, bool source_linear) {
rgb = clamp(rgb, 0.0, 1.0);
vec3 linear = source_linear ? rgb :
mix(rgb / 12.92, pow((rgb + 0.055) / 1.055, vec3(2.4)),
greaterThan(rgb, vec3(0.04045)));
// Convert primaries and scale 250-nit reference white relative to 10,000 nits.
const mat3 to_rec2020 = (250.0 / 10000.0) *
mat3(0.627404, 0.069097, 0.016391,
0.329283, 0.919540, 0.088013,
0.043313, 0.011362, 0.895595);
vec3 powered = pow(to_rec2020 * linear, vec3(2610.0 / 16384.0));
return pow((3424.0 / 4096.0 + (2413.0 / 128.0) * powered) /
(1.0 + (2392.0 / 128.0) * powered), vec3(2523.0 / 32.0));
}
#ifdef IMGUI_HDR
layout(set = 0, binding = 0) uniform texture2D image;
layout(set = 1, binding = 0) uniform sampler image_sampler;
layout(location = 0) in struct { vec4 Color; vec2 UV; } In;
void main() {
vec4 sampled = In.Color * texture(sampler2D(image, image_sampler), In.UV);
color = vec4(SdrToHdr10(sampled.rgb, false), sampled.a);
}
#else
layout(location = 0) in vec2 uv;
layout(binding = 0) uniform sampler2D source;
layout(push_constant) uniform AlphaControl {
uint premultiplied;
uint flags; // bit 0: premultiplied, bit 1: HDR target, bit 2: PQ source, bit 3: linear source
};
void main() {
color = texture(source, uv);
if (premultiplied == 0u) {
color.rgb *= color.a;
const uint HDR_TARGET = 2u;
const uint PQ_SOURCE = 4u;
vec4 sampled = texture(source, uv);
bool premultiplied = (flags & 1u) != 0u;
bool convert_to_hdr = (flags & (HDR_TARGET | PQ_SOURCE)) == HDR_TARGET;
if (convert_to_hdr && premultiplied) {
sampled.rgb = sampled.a > 0.0 ? sampled.rgb / sampled.a : vec3(0.0);
}
if (convert_to_hdr) {
sampled.rgb = SdrToHdr10(sampled.rgb, (flags & 8u) != 0u);
}
if (!premultiplied || convert_to_hdr) {
sampled.rgb *= sampled.a;
}
color = sampled;
}
#endif
+8
View File
@@ -28,6 +28,14 @@ vk::Format VulkanFormat(Prospero::BufferFormat guest_format);
void RequireVulkanSuccess(vk::Result result, const char* operation);
vk::ShaderModule CompileSPV(std::span<const uint32_t> code, vk::Device device);
[[nodiscard]] constexpr bool IsHdrSwapchainFormat(vk::Format format) {
switch (format) {
case vk::Format::eA2B10G10R10UnormPack32:
case vk::Format::eA2R10G10B10UnormPack32: return true;
default: return false;
}
}
template <typename Handle, typename... Args>
void SetVulkanObjectNameF(vk::Device device, Handle handle, fmt::format_string<Args...> format,
Args&&... args) {
+3 -1
View File
@@ -26,11 +26,13 @@ public:
~Presenter();
KYTY_CLASS_NO_COPY(Presenter);
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info);
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info,
bool hdr = false);
[[nodiscard]] Frame& PrepareBlankFrame(uint32_t width, uint32_t height, bool opaque,
CommandBuffer* producer = nullptr);
[[nodiscard]] bool PresentLastFrame();
[[nodiscard]] bool IsGuestPaused() const noexcept;
[[nodiscard]] bool IsHdrSupported() const noexcept;
[[nodiscard]] bool NeedsSystemOverlayRefresh() const noexcept;
[[nodiscard]] RenderContext& Renderer() const noexcept;
void Present(Frame& frame);
@@ -7,6 +7,7 @@
#include "common/file.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "gpu_blit_shaders/gpu_imgui_hdr_spv.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/presentation/ddsImage.h"
#include "imgui.h"
@@ -890,6 +891,11 @@ struct SystemOverlay::Impl {
info.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount = 1;
info.PipelineInfoMain.PipelineRenderingCreateInfo.pColorAttachmentFormats = &color_format;
info.CheckVkResultFn = CheckVulkanResult;
if (IsHdrSwapchainFormat(format)) {
info.CustomShaderFragCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
info.CustomShaderFragCreateInfo.codeSize = sizeof(GPU_IMGUI_HDR_SPV);
info.CustomShaderFragCreateInfo.pCode = GPU_IMGUI_HDR_SPV;
}
EXIT_IF(!ImGui_ImplVulkan_Init(&info));
vulkan_initialized = true;
}
+33 -31
View File
@@ -272,6 +272,7 @@ public:
void Init(uint32_t width, uint32_t height);
FlipQueue& GetFlipQueue() { return m_flip_queue; }
Graphics::RenderContext& Renderer() const noexcept { return m_renderer; }
bool IsHdrSupported() const noexcept { return m_presenter.IsHdrSupported(); }
void VblankBegin();
void VblankEnd();
@@ -558,14 +559,6 @@ Graphics::ImageInfo BufferAttributeGroup::ImageInfo(const VideoOutBuffer& buffer
if (!Graphics::DecodeVideoOutPixelFormat(attribute.pixel_format, pixel_format)) {
EXIT("unsupported video-out pixel format: 0x%016" PRIx64 "\n", attribute.pixel_format);
}
if (attribute.pixel_format == Graphics::VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ) {
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
if (!warned.test_and_set(std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(fmt::format(
"Warning: HDR-to-SDR conversion is not implemented; displaying PQ video-out "
"pixels unchanged (format=0x{:016x}).\n", attribute.pixel_format));
}
}
const auto tile_mode = Graphics::Prospero::TileMode::kRenderTarget;
Graphics::ImageInfo info {};
info.data = {buffer.data_address, 0};
@@ -1071,31 +1064,31 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
uint32_t height = 0;
bool current = false;
bool premultiplied_alpha = false;
bool hdr = false;
{
Common::LockGuard lock(cfg->mutex);
current = cfg->opened && !cfg->closing && cfg->generation == generation;
if (current) {
if (special) {
width = cfg->width;
height = cfg->height;
} else {
const auto& surface = cfg->buffers[index];
if (!surface.Occupied()) {
EXIT("cannot prepare flip from an unregistered surface, id=%" PRIu64
" index=%d\n",
request_id, index);
}
if (surface.group_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX ||
!cfg->groups[surface.group_index].occupied) {
EXIT("video-out surface references an unavailable attribute group, id=%" PRIu64
" index=%d group=%d\n",
request_id, index, surface.group_index);
}
source_info = cfg->groups[surface.group_index].ImageInfo(surface);
premultiplied_alpha =
(cfg->groups[surface.group_index].attribute.option &
VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED) != 0;
if (current && special) {
width = cfg->width;
height = cfg->height;
} else if (current) {
const auto& surface = cfg->buffers[index];
if (!surface.Occupied()) {
EXIT("cannot prepare flip from an unregistered surface, id=%" PRIu64
" index=%d\n",
request_id, index);
}
if (surface.group_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX ||
!cfg->groups[surface.group_index].occupied) {
EXIT("video-out surface references an unavailable attribute group, id=%" PRIu64
" index=%d group=%d\n",
request_id, index, surface.group_index);
}
const auto& group = cfg->groups[surface.group_index];
source_info = group.ImageInfo(surface);
hdr = Graphics::IsHdrVideoOutPixelFormat(group.attribute.pixel_format);
premultiplied_alpha = (group.attribute.option &
VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED) != 0;
}
}
if (!current) {
@@ -1108,12 +1101,18 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
}
return;
}
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
if (hdr && !m_presenter.IsHdrSupported() && !warned.test_and_set(std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
"Warning: HDR output is unavailable; HDR-to-SDR conversion is not implemented, "
"displaying PQ video-out pixels unchanged.\n");
}
Graphics::Presenter::Frame* frame = nullptr;
if (special) {
frame = &m_presenter.PrepareBlankFrame(width, height, index == VIDEO_OUT_BUFFER_INDEX_BLACK,
&buffer);
} else {
frame = &m_presenter.PrepareFrame(buffer, source_info);
frame = &m_presenter.PrepareFrame(buffer, source_info, hdr);
}
Common::LockGuard lock(m_mutex);
@@ -1790,11 +1789,14 @@ KYTY_SYSV_ABI int VideoOutGetOutputStatus(int handle, VideoOutOutputStatus* stat
int32_t attribute3 = 0;
Loader::SystemContentParamSfoGetInt("ATTRIBUTE3", &attribute3);
int32_t attribute = 0;
const bool hdr_allowed = !Loader::SystemContentParamSfoGetInt("ATTRIBUTE", &attribute) ||
(attribute & 0x20000000) != 0;
ctx->mutex.Lock();
// Primary output reports 4K unless param.json Video-out Info enables resolution detection.
status->resolution =
((attribute3 & 4) != 0 && ctx->width < 3840 && ctx->height < 2160 ? 1u : 2u);
status->dynamicRange = 1;
status->dynamicRange = hdr_allowed && DriverState().IsHdrSupported() ? 2u : 1u;
status->refreshRate =
(ctx->output_mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ || Config::GetVblankFrequency() >= 119
? VIDEO_OUT_REFRESH_RATE_119_88HZ
+67 -16
View File
@@ -17,6 +17,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <limits>
#include <memory>
#include <vector>
@@ -31,6 +32,8 @@ struct Presenter::Frame {
vk::ImageView view = nullptr;
uint64_t present_tick = 0;
bool busy = false;
bool hdr = false;
bool blank = false;
void Configure(GraphicContext& graphics, vk::Extent2D extent, vk::Format format);
void Transit(vk::CommandBuffer command, vk::ImageLayout layout, vk::AccessFlags2 access);
@@ -274,14 +277,24 @@ public:
return static_cast<uint32_t>(m_images.size());
}
[[nodiscard]] vk::Format Format() const noexcept { return m_format; }
[[nodiscard]] bool IsHdrSupported() const noexcept { return m_hdr_supported.load(); }
bool SetHdr(bool hdr) {
const bool changed = m_hdr_active != (hdr && IsHdrSupported());
m_hdr_requested = hdr;
return changed;
}
private:
vk::SurfaceFormatKHR SelectFormat();
void Destroy();
void DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer);
WindowContext& m_window;
vk::SwapchainKHR m_handle = nullptr;
vk::Format m_format = vk::Format::eUndefined;
std::atomic_bool m_hdr_supported {false};
bool m_hdr_requested = false;
bool m_hdr_active = false;
vk::Extent2D m_extent {};
// Drawable pixel size observed when this swapchain was created.
vk::Extent2D m_window_extent {};
@@ -346,6 +359,40 @@ struct Presenter::Impl {
std::atomic<uint64_t> presented_overlay_revision {0};
};
vk::SurfaceFormatKHR Swapchain::SelectFormat() {
const auto& formats = m_window.surface_capabilities.formats;
auto& graphics = m_window.graphic_ctx;
const auto hdr_format = std::ranges::find_if(formats, [&](const auto& candidate) {
return candidate.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT &&
IsHdrSwapchainFormat(candidate.format) &&
static_cast<bool>(
graphics.GetFormatProperties(candidate.format).optimalTilingFeatures &
vk::FormatFeatureFlagBits::eBlitDst);
});
m_hdr_supported = Config::HdrEnabled() && hdr_format != formats.end();
m_hdr_active = m_hdr_requested && IsHdrSupported();
if (m_hdr_active) {
return *hdr_format;
}
if (Config::HdrEnabled() && !IsHdrSupported()) {
LOGF("HDR10 surface unavailable; using SDR output\n");
}
if (formats.size() == 1 && formats.front().format == vk::Format::eUndefined) {
return {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear};
}
for (const auto& candidate: formats) {
if (candidate.colorSpace != vk::ColorSpaceKHR::eSrgbNonlinear) {
continue;
}
switch (candidate.format) {
case vk::Format::eB8G8R8A8Unorm:
case vk::Format::eR8G8B8A8Unorm: return candidate;
default: break;
}
}
EXIT("no supported UNORM swapchain format\n");
}
void Swapchain::Create() {
auto& graphics = m_window.graphic_ctx;
EXIT_IF(graphics.device == nullptr);
@@ -385,18 +432,7 @@ void Swapchain::Create() {
? vk::CompositeAlphaFlagBitsKHR::eOpaque
: vk::CompositeAlphaFlagBitsKHR::eInherit;
vk::SurfaceFormatKHR format {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear};
if (surface.formats.size() != 1 || surface.formats.front().format != vk::Format::eUndefined) {
const auto it = std::find_if(surface.formats.begin(), surface.formats.end(),
[](const vk::SurfaceFormatKHR& candidate) {
return candidate.format == vk::Format::eB8G8R8A8Unorm ||
candidate.format == vk::Format::eR8G8B8A8Unorm;
});
if (it == surface.formats.end()) {
EXIT("no supported UNORM swapchain format\n");
}
format = *it;
}
const auto format = SelectFormat();
m_format = format.format;
const auto swapchain_features = graphics.GetFormatProperties(m_format).optimalTilingFeatures;
if (!static_cast<bool>(swapchain_features & vk::FormatFeatureFlagBits::eBlitDst)) {
@@ -436,6 +472,7 @@ void Swapchain::Create() {
RequireVulkanSuccess(graphics.device.createSwapchainKHR(&create_info, nullptr, &m_handle),
"vkCreateSwapchainKHR");
EXIT_IF(m_handle == nullptr);
LOGF("Vulkan presentation: %s\n", m_hdr_active ? "HDR10 (PQ)" : "SDR");
m_images = EnumerateVulkan<vk::Image>(
"vkGetSwapchainImagesKHR", [&](uint32_t* count, vk::Image* images) {
@@ -692,9 +729,11 @@ void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& l
write.descriptorType = vk::DescriptorType::eCombinedImageSampler;
write.pImageInfo = &image;
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eGraphics, m_overlay_layout, 0, 1, &write);
const uint32_t premultiplied = layer.premultiplied_alpha;
const uint32_t flags = (layer.premultiplied_alpha ? 1u : 0u) |
(m_hdr_active ? 2u : 0u) | (frame.hdr ? 4u : 0u) |
(frame.image.format == vk::Format::eR16G16B16A16Sfloat ? 8u : 0u);
command.pushConstants(m_overlay_layout, vk::ShaderStageFlagBits::eFragment, 0,
sizeof(premultiplied), &premultiplied);
sizeof(flags), &flags);
command.bindPipeline(vk::PipelineBindPoint::eGraphics, m_overlay_pipeline);
vk::RenderingAttachmentInfo attachment {};
attachment.imageView = m_image_views[m_image_index];
@@ -868,7 +907,7 @@ Presenter::Presenter(WindowContext& window): m_impl(std::make_unique<Impl>(windo
Presenter::~Presenter() = default;
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info) {
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(buffer.IsInvalid());
auto frame_format = info.pixel_format;
@@ -878,6 +917,8 @@ Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo
default: break;
}
auto* frame = m_impl->frames.Acquire({info.extent.width, info.extent.height}, frame_format);
frame->hdr = hdr;
frame->blank = false;
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
auto& image = m_impl->ResolveSurface(info);
if (image.backing.format == vk::Format::eUndefined) {
@@ -894,6 +935,8 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height,
KYTY_PROFILER_FUNCTION();
auto format = m_impl->frames.GetFormat();
auto* frame = m_impl->frames.Acquire({width, height}, format);
frame->hdr = false;
frame->blank = true;
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
frame->Configure(m_impl->window.graphic_ctx, {width, height}, format);
vk::ClearColorValue clear {};
@@ -922,6 +965,10 @@ bool Presenter::IsGuestPaused() const noexcept {
return m_impl->window.loop.paused.load(std::memory_order_acquire);
}
bool Presenter::IsHdrSupported() const noexcept {
return m_impl->swapchain.IsHdrSupported();
}
bool Presenter::NeedsSystemOverlayRefresh() const noexcept {
const auto visual = GetSystemOverlayVisualState();
return visual.active ||
@@ -967,8 +1014,12 @@ void Presenter::Impl::Present() {
KYTY_PROFILER_FUNCTION();
const auto overlay_visual = GetSystemOverlayVisualState();
// Keep black/blank flips in the current color space; only real main-bus frames switch it.
const auto* main_frame = layers[0].frame;
const bool hdr_changed = main_frame != nullptr && !main_frame->blank &&
swapchain.SetHdr(main_frame->hdr);
// Some window systems keep presenting an old swapchain after a resize.
if (swapchain.NeedsResize()) {
if (hdr_changed || swapchain.NeedsResize()) {
RecoverSwapchain(Swapchain::Status::Recreate);
}
for (uint32_t attempt = 0; attempt < 2; attempt++) {
@@ -478,6 +478,13 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
provoking_vertex.pNext = supported_features2.pNext;
supported_features2.pNext = &provoking_vertex;
}
const bool maintenance8_extension =
HasExtension(device_extensions, VK_KHR_MAINTENANCE_8_EXTENSION_NAME);
vk::PhysicalDeviceMaintenance8FeaturesKHR maintenance8 {};
if (maintenance8_extension) {
maintenance8.pNext = supported_features2.pNext;
supported_features2.pNext = &maintenance8;
}
const bool image_atomic_int64_extension =
HasExtension(device_extensions, VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME);
vk::PhysicalDeviceShaderImageAtomicInt64FeaturesEXT image_atomic_int64 {};
@@ -542,6 +549,9 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
graphics.attachment_feedback_loop_enabled =
feedback_extensions && feedback_layout.attachmentFeedbackLoopLayout &&
feedback_dynamic.attachmentFeedbackLoopDynamicState;
graphics.depth_color_copy_enabled = maintenance8_extension && maintenance8.maintenance8;
LOGF("Vulkan depth/color copy support: %s\n",
graphics.depth_color_copy_enabled ? "true" : "false");
LOGF("Vulkan depth feedback support: %s\n",
graphics.attachment_feedback_loop_enabled ? "true" : "false");
if (graphics.mesh_shader_enabled) {
@@ -638,6 +648,10 @@ static vk::Device VulkanCreateDevice(GraphicContext& graphics,
image_atomic_int64.sparseImageInt64Atomics = VK_FALSE;
create_info.pNext = &image_atomic_int64;
}
if (graphics.depth_color_copy_enabled) {
maintenance8.pNext = const_cast<void*>(create_info.pNext);
create_info.pNext = &maintenance8;
}
create_info.pQueueCreateInfos = &queue_create_info;
create_info.queueCreateInfoCount = 1;
create_info.enabledExtensionCount = static_cast<uint32_t>(device_extensions.size());
@@ -671,6 +685,11 @@ static void VulkanGetExtensions(VulkanExtensions& r) {
});
r.enable_validation_layers = Config::VulkanValidationEnabled();
if (Config::HdrEnabled() &&
HasExtension(r.available_extensions, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME) &&
!HasExtension(r.required_extensions, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) {
r.required_extensions.push_back(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME);
}
if (HasExtension(r.available_extensions, VK_EXT_DEBUG_UTILS_EXTENSION_NAME)) {
r.required_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
@@ -1002,6 +1021,7 @@ void WindowContext::CreateVulkan() {
graphic_ctx.memory_budget_ext_enabled = true;
}
for (const auto* extension: {VK_EXT_ROBUSTNESS_2_EXTENSION_NAME,
VK_KHR_MAINTENANCE_8_EXTENSION_NAME,
VK_EXT_SHADER_IMAGE_ATOMIC_INT64_EXTENSION_NAME,
VK_EXT_PROVOKING_VERTEX_EXTENSION_NAME,
VK_EXT_MESH_SHADER_EXTENSION_NAME,
@@ -323,25 +323,25 @@ uint32_t QueryDimensions(ValueEmitContext& ctx, const IR::MemoryInfo& mem,
return vector;
}
uint32_t PackedOffset(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
const ImageSampleLayout& layout, ImageDimension dimension) {
uint32_t PackedOffset(ValueEmitContext& ctx, IR::Value value, ImageDimension dimension,
uint32_t& mask) {
const auto components = ImageDimensionInfoFor(dimension).spatial_components;
const auto zero = ConstantI32(ctx.state, 0);
if (layout.offset == NoImageComponent || mem.image_address_components <= layout.offset) {
if (components == 1u) return zero;
const auto result = ctx.state.builder.AllocateId();
if (components == 3u) {
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 3),
result, zero, zero, zero);
} else {
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 2),
result, zero, zero);
uint32_t values[3] {};
if (value.IsImmediate()) {
// RDNA2 packs signed six-bit texel offsets into the low bits of each byte.
for (uint32_t index = 0; index < components; index++) {
const auto bits = (value.U32() >> (index * 8u)) & 0x3fu;
values[index] = ConstantI32(ctx.state, static_cast<int32_t>(bits ^ 0x20u) - 0x20);
}
return result;
mask |= spv::ImageOperandsConstOffsetMask;
return components == 1u
? values[0]
: ctx.state.builder.Constant(spv::OpConstantComposite,
TypeI32Vector(ctx.state, components),
std::span(values).first(components));
}
const auto packed = Unary(ctx.state, spv::OpBitcast, TypeI32(ctx.state),
AddressU32(ctx, mem, address, layout.offset));
uint32_t values[3] = {zero, zero, zero};
mask |= spv::ImageOperandsOffsetMask;
const auto packed = Unary(ctx.state, spv::OpBitcast, TypeI32(ctx.state), ctx.Def(value));
for (uint32_t index = 0; index < components; index++) {
values[index] = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpBitFieldSExtract, TypeI32(ctx.state), values[index],
@@ -350,13 +350,8 @@ uint32_t PackedOffset(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR
}
if (components == 1u) return values[0];
const auto result = ctx.state.builder.AllocateId();
if (components == 3u) {
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 3),
result, values[0], values[1], values[2]);
} else {
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 2),
result, values[0], values[1]);
}
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, components),
result, std::span(values).first(components));
return result;
}
@@ -794,21 +789,28 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto layout = Layout(mem);
const auto numeric_class = image.numeric_class;
const bool dref = HasFlag(mem, Decoder::ImageSampleFlagCompare);
IR::Value offset_value;
if (layout.offset != NoImageComponent) {
const auto value = address->Arg(layout.offset).Resolve();
offset_value = IR::ResolveActiveU32(value, inst.Arg(inst.NumArgs() - 1u).Resolve());
if (offset_value.IsEmpty()) offset_value = value;
}
if (dref && state.program.info.images[mem.resource].conversion_format !=
Prospero::BufferFormat::kInvalid) {
ctx.Fail(inst, "uses depth comparison with a converted image");
return;
}
if (op == IR::ValueOpcode::ImageGatherRaw) {
const auto coord = CoordF32(ctx, mem, *address, layout.coord,
dimension_info.coordinate_components, image.cube);
if (dimension == ImageDimension::Dim1D) {
if (dref || !HasFlag(mem, Decoder::ImageSampleFlagLevelZero) ||
if (image.indirect_root == mem.resource || dref ||
!HasFlag(mem, Decoder::ImageSampleFlagLevelZero) ||
HasFlag(mem, Decoder::ImageSampleFlagOffset) ||
HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
ctx.Fail(inst, "has an unsupported 1D gather variant");
return;
}
const auto coord = CoordF32(ctx, mem, *address, layout.coord,
dimension_info.coordinate_components, image.cube);
const auto sample = EmitOneDimensionalGatherLz(ctx, mem, coord, numeric_class);
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample),
numeric_class, false, mem, true));
@@ -829,32 +831,38 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
} else {
component_or_dref = ConstantU32(state, ImageGatherSource(state, mem));
}
uint32_t operand_mask = 0;
uint32_t offset = 0;
if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
operand_mask = spv::ImageOperandsConstOffsetsMask;
offset = HorizontalOffsets(state, dimension);
} else if (layout.offset != NoImageComponent) {
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();
const std::array operands {operand_mask, offset};
state.builder.AddFunction(
dref ? spv::OpImageDrefGather : spv::OpImageGather, result_type,
sample, sampled, coord, component_or_dref,
std::span(operands).first(operand_mask != 0u ? 2u : 0u));
return sample;
const auto EmitGather = [&](uint32_t resource, uint32_t array_index) {
const auto& candidate = state.program.info.images[resource];
const auto dimension = candidate.dimension;
const auto coord = CoordF32(
ctx, mem, *address, layout.coord,
ImageDimensionInfoFor(dimension).coordinate_components, candidate.cube);
uint32_t operand_mask = 0;
uint32_t offset = 0;
if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
operand_mask = spv::ImageOperandsConstOffsetsMask;
offset = HorizontalOffsets(state, dimension);
} else if (layout.offset != NoImageComponent) {
offset = PackedOffset(ctx, offset_value, dimension, operand_mask);
}
const auto EmitMip = [&](uint32_t mip) {
const auto sampled = MakeSampledImage(state, resource, sampler_id, mip, array_index);
const auto sample = state.builder.AllocateId();
const std::array operands {operand_mask, offset};
state.builder.AddFunction(
dref ? spv::OpImageDrefGather : spv::OpImageGather, result_type,
sample, sampled, coord, component_or_dref,
std::span(operands).first(operand_mask != 0u ? 2u : 0u));
return sample;
};
return candidate.mip_mode == IR::ImageMipMode::Dynamic && candidate.mip_count > 1u
? EmitIndexSwitch(
state, GatherMip(ctx, inst, mem, *address, layout, candidate.mip_count),
candidate.mip_count, result_type, EmitMip)
: EmitMip(0u);
};
const auto sample = image.mip_mode == IR::ImageMipMode::Dynamic && image.mip_count > 1u
? EmitIndexSwitch(
state, GatherMip(ctx, inst, mem, *address, layout,
image.mip_count),
image.mip_count, result_type, EmitGather)
: EmitGather(0);
const auto sample = EmitImageAccess(ctx, inst, result_type, EmitGather);
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample),
result_numeric_class, false, mem, true));
return;
@@ -878,7 +886,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
dref_value = AddressF32(ctx, mem, *address, layout.dref);
}
}
std::array<uint32_t, 3> operands {};
std::array<uint32_t, 4> operands {};
size_t operand_count = 0;
if (HasFlag(mem, Decoder::ImageSampleFlagDerivative)) {
operands[0] = spv::ImageOperandsGradMask;
@@ -908,9 +916,20 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
state, ctx.Def(handle->Arg(0)), static_cast<uint32_t>(samplers.size()),
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, samplers[ordinal]); });
}
if (layout.offset != NoImageComponent &&
!offset_value.IsImmediate()) {
ctx.Fail(inst, "has a nonconstant sample texel offset");
}
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler, sampler_index);
const auto EmitSample = [&](uint32_t resource, uint32_t array_index) {
const auto& candidate = state.program.info.images[resource];
auto sample_operands = operands;
auto sample_operand_count = operand_count;
if (layout.offset != NoImageComponent) {
sample_operand_count = std::max(size_t{1}, sample_operand_count);
sample_operands[sample_operand_count++] = PackedOffset(
ctx, offset_value, candidate.dimension, sample_operands[0]);
}
const auto coord =
CoordF32(ctx, mem, *address, layout.coord,
ImageDimensionInfoFor(candidate.dimension).coordinate_components,
@@ -920,7 +939,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto sample = state.builder.AllocateId();
state.builder.AddFunction(opcode, result_type, sample, sampled, coord,
std::span(&dref_value, dref ? 1u : 0u),
std::span<const uint32_t>(operands).first(operand_count));
std::span<const uint32_t>(sample_operands).first(sample_operand_count));
return sample;
};
auto result = EmitImageAccess(ctx, inst, result_type, EmitSample);
@@ -687,7 +687,8 @@ void Translator::IMAGE_SAMPLE(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::ImageSampleRaw, {resource, sampler, address},
const auto result = ir.Emit(IR::ValueOpcode::ImageSampleRaw,
{resource, sampler, address, ir.GetExec()},
AddMemoryInfo(memory, inst.pc));
const bool dref = (memory.image_sample_flags & Decoder::ImageSampleFlagCompare) != 0u;
if (dref && memory.data_bits != 16u) {
@@ -714,7 +715,7 @@ void Translator::IMAGE_GATHER(const Decoder::Instruction& inst) {
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)},
has_lod ? sampler.Instruction()->Arg(2) : IR::Value(0u), ir.GetExec()},
AddMemoryInfo(memory, inst.pc));
for (uint32_t index = 0; index < memory.data_dwords; index++) {
WriteOperand(OffsetOperand(inst.dst, index),
@@ -1163,9 +1163,11 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
}
entry_ir.SetExec(initial_exec);
const auto initial_mask = entry_ir.Emit(IR::ValueOpcode::Ballot, {initial_exec});
entry_ir.SetExecLo(entry_ir.CompositeExtract(initial_mask, 0));
entry_ir.SetExecHi(options.wave_size == 64u ? entry_ir.CompositeExtract(initial_mask, 1)
: IR::U32(IR::Value(0u)));
const auto initial_lo = entry_ir.CompositeExtract(initial_mask, 0);
const auto initial_hi = options.wave_size == 64u ? entry_ir.CompositeExtract(initial_mask, 1)
: IR::U32(IR::Value(0u));
entry_ir.SetExecLo(initial_lo);
entry_ir.SetExecHi(initial_hi);
if (options.stage == ShaderType::Compute) {
const auto* cs = options.input_info.compute;
const auto thread_ids =
@@ -1381,11 +1383,16 @@ IR::Program TranslateProgram(const Decoder::Program& decoded, const CFG::Graph&
}
} else if (options.stage == ShaderType::Vertex) {
// Vulkan owns primitive assembly; each vertex subgroup is one NGG wave.
// Keep its full lane extent: mbcnt(-1) uses lane ordinals, not active counts.
// ESVertCount describes input vertices, independently of lane ordinals.
auto vertex_count = IR::U32(entry_ir.Emit(IR::ValueOpcode::BitCount32, {initial_lo}));
if (options.wave_size == 64u) {
vertex_count = entry_ir.IAdd(vertex_count, IR::U32(entry_ir.Emit(
IR::ValueOpcode::BitCount32, {initial_hi})));
}
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(2),
IR::U32(IR::Value(options.wave_size << 12u)));
entry_ir.SetScalarReg(static_cast<IR::ScalarReg>(3),
IR::U32(IR::Value((1u << 28u) | options.wave_size)));
entry_ir.BitwiseOr(vertex_count, IR::U32(IR::Value(1u << 28u))));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(5),
builtin(IR::StageInputKind::VertexIndex));
entry_ir.SetVectorReg(static_cast<IR::VectorReg>(8),
@@ -3,6 +3,7 @@
#include "graphics/shader/recompiler/BufferFormat.h"
#include <fmt/format.h>
#include <xbyak/xbyak.h>
#include <map>
#include <new>
#include <unordered_map>
@@ -99,6 +100,9 @@ bool EquivalentValue(const ResourcePlan& program, Value left, Value right,
} // namespace
ResourcePlan::ResourcePlan() = default;
ResourcePlan::ResourcePlan(ResourcePlan&&) noexcept = default;
ResourcePlan::~ResourcePlan() {
for (auto& inst: value_storage) {
inst.Invalidate();
+7 -2
View File
@@ -21,6 +21,10 @@
#include <string_view>
#include <vector>
namespace Xbyak {
class CodeGenerator;
}
namespace Libs::Graphics::ShaderRecompiler::IR {
enum class ResourceKind {
@@ -573,12 +577,12 @@ struct ResourcePlan {
uint64_t generation = 0;
};
ResourcePlan() = default;
ResourcePlan();
~ResourcePlan();
ResourcePlan(const ResourcePlan&) = delete;
ResourcePlan& operator=(const ResourcePlan&) = delete;
ResourcePlan(ResourcePlan&&) noexcept = default;
ResourcePlan(ResourcePlan&&) noexcept;
ResourcePlan& operator=(ResourcePlan&& other) noexcept;
ShaderType stage = ShaderType::Unknown;
@@ -597,6 +601,7 @@ struct ResourcePlan {
bool resource_tracking_complete = false;
ShaderInfo info;
UniformFillPlan uniform_fill;
std::unique_ptr<Xbyak::CodeGenerator> srt_code;
// GPU-thread scratch for nested clean/EXEC memos, activity and material keys.
mutable std::deque<EvaluationContext> evaluation_contexts;
mutable uint32_t evaluation_value_count = 0;
@@ -317,8 +317,8 @@ VALUE_OPCODE(BvhIntersect, U32x4, U32x4, ImageAddress, U1)
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, U32, U32, U32, U32)
VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U1)
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U32, U32, U32, U32, U1)
VALUE_OPCODE(ImageAtomicCompareSwap32, U32, ImageResource, ImageAddress, U32, U32, U1)
VALUE_OPCODE(ImageAtomicSwap32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicSwap64, U64, ImageResource, ImageAddress, U64, U1)
@@ -989,23 +989,29 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
// Keep only that Boolean structure, never the varying shader dependency graph.
Value unknown;
bool capture_control_reads = false;
std::unordered_map<const Inst*, Value> predicates;
std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value {
value = value.Resolve();
if (value.IsEmpty()) return {};
const auto* inst = value.TryInstruction();
if (inst == nullptr)
return ValidateRuntimeValue(program, value, RuntimeValueType::Integer) ? value : Value {};
// Preserve shared EXEC predicates, including subgraphs with no uniform factor.
const auto [found, inserted] = predicates.try_emplace(inst);
if (!inserted) return found->second;
auto& result = found->second;
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) {
capture_control_reads |=
!ValidateRuntimeValue(program, value, RuntimeValueType::ImmutableInteger);
return Clone(value);
return result = Clone(value);
}
const auto* inst = value.TryInstruction();
if (inst == nullptr) return {};
const auto op = inst->GetOpcode();
if (op == ValueOpcode::ConditionRef || op == ValueOpcode::LogicalNot) {
const auto operand = ClonePredicate(inst->Arg(0));
if (operand.IsEmpty() || op == ValueOpcode::ConditionRef) return operand;
if (operand.IsEmpty() || op == ValueOpcode::ConditionRef) return result = operand;
auto& node = plan.value_storage.emplace_back(op);
node.SetArg(0, operand);
return Value(&node);
return result = Value(&node);
}
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr) return {};
auto left = ClonePredicate(inst->Arg(0));
@@ -1019,7 +1025,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
auto& node = plan.value_storage.emplace_back(op);
node.SetArg(0, left);
node.SetArg(1, right);
return Value(&node);
return result = Value(&node);
};
plan.control_flow = ResourceControlFlow(program, ClonePredicate);
plan.uniform_fill = AnalyzeUniformFill(program);
@@ -1064,6 +1070,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
}
if (capture_indirect_reads || !plan.source_reads.empty())
plan.resource_tracking_complete &= !program.has_address_writes;
CompileSrtWalker(plan);
return plan;
}
@@ -1405,6 +1412,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
}
EXIT_IF(image.indirect_root == memory.resource &&
inst.GetOpcode() != ValueOpcode::ImageSampleRaw &&
inst.GetOpcode() != ValueOpcode::ImageGatherRaw &&
inst.GetOpcode() != ValueOpcode::ImageRead);
}
}
@@ -1,12 +1,17 @@
#include "graphics/shader/recompiler/ir/passes/SrtWalker.h"
#include "common/abi.h"
#include "common/assert.h"
#include "graphics/shader/recompiler/ir/ShaderIR.h"
#include <xbyak/xbyak.h>
#include <algorithm>
#include <bit>
#include <cmath>
#include <cstring>
#include <map>
#include <unordered_map>
#include <unordered_set>
namespace Libs::Graphics::ShaderRecompiler::IR {
@@ -359,6 +364,131 @@ private:
} // namespace
void CompileSrtWalker(ResourcePlan& program) {
if (!program.resource_tracking_complete || !program.srt_plan_complete ||
program.capture_specialization_reads || !program.control_flow.empty() ||
program.srt_reads.empty()) return;
using Reads = std::map<int32_t, const SrtRead*>;
std::map<uint32_t, Reads> roots;
std::unordered_map<const Inst*, Reads> pointer_uses;
std::unordered_map<const Inst*, int32_t> offsets;
for (const auto& read: program.srt_reads) {
const auto* inst = read.value.Resolve().TryInstruction();
if (inst == nullptr || inst->GetOpcode() != ValueOpcode::LoadAddressU32 ||
inst->NumArgs() < 2 || !IsRawRead(program, *inst) ||
read.flat_offset >= program.srt_reads.size()) return;
const auto offset = inst->Arg(1).Resolve();
if (!offset.IsImmediate() || offset.GetType() != Type::U32) return;
const auto& memory = program.memory_info[inst->Flags<SrtReadFlags>().index];
const auto relative =
(static_cast<int64_t>(static_cast<int32_t>(memory.offset)) & ~int64_t {3}) +
static_cast<int64_t>(offset.U32() & ~3u);
if (relative < INT32_MIN || relative > INT32_MAX ||
!offsets.emplace(inst, static_cast<int32_t>(relative)).second) return;
}
uint32_t user_data_count = 0;
for (const auto& read: program.srt_reads) {
const auto* inst = read.value.ResolveInstruction();
const auto* handle = inst->Arg(0).Resolve().TryInstruction();
if (handle == nullptr || handle->GetOpcode() != ValueOpcode::GetAddressResource ||
handle->NumArgs() != 2) return;
const auto* low = handle->Arg(0).Resolve().TryInstruction();
const auto* high = handle->Arg(1).Resolve().TryInstruction();
if (low == nullptr || high == nullptr) return;
Reads* uses = nullptr;
if (low->GetOpcode() == ValueOpcode::GetUserData &&
high->GetOpcode() == ValueOpcode::GetUserData && low->NumArgs() == 1 &&
high->NumArgs() == 1 && low->Arg(0).GetType() == Type::ScalarReg &&
high->Arg(0).GetType() == Type::ScalarReg) {
const auto reg = RegIndex(low->Arg(0).ScalarRegister());
if (reg < program.user_data_base ||
RegIndex(high->Arg(0).ScalarRegister()) != reg + 1u) return;
const auto base = reg - program.user_data_base;
user_data_count = std::max(user_data_count, base + 2u);
uses = &roots[base];
} else {
const auto lo = offsets.find(low);
const auto hi = offsets.find(high);
if (lo == offsets.end() || hi == offsets.end() ||
static_cast<int64_t>(lo->second) + 4 != hi->second ||
!EquivalentValue(program, low->Arg(0), high->Arg(0))) return;
uses = &pointer_uses[low];
}
if (!uses->emplace(offsets.at(inst), &read).second) return;
}
std::unordered_set<const Inst*> visited;
const auto reachable = [&](auto&& self, const Reads& reads) -> bool {
for (const auto& [offset, read]: reads) {
const auto* inst = read->value.ResolveInstruction();
const auto child = pointer_uses.find(inst);
if (child != pointer_uses.end() &&
(!visited.insert(inst).second || !self(self, child->second))) return false;
}
return true;
};
for (const auto& [reg, reads]: roots) {
if (!reachable(reachable, reads)) return;
}
if (visited.size() != pointer_uses.size()) return;
program.srt_code = std::make_unique<Xbyak::CodeGenerator>(4096, Xbyak::AutoGrow);
auto& code = *program.srt_code;
Xbyak::Label failed, finished;
code.push(code.rbp);
code.mov(code.rbp, code.rsp);
code.cmp(code.r8, user_data_count);
code.jb(failed, Xbyak::CodeGenerator::T_NEAR);
code.mov(code.r9, AddressMask & ~uint64_t {3});
// Follow shadPS4's VisitPointer: copy one table level in offset order, then
// descend through its child pointers. Seed the descriptor evaluator's memo.
const auto visit = [&](auto&& self, int32_t pointer_offset, const Reads& reads) -> void {
code.push(code.rdi);
code.mov(code.rdi, code.qword[code.rdi + pointer_offset]);
code.and_(code.rdi, code.r9);
const auto first = reads.begin()->first;
const auto last = reads.rbegin()->first;
if (first < 0) {
code.mov(code.r10, -static_cast<int64_t>(first));
code.cmp(code.rdi, code.r10);
code.jb(failed, Xbyak::CodeGenerator::T_NEAR);
}
if (last > 0) {
code.mov(code.r10, AddressMask - last);
code.cmp(code.rdi, code.r10);
code.ja(failed, Xbyak::CodeGenerator::T_NEAR);
}
for (const auto& [offset, read]: reads) {
const auto index = read->value.ResolveInstruction()->EvaluationIndex(program.evaluation_value_count);
using Entry = ResourcePlan::EvaluationContext::Entry;
const auto slot = code.rdx + index * sizeof(Entry);
code.mov(code.r10d, code.dword[code.rdi + offset]);
code.mov(code.dword[code.rsi + read->flat_offset * sizeof(uint32_t)], code.r10d);
code.mov(code.qword[slot + offsetof(Entry, value)], code.r10);
code.mov(code.qword[slot + offsetof(Entry, generation)], code.rcx);
}
for (const auto& [offset, read]: reads) {
const auto child = pointer_uses.find(read->value.ResolveInstruction());
if (child == pointer_uses.end()) continue;
self(self, offset, child->second);
}
code.pop(code.rdi);
};
for (const auto& [reg, reads]: roots) {
visit(visit, reg * sizeof(uint32_t), reads);
}
code.mov(code.eax, 1);
code.jmp(finished, Xbyak::CodeGenerator::T_NEAR);
code.L(failed);
code.xor_(code.eax, code.eax);
code.L(finished);
code.mov(code.rsp, code.rbp);
code.pop(code.rbp);
code.ret();
code.readyRE();
EXIT_IF(Xbyak::GetError() != 0);
}
SrtWalker::SrtWalker(const ResourcePlan& program, const SrtRuntime& runtime,
SrtWalker* clean_evaluator, Value active_mask)
: m_program(program), m_runtime(runtime), m_clean_evaluator(clean_evaluator),
@@ -954,6 +1084,15 @@ bool SrtWalker::EvaluateDescriptor(uint32_t source, DescriptorValue& result) {
bool SrtWalker::RefreshFlatBuffer(std::vector<uint32_t>& flat) {
if (!m_program.srt_plan_complete) return false;
if (m_program.srt_code && m_runtime.read_memory == nullptr) {
flat.resize(m_program.srt_reads.size());
m_program.active_sources.clear();
m_context.values.resize(m_program.evaluation_value_count);
using Walker = bool KYTY_SYSV_ABI (*)(const uint32_t*, uint32_t*,
ResourcePlan::EvaluationContext::Entry*, uint64_t, size_t);
return m_program.srt_code->getCode<Walker>()(m_runtime.user_data.data(), flat.data(),
m_context.values.data(), m_context.generation, m_runtime.user_data.size());
}
const auto refresh = [&](uint32_t slot) {
if (slot >= m_program.srt_reads.size()) return false;
const auto& read = m_program.srt_reads[slot];
@@ -45,6 +45,7 @@ enum class RuntimeValueType { Any, Integer, ImmutableInteger };
bool ValidateRuntimeValue(const ResourcePlan& program, Value value,
RuntimeValueType type = RuntimeValueType::Any);
void CompileSrtWalker(ResourcePlan& program);
// Uses the strict reader for values that affect shader specialization.
SrtRuntime CleanRuntime(SrtRuntime runtime);
+436 -384
View File
@@ -16,394 +16,446 @@
<property name="locale">
<locale language="English" country="UnitedStates"/>
</property>
<layout class="QGridLayout" name="gridLayout">
<property name="horizontalSpacing">
<number>8</number>
</property>
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="sectionsLayout" columnstretch="1,1">
<property name="horizontalSpacing">
<number>8</number>
</property>
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="0">
<widget class="QGroupBox" name="user_profile_group">
<property name="title">
<string>User profile</string>
<layout class="QVBoxLayout" name="dialogLayout">
<property name="spacing"><number>8</number></property>
<item>
<widget class="QTabWidget" name="settings_tabs">
<property name="currentIndex"><number>0</number></property>
<widget class="QWidget" name="settings_tab">
<attribute name="title"><string>General</string></attribute>
<layout class="QGridLayout" name="gridLayout">
<property name="horizontalSpacing">
<number>8</number>
</property>
<layout class="QFormLayout" name="userProfileLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_user_name">
<property name="text"><string>User name:</string></property>
</widget>
</item>
<item row="0" column="1">
<widget class="MandatoryLineEdit" name="lineEdit_user_name">
<property name="toolTip"><string>Local user name exposed to games</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_user_id">
<property name="text"><string>User ID:</string></property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spinBox_user_id">
<property name="toolTip"><string>Local user ID exposed to games</string></property>
<property name="maximum"><number>2147483647</number></property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_console_language">
<property name="text"><string>Console language:</string></property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_console_language">
<property name="toolTip"><string>Language</string></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="audio_group">
<property name="title"><string>Audio</string></property>
<layout class="QFormLayout" name="audioLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_audio_input_device">
<property name="text"><string>Microphone:</string></property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_audio_input_device"/>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" rowspan="3">
<widget class="QGroupBox" name="graphics_group">
<property name="title">
<string>Graphics &amp;&amp; display</string>
<property name="verticalSpacing">
<number>8</number>
</property>
<layout class="QFormLayout" name="graphicsLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_screen_resolution">
<property name="text"><string>Screen resolution:</string></property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_screen_resolution">
<property name="toolTip"><string>Window resolution</string></property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_present_mode">
<property name="text"><string>Present mode:</string></property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_present_mode">
<property name="toolTip"><string>Vulkan swapchain presentation mode</string></property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_vblank_frequency">
<property name="text"><string>Vblank frequency:</string></property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="spinBox_vblank_frequency">
<property name="toolTip"><string>Virtual display refresh rate used for frame pacing</string></property>
<property name="minimum"><number>30</number></property>
<property name="maximum"><number>360</number></property>
<property name="value"><number>60</number></property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_shader_optimization">
<property name="text"><string>Shader optimization:</string></property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_shader_optimization_type">
<property name="toolTip"><string>Optimize shaders for code size or performance</string></property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_gpu">
<property name="text"><string>GPU:</string></property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="comboBox_gpu">
<property name="toolTip"><string>Graphics device used by the emulator</string></property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="checkBox_fullscreen">
<property name="toolTip"><string>Run in borderless desktop fullscreen</string></property>
<property name="text"><string>Fullscreen</string></property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="checkBox_readback">
<property name="toolTip"><string>Read back writable linear images</string></property>
<property name="text"><string>Enable readback</string></property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="checkBox_tessellation">
<property name="toolTip"><string>Draw tessellated geometry. Disabled by default; unsupported tessellation may cause crashes.</string></property>
<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>
<item row="9" column="1">
<widget class="QCheckBox" name="checkBox_skip_notice_screen">
<property name="toolTip"><string>Skip startup logos and notices in supported games.</string></property>
<property name="text"><string>Skip boot logos</string></property>
</widget>
</item>
<item row="10" column="1">
<widget class="QCheckBox" name="checkBox_sync_raw_image_buffers">
<property name="toolTip"><string>Can fix missing textures and incorrect transparency in some games. May reduce performance.</string></property>
<property name="text"><string>Accurate texture synchronization</string></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0">
<widget class="QGroupBox" name="notifications_group">
<property name="title"><string>Notifications</string></property>
<layout class="QVBoxLayout" name="notificationsLayout">
<item>
<widget class="QCheckBox" name="checkBox_trophy_notifications">
<property name="toolTip"><string>Show trophy unlock notifications and play their sound.
To use your own sound, replace the emulator's assets/sounds/trophy-unlock.wav with a WAV file and restart the game.</string></property>
<property name="text"><string>Enable trophy notifications</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="4" column="0" colspan="2">
<widget class="QGroupBox" name="debug_group">
<property name="title">
<string>Debugging &amp;&amp; logs</string>
</property>
<layout class="QGridLayout" name="debugLayout" columnstretch="0,1,0,1">
<property name="horizontalSpacing"><number>8</number></property>
<property name="verticalSpacing"><number>6</number></property>
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_vulkan_validation">
<property name="toolTip"><string>Enable Vulkan validation layers</string></property>
<property name="text"><string>Vulkan validation</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
<item row="0" column="2" colspan="2">
<widget class="QCheckBox" name="checkBox_shader_validation">
<property name="toolTip"><string>Validate SPIR-V binary</string></property>
<property name="text"><string>Shader validation</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_renderdoc_capture">
<property name="toolTip"><string>Enable RenderDoc capture</string></property>
<property name="text"><string>RenderDoc capture</string></property>
</widget>
</item>
<item row="1" column="2" colspan="2">
<widget class="QCheckBox" name="checkBox_cmd_dump">
<property name="toolTip"><string>Dump command buffers</string></property>
<property name="text"><string>Command buffer dump</string></property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_shader_log_direction">
<property name="text"><string>Shader logging:</string></property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_shader_log_direction">
<property name="toolTip"><string>Dump shaders to file or console window. If enabled may decrease emulator performance</string></property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="label_shader_log_folder">
<property name="text"><string>Shader log folder:</string></property>
</widget>
</item>
<item row="2" column="3">
<widget class="MandatoryLineEdit" name="lineEdit_shader_log_folder">
<property name="toolTip"><string>Specify directory to dump shaders</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_printf_direction">
<property name="text"><string>Printf output:</string></property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_printf_direction">
<property name="toolTip"><string>Print logs to file or console window. If enabled may decrease emulator performance</string></property>
</widget>
</item>
<item row="3" column="2">
<widget class="QLabel" name="label_printf_file">
<property name="text"><string>Printf output file:</string></property>
</widget>
</item>
<item row="3" column="3">
<widget class="MandatoryLineEdit" name="lineEdit_printf_file">
<property name="toolTip"><string>Specify file to dump logs</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="checkBox_profiler">
<property name="toolTip"><string>Enable the Tracy profiler. Profiling may decrease emulator performance</string></property>
<property name="text"><string>Tracy profiler</string></property>
</widget>
</item>
<item row="4" column="2">
<widget class="QLabel" name="label_cmd_dump_folder">
<property name="text"><string>Command buffer folder:</string></property>
</widget>
</item>
<item row="4" column="3">
<widget class="MandatoryLineEdit" name="lineEdit_cmd_dump_folder">
<property name="toolTip"><string>Specify directory to dump command buffers</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="compatibility_group">
<property name="title">
<string>Compatibility</string>
</property>
<layout class="QVBoxLayout" name="compatibilityLayout">
<item>
<widget class="QCheckBox" name="checkBox_amd_cpu">
<property name="toolTip"><string>Patch CPU instructions that differ between AMD and the host CPU.</string></property>
<property name="text"><string>AMD CPU patch (experimental)</string></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_red_zone_protection">
<property name="toolTip"><string>Helps prevent crashes in some games on Windows</string></property>
<property name="text"><string>Windows SysV red zone crash protection (experimental)</string></property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<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 speaker over USB and Bluetooth. 50% is the previous default; 100% adds 6 dB of gain.</string></property>
<property name="maximum"><number>100</number></property>
<property name="value"><number>50</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>50%</string></property>
</widget>
</item>
</layout>
</item>
</layout>
<item row="0" column="0" colspan="2">
<layout class="QGridLayout" name="sectionsLayout" columnstretch="1,1">
<property name="horizontalSpacing">
<number>8</number>
</property>
<property name="verticalSpacing">
<number>8</number>
</property>
<item row="0" column="0">
<widget class="QGroupBox" name="user_profile_group">
<property name="title">
<string>User profile</string>
</property>
<layout class="QFormLayout" name="userProfileLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_user_name">
<property name="text"><string>User name:</string></property>
</widget>
</item>
<item row="0" column="1">
<widget class="MandatoryLineEdit" name="lineEdit_user_name">
<property name="toolTip"><string>Local user name exposed to games</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_user_id">
<property name="text"><string>User ID:</string></property>
</widget>
</item>
<item row="1" column="1">
<widget class="QSpinBox" name="spinBox_user_id">
<property name="toolTip"><string>Local user ID exposed to games</string></property>
<property name="maximum"><number>2147483647</number></property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_console_language">
<property name="text"><string>Console language:</string></property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_console_language">
<property name="toolTip"><string>Language</string></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="audio_group">
<property name="title"><string>Audio</string></property>
<layout class="QFormLayout" name="audioLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_audio_input_device">
<property name="text"><string>Microphone:</string></property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_audio_input_device"/>
</item>
</layout>
</widget>
</item>
<item row="0" column="1" rowspan="3">
<widget class="QGroupBox" name="graphics_group">
<property name="title">
<string>Graphics &amp;&amp; display</string>
</property>
<layout class="QFormLayout" name="graphicsLayout">
<property name="fieldGrowthPolicy">
<enum>QFormLayout::AllNonFixedFieldsGrow</enum>
</property>
<item row="0" column="0">
<widget class="QLabel" name="label_screen_resolution">
<property name="text"><string>Screen resolution:</string></property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="comboBox_screen_resolution">
<property name="toolTip"><string>Window resolution</string></property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_present_mode">
<property name="text"><string>Present mode:</string></property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="comboBox_present_mode">
<property name="toolTip"><string>Vulkan swapchain presentation mode</string></property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_vblank_frequency">
<property name="text"><string>Vblank frequency:</string></property>
</widget>
</item>
<item row="2" column="1">
<widget class="QSpinBox" name="spinBox_vblank_frequency">
<property name="toolTip"><string>Virtual display refresh rate used for frame pacing</string></property>
<property name="minimum"><number>30</number></property>
<property name="maximum"><number>360</number></property>
<property name="value"><number>60</number></property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_shader_optimization">
<property name="text"><string>Shader optimization:</string></property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_shader_optimization_type">
<property name="toolTip"><string>Optimize shaders for code size or performance</string></property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label_gpu">
<property name="text"><string>GPU:</string></property>
</widget>
</item>
<item row="4" column="1">
<widget class="QComboBox" name="comboBox_gpu">
<property name="toolTip"><string>Graphics device used by the emulator</string></property>
</widget>
</item>
<item row="5" column="1">
<widget class="QCheckBox" name="checkBox_readback">
<property name="toolTip"><string>Read back writable linear images</string></property>
<property name="text"><string>Enable readback</string></property>
</widget>
</item>
<item row="6" column="1">
<widget class="QCheckBox" name="checkBox_tessellation">
<property name="toolTip"><string>Draw tessellated geometry. Disabled by default; unsupported tessellation may cause crashes.</string></property>
<property name="text"><string>Enable tessellation support (experimental)</string></property>
</widget>
</item>
<item row="7" column="1">
<widget class="QCheckBox" name="checkBox_sync_raw_image_buffers">
<property name="toolTip"><string>Can fix missing textures and incorrect transparency in some games. May reduce performance.</string></property>
<property name="text"><string>Accurate texture synchronization</string></property>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="checkBox_hdr">
<property name="toolTip"><string>Enable HDR10 output for supported games. Requires HDR enabled on the host display.</string></property>
<property name="text"><string>Enable HDR display support</string></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="0">
<widget class="QGroupBox" name="notifications_group">
<property name="title"><string>Notifications</string></property>
<layout class="QVBoxLayout" name="notificationsLayout">
<item>
<widget class="QCheckBox" name="checkBox_trophy_notifications">
<property name="toolTip"><string>Show trophy unlock notifications and play their sound.
To use your own sound, replace the emulator's assets/sounds/trophy-unlock.wav with a WAV file and restart the game.</string></property>
<property name="text"><string>Enable trophy notifications</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="3" column="1">
<widget class="QGroupBox" name="general_group">
<property name="title"><string>General</string></property>
<layout class="QVBoxLayout" name="generalLayout">
<item>
<widget class="QCheckBox" name="checkBox_fullscreen">
<property name="toolTip"><string>Run in borderless desktop fullscreen</string></property>
<property name="text"><string>Run in fullscreen</string></property>
</widget>
</item>
<item>
<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>
<item>
<widget class="QCheckBox" name="checkBox_skip_notice_screen">
<property name="toolTip"><string>Skip startup logos and notices in supported games.</string></property>
<property name="text"><string>Skip boot logos</string></property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="1" column="0">
<widget class="QGroupBox" name="compatibility_group">
<property name="title">
<string>Compatibility</string>
</property>
<layout class="QVBoxLayout" name="compatibilityLayout">
<item>
<widget class="QCheckBox" name="checkBox_amd_cpu">
<property name="toolTip"><string>Patch CPU instructions that differ between AMD and the host CPU.</string></property>
<property name="text"><string>AMD CPU patch (experimental)</string></property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="checkBox_red_zone_protection">
<property name="toolTip"><string>Helps prevent crashes in some games on Windows</string></property>
<property name="text"><string>Windows SysV red zone crash protection (experimental)</string></property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
<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 speaker over USB and Bluetooth. 50% is the previous default; 100% adds 6 dB of gain.</string></property>
<property name="maximum"><number>100</number></property>
<property name="value"><number>50</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>50%</string></property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="network_tab">
<attribute name="title"><string>Network</string></attribute>
<layout class="QVBoxLayout" name="networkTabLayout">
<item>
<widget class="QCheckBox" name="checkBox_network_connected">
<property name="toolTip"><string>Let games use your computer's local network connection.</string></property>
<property name="text"><string>Enable network connection</string></property>
</widget>
</item>
<item>
<spacer name="networkSpacer">
<property name="orientation"><enum>Qt::Vertical</enum></property>
<property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property>
</spacer>
</item>
</layout>
</widget>
<widget class="QWidget" name="debug_tab">
<attribute name="title"><string>Debug</string></attribute>
<layout class="QVBoxLayout" name="debugTabLayout">
<item>
<widget class="QGroupBox" name="debug_group">
<property name="title">
<string>Debugging &amp;&amp; logs</string>
</property>
<layout class="QGridLayout" name="debugLayout" columnstretch="0,1,0,1">
<property name="horizontalSpacing"><number>8</number></property>
<property name="verticalSpacing"><number>6</number></property>
<item row="0" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_vulkan_validation">
<property name="toolTip"><string>Enable Vulkan validation layers</string></property>
<property name="text"><string>Vulkan validation</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
<item row="0" column="2" colspan="2">
<widget class="QCheckBox" name="checkBox_shader_validation">
<property name="toolTip"><string>Validate SPIR-V binary</string></property>
<property name="text"><string>Shader validation</string></property>
<property name="checked"><bool>true</bool></property>
</widget>
</item>
<item row="1" column="0" colspan="2">
<widget class="QCheckBox" name="checkBox_renderdoc_capture">
<property name="toolTip"><string>Enable RenderDoc capture</string></property>
<property name="text"><string>RenderDoc capture</string></property>
</widget>
</item>
<item row="1" column="2" colspan="2">
<widget class="QCheckBox" name="checkBox_cmd_dump">
<property name="toolTip"><string>Dump command buffers</string></property>
<property name="text"><string>Command buffer dump</string></property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_shader_log_direction">
<property name="text"><string>Shader logging:</string></property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="comboBox_shader_log_direction">
<property name="toolTip"><string>Dump shaders to file or console window. If enabled may decrease emulator performance</string></property>
</widget>
</item>
<item row="2" column="2">
<widget class="QLabel" name="label_shader_log_folder">
<property name="text"><string>Shader log folder:</string></property>
</widget>
</item>
<item row="2" column="3">
<widget class="MandatoryLineEdit" name="lineEdit_shader_log_folder">
<property name="toolTip"><string>Specify directory to dump shaders</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_printf_direction">
<property name="text"><string>Printf output:</string></property>
</widget>
</item>
<item row="3" column="1">
<widget class="QComboBox" name="comboBox_printf_direction">
<property name="toolTip"><string>Print logs to file or console window. If enabled may decrease emulator performance</string></property>
</widget>
</item>
<item row="3" column="2">
<widget class="QLabel" name="label_printf_file">
<property name="text"><string>Printf output file:</string></property>
</widget>
</item>
<item row="3" column="3">
<widget class="MandatoryLineEdit" name="lineEdit_printf_file">
<property name="toolTip"><string>Specify file to dump logs</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
<item row="4" column="0">
<widget class="QCheckBox" name="checkBox_profiler">
<property name="toolTip"><string>Enable the Tracy profiler. Profiling may decrease emulator performance</string></property>
<property name="text"><string>Tracy profiler</string></property>
</widget>
</item>
<item row="4" column="2">
<widget class="QLabel" name="label_cmd_dump_folder">
<property name="text"><string>Command buffer folder:</string></property>
</widget>
</item>
<item row="4" column="3">
<widget class="MandatoryLineEdit" name="lineEdit_cmd_dump_folder">
<property name="toolTip"><string>Specify directory to dump command buffers</string></property>
<property name="clearButtonEnabled"><bool>true</bool></property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="debugSpacer">
<property name="orientation"><enum>Qt::Vertical</enum></property>
<property name="sizeHint" stdset="0"><size><width>20</width><height>40</height></size></property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item row="2" column="0" colspan="2">
<item>
<layout class="QHBoxLayout" name="buttonLayout">
<item>
<widget class="QPushButton" name="clear_button">
+4
View File
@@ -117,12 +117,14 @@ public:
PresentMode present_mode = PresentMode::Mailbox;
int gpu_index = -1;
bool fullscreen_enabled = false;
bool hdr_enabled = false;
bool hide_cursor_enabled = false;
bool readback_linear_images = false;
bool sync_raw_image_buffers = false;
bool tessellation_enabled = false;
bool trophy_enabled = true;
bool skip_notice_screen = false;
bool network_connected = false;
int vblank_frequency = 60;
int console_language = DEFAULT_CONSOLE_LANGUAGE;
bool vulkan_validation_enabled = false;
@@ -152,12 +154,14 @@ public:
present_mode = other.present_mode;
gpu_index = other.gpu_index;
fullscreen_enabled = other.fullscreen_enabled;
hdr_enabled = other.hdr_enabled;
hide_cursor_enabled = other.hide_cursor_enabled;
readback_linear_images = other.readback_linear_images;
sync_raw_image_buffers = other.sync_raw_image_buffers;
tessellation_enabled = other.tessellation_enabled;
trophy_enabled = other.trophy_enabled;
skip_notice_screen = other.skip_notice_screen;
network_connected = other.network_connected;
vblank_frequency = other.vblank_frequency;
console_language = other.console_language;
vulkan_validation_enabled = other.vulkan_validation_enabled;
+4
View File
@@ -10,12 +10,14 @@ QVariantMap Configuration::GameSettings() const {
KYTY_CFG_SET(present_mode);
KYTY_CFG_SET(gpu_index);
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(hdr_enabled);
KYTY_CFG_SET(hide_cursor_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(sync_raw_image_buffers);
KYTY_CFG_SET(tessellation_enabled);
KYTY_CFG_SET(trophy_enabled);
KYTY_CFG_SET(skip_notice_screen);
KYTY_CFG_SET(network_connected);
KYTY_CFG_SET(vblank_frequency);
KYTY_CFG_SET(console_language);
KYTY_CFG_SET(vulkan_validation_enabled);
@@ -55,12 +57,14 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
present_mode = PresentMode::Mailbox;
}
KYTY_CFG_GET(fullscreen_enabled);
KYTY_CFG_GET(hdr_enabled);
KYTY_CFG_GET(hide_cursor_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(sync_raw_image_buffers);
KYTY_CFG_GET(tessellation_enabled);
trophy_enabled = s.value("trophy_enabled", trophy_enabled).toBool();
KYTY_CFG_GET(skip_notice_screen);
KYTY_CFG_GET(network_connected);
vblank_frequency = s.value("vblank_frequency", vblank_frequency).toInt();
console_language = s.value("console_language", console_language).toInt();
if (console_language < 0 || console_language > MAX_CONSOLE_LANGUAGE) {
@@ -273,11 +273,13 @@ 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_hdr->setChecked(info.hdr_enabled);
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
m_ui->checkBox_tessellation->setChecked(info.tessellation_enabled);
m_ui->checkBox_skip_notice_screen->setChecked(info.skip_notice_screen);
m_ui->checkBox_network_connected->setChecked(info.network_connected);
m_ui->checkBox_trophy_notifications->setChecked(info.trophy_enabled);
m_ui->spinBox_vblank_frequency->setValue(info.vblank_frequency);
m_ui->comboBox_console_language->clear();
@@ -434,11 +436,13 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
TextToEnum<Configuration::PresentMode>(ui.comboBox_present_mode->currentText());
info.gpu_index = ui.comboBox_gpu->currentIndex() - 1;
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
info.hdr_enabled = ui.checkBox_hdr->isChecked();
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
info.tessellation_enabled = ui.checkBox_tessellation->isChecked();
info.skip_notice_screen = ui.checkBox_skip_notice_screen->isChecked();
info.network_connected = ui.checkBox_network_connected->isChecked();
info.trophy_enabled = ui.checkBox_trophy_notifications->isChecked();
info.vblank_frequency = ui.spinBox_vblank_frequency->value();
info.console_language = ui.comboBox_console_language->currentIndex();
@@ -463,6 +467,14 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
}
void ConfigurationEditDialog::save() {
for (auto* field: {m_ui->lineEdit_shader_log_folder, m_ui->lineEdit_printf_file,
m_ui->lineEdit_cmd_dump_folder}) {
if (field->isEnabled() && field->text().isEmpty()) {
m_ui->settings_tabs->setCurrentWidget(m_ui->debug_tab);
field->setFocus();
break;
}
}
if (MandatoryLineEdit::FindEmpty(this)) {
QMessageBox::critical(this, tr("Save failed"), tr("Please fill all mandatory fields"));
return;
+4
View File
@@ -245,6 +245,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
if (info.fullscreen_enabled) {
args << "--fullscreen";
}
if (info.hdr_enabled) {
args << "--hdr";
}
if (info.hide_cursor_enabled) {
args << "--hide-cursor";
}
@@ -252,6 +255,7 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
args << "--sync-raw-image-buffers" << BoolArg(info.sync_raw_image_buffers);
args << "--trophy-notifications" << BoolArg(info.trophy_enabled);
args << "--skip-notice-screen" << BoolArg(info.skip_notice_screen);
args << "--network-connected" << BoolArg(info.network_connected);
if (info.tessellation_enabled) {
args << "--tessellation";
}
+46 -138
View File
@@ -57,7 +57,6 @@ struct DriverDmem {
};
static DriverDmem g_driver_dmem;
static int InitializeDmem();
static void ReleaseDmem();
} // namespace Gen5Driver
@@ -82,6 +81,8 @@ void Initialize() {
auto& video_out = VideoOut::VideoOutInit(width, height, presenter);
g_renderer = &presenter.Renderer();
g_renderer->InitializeGpu(&video_out);
// Native driver module startup reserves its DMEM before the guest can allocate it.
EXIT_IF(Gen5Driver::Initialize() != OK);
ShaderInit();
}
@@ -258,29 +259,6 @@ struct CommandBuffer {
void* user_data;
uint32_t reserved_dw;
void DbgDump() const {
if (!Config::GraphicsDebugDumpEnabled() ||
Config::GetPrintfDirection() == Config::LogDirection::Silent) {
return;
}
static std::atomic<uint32_t> log_count {0};
if (log_count.fetch_add(1) >= 64) {
return;
}
LOGF("\t bottom = 0x%016" PRIx64 "\n"
"\t top = 0x%016" PRIx64 "\n"
"\t cursor_up = 0x%016" PRIx64 "\n"
"\t cursor_down = 0x%016" PRIx64 "\n"
"\t callback = 0x%016" PRIx64 "\n"
"\t user_data = 0x%016" PRIx64 "\n"
"\t reserved_dw = %" PRIu32 "\n",
reinterpret_cast<uint64_t>(bottom), reinterpret_cast<uint64_t>(top),
reinterpret_cast<uint64_t>(cursor_up), reinterpret_cast<uint64_t>(cursor_down),
reinterpret_cast<uint64_t>(callback), reinterpret_cast<uint64_t>(user_data),
reserved_dw);
}
[[nodiscard]] KYTY_SYSV_ABI uint64_t GetAvailableSizeDW() const {
// Interpret the signed cursor distance as an unsigned 64-bit DWORD count.
const auto distance = static_cast<int64_t>(reinterpret_cast<uintptr_t>(cursor_down) -
@@ -301,7 +279,6 @@ struct CommandBuffer {
"\t command buffer exhausted and has no grow callback: requested = %" PRIu32
", remaining = %" PRIu64 ", reserved_dw = %" PRIu32 "\n",
num_dw, remaining, reserved_dw);
DbgDump();
return false;
}
@@ -311,7 +288,6 @@ struct CommandBuffer {
"\t command buffer grow callback failed: requested = %" PRIu32
", remaining = %" PRIu64 ", reserved_dw = %" PRIu32 "\n",
num_dw, remaining, reserved_dw);
DbgDump();
return false;
}
if (GetAvailableSizeDW() < num_dw) {
@@ -320,7 +296,6 @@ struct CommandBuffer {
"requested = %" PRIu32 ", remaining = %" PRIu64
", reserved_dw = %" PRIu32 "\n",
num_dw, GetAvailableSizeDW(), reserved_dw);
DbgDump();
return false;
}
}
@@ -356,38 +331,6 @@ int KYTY_SYSV_ABI AgcInit(uint32_t* state, uint32_t ver) {
}
printf("version = %u\n", ver);
std::scoped_lock lock {Gen5Driver::g_driver_dmem.mutex};
if (Gen5Driver::g_driver_dmem.physical_offset >= 0) {
return OK;
}
if (const auto result = Gen5Driver::InitializeDmem(); result != OK) {
return result;
}
struct FunctionShaderBindings {
uint64_t code;
uint32_t user_data[2];
};
static_assert(sizeof(FunctionShaderBindings) == 16);
// Ordered-count queue validator, including its return to the caller.
static constexpr std::array<uint32_t, 16> validator {
0xbeeb03ff, 0x00000021, 0xb96a1818, 0xbf06106a,
0xbf800000, 0x8590807e, 0xb96a0a18, 0xbf066a80,
0xbf800000, 0x85ea807e, 0x88ea106a, 0xbf870003,
0xbefc03ff, 0x8a6ca000, 0xbf920009, 0xbefd210e,
};
const auto base = Gen5Driver::DriverDmemBase + Gen5Driver::AgcDmemOffset;
const auto function_table = base + 0x60;
const auto code_address = base + 0x100; // Shader code requires 256-byte alignment.
std::array<FunctionShaderBindings, 8> functions {};
functions[2].code = code_address;
const std::array<uint32_t, 4> descriptor {
static_cast<uint32_t>(function_table),
static_cast<uint32_t>(function_table >> 32u) | 0x00100000u,
static_cast<uint32_t>(functions.size()), 0x5204u,
};
std::memcpy(reinterpret_cast<void*>(function_table), functions.data(), sizeof(functions));
std::memcpy(reinterpret_cast<void*>(code_address), validator.data(), sizeof(validator));
std::memcpy(reinterpret_cast<void*>(base), descriptor.data(), sizeof(descriptor));
return OK;
}
@@ -1629,8 +1572,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbContextStateOp(CommandBuffer* buf, uint32_t operat
return nullptr;
}
buf->DbgDump();
uint32_t* first = nullptr;
const auto append = [&](uint32_t segment_size_dw) {
auto* segment = buf->AllocateDW(segment_size_dw);
@@ -1933,8 +1874,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbSetShRegisterRangeDirect(CommandBuffer* buf, uint32
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(num_values + 2);
if (cmd == nullptr) {
@@ -1976,8 +1915,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbSetShRegistersDirect(CommandBuffer*
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED(regs == nullptr);
buf->DbgDump();
std::vector<ShaderRegister> local_regs(num_regs);
for (uint32_t i = 0; i < num_regs; i++) {
local_regs[i].offset = regs[i].offset;
@@ -2043,8 +1980,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbSetUcRegistersDirect(CommandBuffer*
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED(regs == nullptr);
buf->DbgDump();
// Original implementation stages register values in a temporary DWORD array. It reads each new run only
// after the preceding run has been allocated, so preserve that ordering around grow callbacks.
std::vector<uint32_t> values(num_regs);
@@ -2135,8 +2070,6 @@ uint32_t* KYTY_SYSV_ABI AgcCbReleaseMem(CommandBuffer* buf, uint8_t action, uint
data_sel != 5);
EXIT_NOT_IMPLEMENTED(interrupt > 4);
buf->DbgDump();
auto* cmd = buf->AllocateDW(8);
if (cmd == nullptr) {
@@ -2189,8 +2122,6 @@ uint32_t* KYTY_SYSV_ABI AgcAcbResetQueue(CommandBuffer* buf, uint32_t op) {
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED((op & ~0x1c2u) != 0);
buf->DbgDump();
auto* cmd = buf->AllocateDW(2);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2211,8 +2142,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbResetQueue(CommandBuffer* buf, uint32_t op, uint32
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED((op & ~0xfffu) != 0);
buf->DbgDump();
auto* cmd = buf->AllocateDW(2);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2234,8 +2163,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbWaitUntilSafeForRendering(CommandBuffer* buf,
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(7);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2288,8 +2215,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetWorkloadsActive(CommandBuffer* buf, uint32_t st
}
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(WORKLOAD_ACTIVE_PACKET_SIZE_DW);
if (cmd == nullptr) {
return nullptr;
@@ -2325,8 +2250,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetWorkloadComplete(CommandBuffer* buf, uint32_t s
}
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(WORKLOAD_COMPLETE_PACKET_SIZE_DW);
if (cmd == nullptr) {
return nullptr;
@@ -2350,8 +2273,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegisterDirect(CommandBuffer* buf, ShaderRegi
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
if (cmd == nullptr) {
@@ -2371,8 +2292,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegisterDirect(CommandBuffer* buf, ShaderRegi
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
if (cmd == nullptr) {
@@ -2398,8 +2317,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegisterDirect(CommandBuffer* buf, ShaderRegi
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
if (cmd == nullptr) {
@@ -2425,8 +2342,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetCxRegistersIndirect(CommandBuffer*
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2454,8 +2369,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetShRegistersIndirect(CommandBuffer*
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2484,8 +2397,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetUcRegistersIndirect(CommandBuffer*
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(RegIndirectPacketSizeDw);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2506,8 +2417,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetIndexSize(CommandBuffer* buf, uint8_t index_siz
index_size, cache_policy);
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2527,8 +2436,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetIndexBuffer(CommandBuffer* buf, uint64_t index_
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED((index_addr & 1u) != 0);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2547,8 +2454,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetIndexCount(CommandBuffer* buf, uint32_t index_c
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(2);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2568,8 +2473,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetNumInstances(CommandBuffer* buf, uint32_t num_i
return nullptr;
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(2);
if (cmd == nullptr) {
@@ -2666,8 +2569,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndex(CommandBuffer* buf, uint32_t index_count
EXIT_NOT_IMPLEMENTED(index_addr == nullptr);
EXIT_NOT_IMPLEMENTED((reinterpret_cast<uint64_t>(index_addr) & 1u) != 0);
buf->DbgDump();
auto* cmd = buf->AllocateDW(6);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2709,8 +2610,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexMultiInstanced(CommandBuffer* buf, uint32
EXIT_NOT_IMPLEMENTED(object_ids == nullptr);
EXIT_NOT_IMPLEMENTED((reinterpret_cast<uint64_t>(index_addr) & 1u) != 0);
buf->DbgDump();
auto* cmd = buf->AllocateDW(9);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2778,8 +2677,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexAuto(CommandBuffer* buf, uint32_t index_c
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2808,8 +2705,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexOffset(CommandBuffer* buf, uint32_t index
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(5);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2839,8 +2734,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetBaseIndirectArgs(CommandBuffer* buf, uint32_t s
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(4);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2865,8 +2758,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexIndirect(CommandBuffer* buf, uint32_t dat
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(5);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2894,8 +2785,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndirect(CommandBuffer* buf, uint32_t data_off
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(5);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -2932,8 +2821,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndirectMulti(CommandBuffer* buf,
return nullptr;
}
buf->DbgDump();
constexpr uint32_t draw_packet_size_dw = 10u;
constexpr uint32_t total_size_dw =
AGC_INTERNAL_DATA_PACKET_SIZE_DW + draw_packet_size_dw +
@@ -2994,8 +2881,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDrawIndexIndirectMulti(CommandBuffer* buf,
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED((count_indirect & ~1u) != 0);
buf->DbgDump();
auto* cmd = buf->AllocateDW(10);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -3044,8 +2929,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbDispatchIndirect(CommandBuffer* buf, uint32_t data
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(3);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -3077,8 +2960,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbEventWrite(CommandBuffer* buf, uint8_t event_type,
EXIT_NOT_IMPLEMENTED(buf == nullptr);
EXIT_NOT_IMPLEMENTED(event_type > 0x3fu);
buf->DbgDump();
const bool addressed_event = ((event_type & 0xfeu) == 0x38u);
auto* cmd = buf->AllocateDW(addressed_event ? 4u : 2u);
@@ -3174,8 +3055,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbAcquireMem(CommandBuffer* buf, uint8_t engine, uin
}
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(8);
if (cmd == nullptr) {
@@ -3234,8 +3113,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbCondExec(CommandBuffer* buf, const volatile uint32
return nullptr;
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(5);
if (cmd == nullptr) {
@@ -3407,8 +3284,6 @@ uint32_t* KYTY_SYSV_ABI AgcAcbDispatchIndirect(CommandBuffer* buf,
return nullptr;
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(4);
if (cmd == nullptr) {
@@ -3457,8 +3332,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbStallCommandBufferParser(CommandBuffer* buf) {
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(2);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -3745,6 +3618,20 @@ int KYTY_SYSV_ABI AgcSetPacketPredication(uint32_t* packet, uint32_t predication
return OK;
}
int KYTY_SYSV_ABI AgcSetNop(uint32_t* packet) {
PRINT_NAME();
const auto size = AgcGetPacketSize(packet);
packet[0] = (packet[0] & 0xffff0003u) | (Pm4::IT_NOP << 8u);
// Prevent the disabled payload from being treated as an executable marker.
if (size > 1 && (packet[1] & 0xffff0000u) == 0x68750000u) {
packet[1] &= 0xffffu;
}
return OK;
}
int KYTY_SYSV_ABI AgcSetRangePredication(uint32_t* start, const volatile uint32_t* end,
uint32_t predication) {
PRINT_NAME();
@@ -3864,8 +3751,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbWriteData(CommandBuffer* buf, uint8_t dst, uint8_t
return nullptr;
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(4 + num_dwords);
if (cmd == nullptr) {
@@ -3918,8 +3803,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbGetLodStats(CommandBuffer* buf, uint8_t cache_poli
return nullptr;
}
buf->DbgDump();
auto* cmd = buf->AllocateDW(5);
if (cmd == nullptr) {
@@ -4122,8 +4005,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbWaitRegMem(CommandBuffer* buf, uint8_t size, uint8
cache_policy);
}
buf->DbgDump();
auto address_value = reinterpret_cast<uint64_t>(address);
bool wait32 = (size == 0);
auto poll = wait_reg_mem_poll_cycles_to_packet(poll_cycles);
@@ -4253,8 +4134,6 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetFlip(CommandBuffer* buf, uint32_t video_out_han
EXIT_NOT_IMPLEMENTED(buf == nullptr);
buf->DbgDump();
auto* cmd = buf->AllocateDW(6);
EXIT_NOT_IMPLEMENTED(cmd == nullptr);
@@ -4275,7 +4154,11 @@ namespace Gen5Driver {
LIB_NAME("Graphics5Driver", "Graphics5Driver");
static int InitializeDmem() {
int Initialize() {
std::scoped_lock lock {g_driver_dmem.mutex};
if (g_driver_dmem.physical_offset >= 0) {
return OK;
}
int64_t physical_offset = -1;
auto result = LibKernel::Memory::KernelAllocateDirectMemory(
0, LibKernel::Memory::KernelGetDirectMemorySize(), DriverDmemSize, DriverDmemSize,
@@ -4292,6 +4175,31 @@ static int InitializeDmem() {
}
EXIT_IF(reinterpret_cast<uint64_t>(address) != DriverDmemBase);
std::memset(address, 0, DriverDmemSize);
struct FunctionShaderBindings {
uint64_t code;
uint32_t user_data[2];
};
static_assert(sizeof(FunctionShaderBindings) == 16);
// Ordered-count queue validator, including its return to the caller.
static constexpr std::array<uint32_t, 16> validator {
0xbeeb03ff, 0x00000021, 0xb96a1818, 0xbf06106a,
0xbf800000, 0x8590807e, 0xb96a0a18, 0xbf066a80,
0xbf800000, 0x85ea807e, 0x88ea106a, 0xbf870003,
0xbefc03ff, 0x8a6ca000, 0xbf920009, 0xbefd210e,
};
const auto base = DriverDmemBase + AgcDmemOffset;
const auto function_table = base + 0x60;
const auto code_address = base + 0x100; // Shader code requires 256-byte alignment.
std::array<FunctionShaderBindings, 8> functions {};
functions[2].code = code_address;
const std::array<uint32_t, 4> descriptor {
static_cast<uint32_t>(function_table),
static_cast<uint32_t>(function_table >> 32u) | 0x00100000u,
static_cast<uint32_t>(functions.size()), 0x5204u,
};
std::memcpy(reinterpret_cast<void*>(function_table), functions.data(), sizeof(functions));
std::memcpy(reinterpret_cast<void*>(code_address), validator.data(), sizeof(validator));
std::memcpy(reinterpret_cast<void*>(base), descriptor.data(), sizeof(descriptor));
g_driver_dmem.physical_offset = physical_offset;
return OK;
}
+3
View File
@@ -270,6 +270,7 @@ uint32_t KYTY_SYSV_ABI AgcGetPacketSize(uint32_t* packet);
int KYTY_SYSV_ABI AgcSetPacketPredication(uint32_t* packet, uint32_t predication);
int KYTY_SYSV_ABI AgcSetRangePredication(uint32_t* start, const volatile uint32_t* end,
uint32_t predication);
int KYTY_SYSV_ABI AgcSetNop(uint32_t* packet);
int KYTY_SYSV_ABI AgcRewindPatchSetRewindState(uint32_t* cmd, uint8_t state);
int KYTY_SYSV_ABI AgcCondExecPatchSetEnd(uint32_t* cmd, const volatile uint32_t* buffer);
int KYTY_SYSV_ABI AgcCondExecPatchSetCommandAddress(uint32_t* cmd,
@@ -307,6 +308,8 @@ uint32_t* KYTY_SYSV_ABI AgcDcbSetFlip(CommandBuffer* buf, uint32_t video_out_han
namespace Gen5Driver {
int Initialize();
struct Packet {
uint32_t* addr;
uint32_t dw_num;
+2 -8
View File
@@ -978,11 +978,7 @@ public:
return false;
}
if (current_video) {
retired_video.push_back(std::move(current_video->buffer));
if (retired_video.size() >= static_cast<size_t>(max_video_buffers - 1)) {
video_buffers.Push(std::move(retired_video.front()));
retired_video.pop_front();
}
video_buffers.Push(std::move(current_video->buffer));
}
current_video = std::move(*frame);
*out = current_video->info;
@@ -1060,7 +1056,6 @@ private:
video_buffers.Clear();
audio_buffers.Clear();
if (!retain_output_buffers) {
retired_video.clear();
current_video.reset();
current_audio.reset();
}
@@ -1154,7 +1149,7 @@ private:
bool AllocateBuffers() {
if (video_id) {
auto size = VideoBufferSize(fmt->streams[video_id.value()]);
auto retained = retired_video.size() + (current_video ? 1u : 0u);
auto retained = current_video ? 1u : 0u;
for (size_t i = retained; i < static_cast<size_t>(max_video_buffers); i++) {
auto buffer = std::make_unique<GuestBuffer>(mem, 0x100, size, true);
if (!buffer->Valid()) {
@@ -1676,7 +1671,6 @@ private:
WorkQueue<ReadyFrame> audio_frames;
std::optional<ReadyFrame> current_video;
std::optional<ReadyFrame> current_audio;
std::deque<std::unique_ptr<GuestBuffer>> retired_video;
LibKernel::Pthread demux_thread = nullptr;
LibKernel::Pthread video_thread = nullptr;
LibKernel::Pthread audio_thread = nullptr;
+1
View File
@@ -143,6 +143,7 @@ LIB_DEFINE(InitAgcDriver_1) {
LIB_FUNC("Lkf86B98qPc", Gen5::AgcGetPacketSize);
LIB_FUNC("w6Dj1VJt5qY", Gen5::AgcSetPacketPredication);
LIB_FUNC("n8vgpaQg6dA", Gen5::AgcSetRangePredication);
LIB_FUNC("K2mciNVxUCE", Gen5::AgcSetNop);
LIB_FUNC("i1jyy49AjXU", Gen5::AgcDcbWriteData);
LIB_FUNC("p9tI+yTvx68", Gen5::AgcDcbWriteDataGetSize);
LIB_FUNC("vuSXe69VILM", Gen5::AgcDcbGetLodStats);
+18 -15
View File
@@ -868,6 +868,10 @@ static uint64_t AprReadFileRecordSize(uint64_t file_offset) {
return (file_offset >> 32u) != 0 ? READ_FILE_RECORD_SIZE_EXT : READ_FILE_RECORD_SIZE;
}
static uint64_t WriteAddressRecordSize(uint64_t value) {
return value <= 3 ? 8 : value < (uint64_t {1} << 34) ? 12 : 16;
}
static uint64_t AprReadGatherRecordSize(uint64_t file_offset) {
return file_offset > 0x3ffffu ? READ_GATHER_RECORD_SIZE_EXT : READ_GATHER_RECORD_SIZE;
}
@@ -1617,11 +1621,10 @@ static uint64_t KYTY_SYSV_ABI MeasureCommandSizeFixed32(uint64_t, uint64_t, uint
return 0x20;
}
static uint64_t KYTY_SYSV_ABI MeasureCommandSizeWriteAddressOnCompletion(volatile uint64_t*,
uint64_t) {
static uint64_t KYTY_SYSV_ABI MeasureCommandSizeWriteAddress(volatile uint64_t*, uint64_t value) {
PRINT_NAME();
return 0x20;
return WriteAddressRecordSize(value);
}
static uint64_t KYTY_SYSV_ABI
@@ -1950,7 +1953,7 @@ static int KYTY_SYSV_ABI CommandBufferWaitOnCounter(void* command_buffer, uint8_
}
static int AppendWriteAddressCommand(void* command_buffer, volatile uint64_t* address,
uint64_t value) {
uint64_t value, uint64_t record_size) {
if (command_buffer == nullptr || address == nullptr ||
(reinterpret_cast<uint64_t>(address) & 7) != 0) {
return LibKernel::KERNEL_ERROR_EINVAL;
@@ -1958,7 +1961,7 @@ static int AppendWriteAddressCommand(void* command_buffer, volatile uint64_t* ad
return AppendCommandRecord(
reinterpret_cast<uint64_t>(command_buffer),
CommandBufferState::WriteAddressCommand {reinterpret_cast<uint64_t>(address), value},
0x20)
record_size)
? OK
: LibKernel::KERNEL_ERROR_EBUSY;
}
@@ -1967,7 +1970,7 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddress(void* command_buffer, volatil
uint64_t value, uint32_t) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, value);
return AppendWriteAddressCommand(command_buffer, address, value, WriteAddressRecordSize(value));
}
static int KYTY_SYSV_ABI CommandBufferWriteAddressOnCompletion(void* command_buffer,
@@ -1975,7 +1978,7 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressOnCompletion(void*
uint64_t value) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, value);
return AppendWriteAddressCommand(command_buffer, address, value, WriteAddressRecordSize(value));
}
static int KYTY_SYSV_ABI CommandBufferWriteCounter(void* command_buffer, uint8_t index,
@@ -2007,14 +2010,14 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressFromTimeCounter(void*
uint32_t) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
}
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromTimeCounterOnCompletion(
void* command_buffer, volatile uint64_t* address) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
}
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounter(void* command_buffer,
@@ -2022,14 +2025,14 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounter(void*
uint32_t) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
}
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterOnCompletion(
void* command_buffer, volatile uint64_t* address, uint8_t) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
}
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterPair(void* command_buffer,
@@ -2037,14 +2040,14 @@ static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterPair(void*
uint8_t, uint32_t) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
}
static int KYTY_SYSV_ABI CommandBufferWriteAddressFromCounterPairOnCompletion(
void* command_buffer, volatile uint64_t* address, uint8_t) {
PRINT_NAME();
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0});
return AppendWriteAddressCommand(command_buffer, address, uint64_t {0}, 0x20);
}
static int KYTY_SYSV_ABI AprCommandBufferReadFileGather(void* command_buffer, uint64_t, uint64_t,
@@ -2452,14 +2455,14 @@ LIB_DEFINE(InitAmpr_1) {
LIB_FUNC("vWU-odnS+fU", Ampr::MeasureCommandSizeReadFile);
LIB_FUNC("sSAUCCU1dv4", Ampr::MeasureCommandSizeWriteKernelEventQueue);
LIB_FUNC("Zi3dBUjgyXI", Ampr::MeasureCommandSizeWriteKernelEventQueue);
LIB_FUNC("C+IEj+BsAFM", Ampr::MeasureCommandSizeWriteAddressOnCompletion);
LIB_FUNC("C+IEj+BsAFM", Ampr::MeasureCommandSizeWriteAddress);
LIB_FUNC("x7SQEXfeovg", Ampr::MeasureCommandSizeWriteAddressFromTimeCounterOnCompletion);
LIB_FUNC("32AcaTaBPSY", Ampr::MeasureCommandSizeWriteAddressFromCounterOnCompletion);
LIB_FUNC("vxC58+DRk-U", Ampr::MeasureCommandSizeWriteAddressFromCounterPairOnCompletion);
LIB_FUNC("4muPEJ-x5N8", Ampr::MeasureCommandSizeWriteCounterOnCompletion);
LIB_FUNC("0BMj1hgG+kE", Ampr::MeasureCommandSizeFixed32);
LIB_FUNC("ClnsFLLLcss", Ampr::MeasureCommandSizeFixed32);
LIB_FUNC("4fgtGfXDrFc", Ampr::MeasureCommandSizeFixed32);
LIB_FUNC("4fgtGfXDrFc", Ampr::MeasureCommandSizeWriteAddress);
LIB_FUNC("gAtc79UTt5E", Ampr::MeasureCommandSizeFixed32);
LIB_FUNC("JYd9g9L+TmE", Ampr::MeasureCommandSizeFixed32);
LIB_FUNC("2Hw8gjMdwSY", Ampr::MeasureCommandSizeFixed32);
+2 -8
View File
@@ -54,11 +54,7 @@ struct CContext {
std::list<atexit_func_t> atexit;
};
struct InitEnvParams {
int argc;
uint32_t pad;
const char* argv[3];
};
using InitEnvParams = Loader::EntryParams;
static int g_argc = 0;
static const char* const* g_argv = nullptr;
@@ -271,9 +267,7 @@ static KYTY_SYSV_ABI void init_env(const InitEnvParams* params) {
return;
}
constexpr int argv_capacity = static_cast<int>(sizeof(params->argv) / sizeof(params->argv[0]));
EXIT_NOT_IMPLEMENTED(params->argc < 0 || params->argc >= argv_capacity);
EXIT_NOT_IMPLEMENTED(params->argc < 0);
g_argc = params->argc;
g_argv = params->argv;
+1
View File
@@ -3117,6 +3117,7 @@ LIB_DEFINE(InitLibKernel_1_FS) {
LIB_FUNC("mBd4AfLP+u8", FileSystem::KernelPwritev);
LIB_FUNC("eV9wAD2riIA", FileSystem::KernelStat);
LIB_FUNC("kBwCPsYX-m4", FileSystem::KernelFstat);
LIB_FUNC("VW3TVZiM4-E", FileSystem::KernelFtruncate);
LIB_FUNC("AUXVxWeJU-A", FileSystem::KernelUnlink);
LIB_FUNC("52NcYU9+lEo", FileSystem::KernelRename);
LIB_FUNC("taRWhTJFTgE", FileSystem::KernelGetdirentries);
+7
View File
@@ -4,6 +4,7 @@
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "emulator.h"
#include "graphics/presentation/systemOverlay.h"
#include "libs/dialog.h"
#include "libs/errno.h"
@@ -99,6 +100,11 @@ static int KYTY_SYSV_ABI SystemServiceHideSplashScreen() {
return OK;
}
int KYTY_SYSV_ABI SystemServiceLoadExec(const char* path, const char* const argv[]) {
PRINT_NAME();
return Emulator::LoadExec(path, argv);
}
static int KYTY_SYSV_ABI SystemServiceParamGetInt(int param_id, int* value) {
PRINT_NAME();
@@ -559,6 +565,7 @@ LIB_DEFINE(InitSystemGesture_1) {
} // namespace SystemGesture
LIB_DEFINE(InitSystemService_1) {
LIB_FUNC("JoBqSQt1yyA", SystemService::SystemServiceLoadExec);
LIB_FUNC("Vo5V8KAwCmk", SystemService::SystemServiceHideSplashScreen);
LIB_FUNC("fZo48un7LK4", SystemService::SystemServiceParamGetInt);
LIB_FUNC("SsC-m-S9JTA", SystemService::SystemServiceParamGetString);
+86 -57
View File
@@ -15,6 +15,9 @@
#else
#include <arpa/inet.h>
#include <fcntl.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <net/route.h>
#include <netdb.h>
#include <netinet/tcp.h>
#include <sys/select.h>
@@ -41,10 +44,12 @@
#include <cstring>
#include <fmt/format.h>
#include <fmt/ranges.h>
#include <fstream>
#include <limits>
#include <memory>
#include <mutex>
#include <optional>
#include <sstream>
#include <string>
#include <utility>
#include <vector>
@@ -3386,7 +3391,6 @@ constexpr int NET_CTL_STATE_DISCONNECTED = 0;
constexpr int NET_CTL_STATE_IPOBTAINED = 3;
constexpr int NET_CTL_DEVICE_WIRED = 0;
constexpr int NET_CTL_DEVICE_WIRELESS = 1;
constexpr int NET_CTL_LINK_DISCONNECTED = 0;
constexpr int NET_CTL_LINK_CONNECTED = 1;
constexpr int NET_CTL_EVENT_DISCONNECTED = 1;
constexpr int NET_CTL_EVENT_IPOBTAINED = 3;
@@ -3448,8 +3452,6 @@ struct NetCtlCallbackSlot {
static Common::Mutex g_net_ctl_callbacks_mutex;
static std::array<NetCtlCallbackSlot, 8> g_net_ctl_callbacks;
static std::atomic_bool g_net_ctl_connected = false;
static std::atomic_bool g_net_ctl_status_initialized = false;
struct HostNetworkInfo {
bool connected = false;
@@ -3463,22 +3465,16 @@ struct HostNetworkInfo {
};
static void CopyIpv4String(char* dst, size_t dst_size, const sockaddr* addr) {
#if defined(_WIN32)
if (dst == nullptr || dst_size == 0 || addr == nullptr || addr->sa_family != AF_INET) {
return;
}
const auto* in = reinterpret_cast<const sockaddr_in*>(addr);
inet_ntop(AF_INET, &in->sin_addr, dst, static_cast<socklen_t>(dst_size));
#else
(void)dst;
(void)dst_size;
(void)addr;
#endif
}
static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
#if defined(_WIN32)
static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
if (dst == nullptr || dst_size == 0 || prefix_len > 32) {
return;
}
@@ -3487,26 +3483,22 @@ static void CopyIpv4Netmask(char* dst, size_t dst_size, uint8_t prefix_len) {
in_addr in {};
in.S_un.S_addr = htonl(mask);
inet_ntop(AF_INET, &in, dst, static_cast<socklen_t>(dst_size));
#else
(void)dst;
(void)dst_size;
(void)prefix_len;
#endif
}
#endif
static HostNetworkInfo QueryHostNetworkInfo() {
HostNetworkInfo info;
#if defined(_WIN32)
ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST;
ULONG flags = GAA_FLAG_SKIP_ANYCAST | GAA_FLAG_SKIP_MULTICAST | GAA_FLAG_INCLUDE_GATEWAYS;
ULONG size = 0;
auto ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, nullptr, &size);
auto ret = GetAdaptersAddresses(AF_INET, flags, nullptr, nullptr, &size);
if (ret != ERROR_BUFFER_OVERFLOW || size == 0) {
return info;
}
std::vector<uint8_t> buffer(size);
auto* adapters = reinterpret_cast<IP_ADAPTER_ADDRESSES*>(buffer.data());
ret = GetAdaptersAddresses(AF_UNSPEC, flags, nullptr, adapters, &size);
ret = GetAdaptersAddresses(AF_INET, flags, nullptr, adapters, &size);
if (ret != NO_ERROR) {
return info;
}
@@ -3557,40 +3549,76 @@ static HostNetworkInfo QueryHostNetworkInfo() {
}
return info;
}
} else if (addr->sa_family == AF_INET6) {
const auto* in6 = reinterpret_cast<const sockaddr_in6*>(addr);
if (!IN6_IS_ADDR_LOOPBACK(&in6->sin6_addr) &&
!IN6_IS_ADDR_UNSPECIFIED(&in6->sin6_addr)) {
info.connected = true;
}
}
}
}
return info;
#else
info.connected = true;
ifaddrs* addresses = nullptr;
if (getifaddrs(&addresses) != 0) {
return info;
}
const auto usable = [](const ifaddrs* address) {
return address->ifa_addr != nullptr && address->ifa_addr->sa_family == AF_INET &&
(address->ifa_flags & (IFF_UP | IFF_RUNNING | IFF_LOOPBACK)) ==
(IFF_UP | IFF_RUNNING) &&
reinterpret_cast<const sockaddr_in*>(address->ifa_addr)->sin_addr.s_addr != 0;
};
const ifaddrs* selected = nullptr;
for (auto* address = addresses; address != nullptr; address = address->ifa_next) {
if (usable(address)) {
selected = address;
break;
}
}
std::ifstream routes("/proc/net/route");
std::string line;
uint32_t best_metric = std::numeric_limits<uint32_t>::max();
uint32_t gateway = 0;
bool has_default_route = false;
while (std::getline(routes, line)) {
std::istringstream fields(line);
std::string name;
uint32_t destination = 0, candidate_gateway = 0, flags = 0, refs = 0, uses = 0;
uint32_t metric = 0, mask = 0;
if (!(fields >> name >> std::hex >> destination >> candidate_gateway >> flags >>
std::dec >> refs >> uses >> metric >> std::hex >> mask) ||
destination != 0 || mask != 0 || (flags & (RTF_UP | RTF_REJECT)) != RTF_UP ||
(has_default_route && metric >= best_metric)) {
continue;
}
for (auto* address = addresses; address != nullptr; address = address->ifa_next) {
if (usable(address) && name == address->ifa_name) {
selected = address;
gateway = candidate_gateway;
best_metric = metric;
has_default_route = true;
break;
}
}
}
if (selected != nullptr) {
CopyIpv4String(info.ip_address, sizeof(info.ip_address), selected->ifa_addr);
CopyIpv4String(info.netmask, sizeof(info.netmask), selected->ifa_netmask);
info.connected = info.ip_address[0] != '\0';
if (has_default_route) {
in_addr address {gateway};
inet_ntop(AF_INET, &address, info.default_route, sizeof(info.default_route));
}
}
freeifaddrs(addresses);
return info;
#endif
}
static bool NetCtlConnected() {
if (!g_net_ctl_status_initialized.load()) {
g_net_ctl_connected = false;
g_net_ctl_status_initialized = true;
LOGF("\t host network connected = false (forced offline)\n");
}
return g_net_ctl_connected.load();
return Config::IsNetworkConnected() && QueryHostNetworkInfo().connected;
}
int KYTY_SYSV_ABI NetCtlInit() {
PRINT_NAME();
g_net_ctl_connected = false;
g_net_ctl_status_initialized = true;
LOGF("\t host network connected = false (forced offline)\n");
return OK;
}
@@ -3651,7 +3679,7 @@ int KYTY_SYSV_ABI NetCtlGetStateV6(int* state) {
return NET_CTL_ERROR_INVALID_ADDR;
}
*state = (NetCtlConnected() ? NET_CTL_STATE_IPOBTAINED : NET_CTL_STATE_DISCONNECTED);
*state = NET_CTL_STATE_DISCONNECTED;
return OK;
}
@@ -3720,24 +3748,28 @@ int KYTY_SYSV_ABI NetCtlGetResult(int event_type, int* error_code) {
int KYTY_SYSV_ABI NetCtlGetInfo(int code, NetCtlInfo* info) {
PRINT_NAME();
EXIT_NOT_IMPLEMENTED(info == nullptr);
if (info == nullptr) {
return NET_CTL_ERROR_INVALID_ADDR;
}
if (code < 1 || code > 21) {
return NET_CTL_ERROR_INVALID_CODE;
}
if (!Config::IsNetworkConnected()) {
return NET_CTL_ERROR_NOT_CONNECTED;
}
const auto host = QueryHostNetworkInfo();
if (!host.connected || (code == 16 && host.default_route[0] == '\0')) {
return NET_CTL_ERROR_NOT_CONNECTED;
}
memset(info, 0, sizeof(NetCtlInfo));
// Online/host-backed info responses are left below for testing, but disabled
// to preserve the disconnected-console behavior.
return NET_CTL_ERROR_NOT_CONNECTED;
switch (code) {
case 1:
info->device =
(QueryHostNetworkInfo().wireless ? NET_CTL_DEVICE_WIRELESS : NET_CTL_DEVICE_WIRED);
info->device = (host.wireless ? NET_CTL_DEVICE_WIRELESS : NET_CTL_DEVICE_WIRED);
break;
case 2: info->ether_addr = QueryHostNetworkInfo().ether_addr; break;
case 2: info->ether_addr = host.ether_addr; break;
case 3: info->mtu = 1500; break;
case 4:
info->link = (NetCtlConnected() ? NET_CTL_LINK_CONNECTED : NET_CTL_LINK_DISCONNECTED);
break;
case 4: info->link = NET_CTL_LINK_CONNECTED; break;
case 5: break;
case 6: break;
case 7: info->wifi_security = 0; break;
@@ -3748,28 +3780,25 @@ int KYTY_SYSV_ABI NetCtlGetInfo(int code, NetCtlInfo* info) {
case 12: break;
case 13: break;
case 14:
std::strncpy(info->ip_address, QueryHostNetworkInfo().ip_address,
std::strncpy(info->ip_address, host.ip_address,
sizeof(info->ip_address) - 1);
break;
case 15:
std::strncpy(info->netmask, QueryHostNetworkInfo().netmask, sizeof(info->netmask) - 1);
std::strncpy(info->netmask, host.netmask, sizeof(info->netmask) - 1);
break;
case 16:
std::strncpy(info->default_route, QueryHostNetworkInfo().default_route,
std::strncpy(info->default_route, host.default_route,
sizeof(info->default_route) - 1);
break;
case 17:
std::strncpy(info->primary_dns, QueryHostNetworkInfo().primary_dns,
sizeof(info->primary_dns) - 1);
std::strncpy(info->primary_dns, host.primary_dns, sizeof(info->primary_dns) - 1);
break;
case 18:
std::strncpy(info->secondary_dns, QueryHostNetworkInfo().secondary_dns,
sizeof(info->secondary_dns) - 1);
std::strncpy(info->secondary_dns, host.secondary_dns, sizeof(info->secondary_dns) - 1);
break;
case 19: info->http_proxy_config = NET_CTL_HTTP_PROXY_OFF; break;
case 20: break;
case 21: info->http_proxy_port = 0; break;
default: LOGF("\t unknown NetCtl info code: %d\n", code); return NET_CTL_ERROR_INVALID_CODE;
}
return OK;
+130
View File
@@ -0,0 +1,130 @@
#include "common/file.h"
#include "common/logging/log.h"
#include "emulator.h"
#include "kernel/fileSystem.h"
#include "libs/errno.h"
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <string_view>
#include <vector>
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#elif defined(__linux__)
#include <linux/close_range.h>
#include <unistd.h>
#endif
namespace Emulator {
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
static std::wstring QuoteArgument(std::wstring_view argument) {
std::wstring quoted = L"\"";
size_t slashes = 0;
for (wchar_t c: argument) {
if (c == L'\\') {
++slashes;
continue;
}
quoted.append(c == L'"' ? slashes * 2 + 1 : slashes, L'\\');
quoted += c;
slashes = 0;
}
quoted.append(slashes * 2, L'\\');
return quoted + L'"';
}
#endif
int LoadExec(const char* path, const char* const argv[]) {
using namespace Libs::SystemService;
if (path == nullptr || !std::string_view(path).starts_with("/app0/") ||
!Common::File::IsFileExisting(Libs::LibKernel::FileSystem::GetRealFilename(path))) {
return SYSTEM_SERVICE_ERROR_PARAMETER;
}
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS || defined(__linux__)
std::vector<std::string> arguments {"--guest-exec", path};
size_t bytes = 0;
for (size_t i = 0; argv != nullptr && argv[i] != nullptr; ++i) {
const auto size = strnlen(argv[i], 4096 - bytes) + 1;
if (size > 4096 - bytes) {
return SYSTEM_SERVICE_ERROR_PARAMETER;
}
bytes += size;
arguments.emplace_back(argv[i], size - 1);
}
LOGF("LoadExec: %s\n", path);
Log::Flush();
#if KYTY_PLATFORM == KYTY_PLATFORM_WINDOWS
wchar_t executable[32768] {};
const auto length = GetModuleFileNameW(nullptr, executable, 32768);
if (length == 0 || length >= 32768) {
return SYSTEM_SERVICE_ERROR_INTERNAL;
}
if (__wargv == nullptr) {
return SYSTEM_SERVICE_ERROR_INTERNAL;
}
std::wstring command = QuoteArgument(executable);
for (int i = 1; i < __argc && __wargv[i] != nullptr; ++i) {
if (std::wstring_view(__wargv[i]) == L"--guest-exec") {
break;
}
command += L" " + QuoteArgument(__wargv[i]);
}
for (const auto& argument: arguments) {
const int size =
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, argument.c_str(), -1, nullptr, 0);
if (size == 0) {
return SYSTEM_SERVICE_ERROR_PARAMETER;
}
std::wstring wide(size, L'\0');
if (MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, argument.c_str(), -1, wide.data(),
size) == 0) {
return SYSTEM_SERVICE_ERROR_PARAMETER;
}
wide.resize(size - 1);
command += L" " + QuoteArgument(wide);
}
STARTUPINFOW startup {};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process {};
if (!CreateProcessW(executable, command.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW,
nullptr, nullptr, &startup, &process)) {
LOGF("LoadExec: process creation failed: %lu\n", GetLastError());
return SYSTEM_SERVICE_ERROR_INTERNAL;
}
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
std::fflush(nullptr);
std::quick_exit(0);
#else
std::ifstream command("/proc/self/cmdline", std::ios::binary);
std::vector<std::string> host_arguments;
for (std::string argument;
std::getline(command, argument, '\0') && argument != "--guest-exec";) {
host_arguments.push_back(std::move(argument));
}
if (host_arguments.empty()) {
return SYSTEM_SERVICE_ERROR_INTERNAL;
}
host_arguments.insert(host_arguments.end(), arguments.begin(), arguments.end());
std::vector<char*> host_argv;
for (auto& argument: host_arguments) {
host_argv.push_back(argument.data());
}
host_argv.push_back(nullptr);
if (close_range(3, ~0u, CLOSE_RANGE_UNSHARE | CLOSE_RANGE_CLOEXEC) == 0) {
execvp(host_argv[0], host_argv.data());
}
LOGF("LoadExec failed: %s\n", std::strerror(errno));
#endif
return SYSTEM_SERVICE_ERROR_INTERNAL;
#else
return SYSTEM_SERVICE_ERROR_UNAVAILABLE;
#endif
}
} // namespace Emulator
+10 -14
View File
@@ -25,6 +25,7 @@
#include <algorithm>
#include <atomic>
#include <cstddef>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -88,16 +89,6 @@ ThreadLocalStorage::~ThreadLocalStorage() {
}
}
#pragma pack(1)
struct EntryParams {
int argc;
uint32_t pad;
const char* argv[3];
};
#pragma pack()
using atexit_func_t = KYTY_SYSV_ABI void (*)();
using entry_func_t = KYTY_SYSV_ABI void (*)(EntryParams* params, atexit_func_t atexit_func);
using module_ini_fini_func_t = KYTY_SYSV_ABI int (*)(size_t args, const void* argp,
@@ -1283,11 +1274,16 @@ void RuntimeLinker::Execute(const std::filesystem::path& game_patch) {
LOGF_COLOR(Log::Color::BrightYellow, "---\n--- Execute: %s\n---\n", "Main");
if (auto entry = GetEntry(); entry != 0) {
const auto& arguments = Config::GetGuestArguments();
const auto params_size = offsetof(EntryParams, argv) +
(arguments.size() + 2) * sizeof(const char*);
auto* params = reinterpret_cast<EntryParams*>(
(reinterpret_cast<uintptr_t>(main_stack_top) - 0x100u) & ~static_cast<uintptr_t>(0x0f));
std::memset(params, 0, sizeof(EntryParams));
params->argc = 1;
params->argv[0] = "KytyEmu";
(reinterpret_cast<uintptr_t>(main_stack_top) - params_size - 0x100u) &
~static_cast<uintptr_t>(0x0f));
std::memset(params, 0, params_size);
for (const auto& argument: arguments) {
params->argv[params->argc++] = argument.c_str();
}
LOGF("stack_addr = %" PRIx64 "\n", reinterpret_cast<uint64_t>(params));
+6
View File
@@ -14,6 +14,12 @@
namespace Loader {
struct EntryParams {
int argc;
uint32_t pad;
const char* argv[1];
};
class Elf64;
struct Elf64_Sym;
struct Elf64_Rela;
+5 -1
View File
@@ -164,6 +164,8 @@ void Psf::OpenJson(const std::filesystem::path& file_name) {
add_string_param("contentId", "CONTENT_ID");
add_string_param("contentVersion", "APP_VER");
add_string_param("appVersion", "APP_VER");
AddParamInt("ATTRIBUTE", 0);
add_int_param("attribute", "ATTRIBUTE");
add_int_param("attribute3", "ATTRIBUTE3");
add_int_param("userDefinedParam1", "USER_DEFINED_PARAM_1");
add_int_param("userDefinedParam2", "USER_DEFINED_PARAM_2");
@@ -360,10 +362,12 @@ void PlayGo::Open(const std::filesystem::path& file_name) {
}
uint32_t magic1 = 0;
constexpr uint32_t PLGO_MAGIC = 0x6f676c70;
constexpr uint32_t PLGX_MAGIC = 0x78676c70;
m_f.Read(&magic1, 4);
if (magic1 != 0x6f676c70) {
if (magic1 != PLGO_MAGIC && magic1 != PLGX_MAGIC) {
LOGF("invalid file: magic1 = %08" PRIx32 "\n", magic1);
return;
}
+16
View File
@@ -61,6 +61,7 @@ static void PrintUsage() {
::printf(
" --gpu <index> Vulkan physical device index. Default: auto.\n");
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
::printf(" --hdr Enable HDR10 output when supported.\n");
::printf(
" --hide-cursor Hide the cursor after 2 s idle. Default: off.\n");
::printf(" --no-splash Disable the startup splash screen.\n");
@@ -69,6 +70,7 @@ static void PrintUsage() {
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n");
::printf(" --vulkan-validation <true|false> Enable Vulkan validation.\n");
::printf(" --network-connected <true|false> Use the host local network (default: false).\n");
::printf(" --gpu-assisted-validation <t|f> Bounds-check shader accesses on the GPU.\n"
" Implies --vulkan-validation; very slow.\n");
::printf(" --shader-validation <true|false> Enable shader validation.\n");
@@ -215,6 +217,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
continue;
}
if (arg == "--hdr") {
options.config.hdr_enabled = true;
continue;
}
if (arg == "--hide-cursor") {
options.config.hide_cursor_enabled = true;
continue;
@@ -300,6 +307,10 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
value.c_str());
return false;
}
} else if (arg == "--guest-exec") {
options.elf = Common::PathFromUtf8(Common::FixFilenameSlash(value));
options.config.guest_arguments.assign(argv + i + 1, argv + argc);
break;
} else if (arg == "--game-patch") {
if (!options.game_patch.empty()) {
::printf("--game-patch can only be specified once\n");
@@ -378,6 +389,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid console language: %s\n", value.c_str());
return false;
}
} else if (arg == "--network-connected") {
if (!ParseBool(value, options.config.network_connected)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--vulkan-validation") {
if (!ParseBool(value, options.config.vulkan_validation_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
+86 -3
View File
@@ -4,16 +4,27 @@
#include <array>
#include <chrono>
#include <new>
#include <thread>
namespace {
std::filesystem::path content_root;
}
namespace Libs::LibKernel {
int KYTY_SYSV_ABI PthreadCreate(Pthread*, const PthreadAttr*, pthread_entry_func_t, void*, const char*) {
std::abort();
struct PthreadPrivate {
std::thread thread;
};
int KYTY_SYSV_ABI PthreadCreate(Pthread* thread, const PthreadAttr*, pthread_entry_func_t entry,
void* arg, const char*) {
*thread = new PthreadPrivate {std::thread([=] { entry(arg); })};
return 0;
}
int KYTY_SYSV_ABI PthreadJoin(Pthread thread, void**) {
thread->thread.join();
delete thread;
return 0;
}
int KYTY_SYSV_ABI PthreadJoin(Pthread, void**) { std::abort(); }
namespace FileSystem {
std::filesystem::path GetRealFilename(const std::string& path) {
return path.starts_with("/app0/") ? content_root / path.substr(6) : std::filesystem::path {};
@@ -82,6 +93,76 @@ int KYTY_SYSV_ABI Read(void* object, uint8_t* buffer, uint64_t offset, uint32_t
return static_cast<int>(bytes);
}
uint64_t KYTY_SYSV_ABI Size(void* object) { return static_cast<Callbacks*>(object)->data.size(); }
struct VideoMemory {
unsigned allocations = 0;
unsigned deallocations = 0;
void* protected_buffer = nullptr;
bool released_protected = false;
};
void* KYTY_SYSV_ABI Allocate(void* object, uint32_t alignment, uint32_t size) {
Check(alignment == 0x100, "video allocation alignment");
++static_cast<VideoMemory*>(object)->allocations;
return ::operator new(size, std::align_val_t(alignment), std::nothrow);
}
void KYTY_SYSV_ABI Deallocate(void* object, void* data) {
auto& memory = *static_cast<VideoMemory*>(object);
++memory.deallocations;
memory.released_protected |= data == memory.protected_buffer;
::operator delete(data, std::align_val_t(0x100));
}
void CheckVideoReadAhead(const std::filesystem::path& root) {
// A 16x16 red MPEG-4 intra frame at 30 Hz; repeat it with increasing VOP times.
std::array<uint8_t, 50> frame {
0x00, 0x00, 0x01, 0xb0, 0x01, 0x00, 0x00, 0x01, 0xb5, 0x89,
0x13, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x20, 0x00,
0xc4, 0x8d, 0x88, 0x00, 0xf5, 0x00, 0x84, 0x02, 0x14, 0x63,
0x00, 0x00, 0x01, 0xb3, 0x00, 0x10, 0x07, 0x00, 0x00, 0x01,
0xb6, 0x10, 0x60, 0x51, 0x85, 0x06, 0xd8, 0x2c, 0x81, 0xe0};
{
Common::File output;
Check(output.Create(root / "movie.m4v"), "create video fixture");
for (unsigned i = 0; i < 24; ++i) {
frame[41] = static_cast<uint8_t>(0x10 + i / 2);
frame[42] = i % 2 == 0 ? 0x60 : 0xe0;
output.Write(frame.data(), frame.size());
}
}
VideoMemory memory;
const AvPlayerMemAllocator allocator {&memory, Allocate, Deallocate, Allocate, Deallocate};
Source source(allocator, {}, {}, 6, false, 0);
Check(source.Init("/app0/movie.m4v", AvPlayerSourceFileMp4) == 0, "open video fixture");
source.SetSync(1);
Check(source.Start() == 0, "start video workers");
AvPlayerFrameInfoEx info {};
for (unsigned i = 0; i < 8; ++i) {
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(1);
while (!source.Video(&info)) {
Check(std::chrono::steady_clock::now() < deadline, "decode steady-state video");
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
// Let the producer refill, then model the game's immediate late-frame retries.
std::this_thread::sleep_for(std::chrono::milliseconds(50));
for (unsigned i = 0; i < 5; ++i) {
const auto previous = info.time_stamp;
Check(source.Video(&info), "six output buffers keep five video frames ready");
Check(info.time_stamp > previous, "video retries return successive frames");
}
Check(source.Pause() == 0, "pause video");
memory.protected_buffer = info.data;
std::array<uint8_t, 256> current {};
std::memcpy(current.data(), info.data, current.size());
Check(!source.Video(&info) && info.data == memory.protected_buffer,
"failed video get preserves output");
Check(source.Start() == 0, "restart video with retained current output");
Check(!memory.released_protected &&
std::memcmp(current.data(), memory.protected_buffer, current.size()) == 0,
"restart preserves output until the next successful video get");
Check(source.Stop() == 0 && memory.allocations == memory.deallocations,
"stop releases all video buffers");
}
}
int main() {
@@ -127,6 +208,8 @@ int main() {
FileStreamer missing({});
Check(!missing.Init("/unmounted/movie.bin"), "reject unmapped media");
}
content_root = root;
CheckVideoReadAhead(root);
std::error_code error;
std::filesystem::remove_all(root, error);
Check(!error, "release all archive handles");
+144 -2
View File
@@ -35,6 +35,9 @@
#include <vector>
#if defined(__linux__)
#include <arpa/inet.h>
#include <ifaddrs.h>
#include <net/if.h>
#include <sys/mman.h>
#include <unistd.h>
#endif
@@ -898,6 +901,11 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
using WriteCounter = int (KYTY_SYSV_ABI *)(void *, uint8_t, uint8_t, uint64_t,
uint8_t, uint32_t);
using WriteAddress = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t);
using WriteImmediately = int (KYTY_SYSV_ABI *)(void *, volatile uint64_t *, uint64_t,
uint32_t);
using MeasureWriteAddress = uint64_t (KYTY_SYSV_ABI *)(volatile uint64_t *, uint64_t);
using MeasureWriteImmediately = uint64_t (KYTY_SYSV_ABI *)(volatile uint64_t *, uint64_t,
uint32_t);
using ReadFile = int (KYTY_SYSV_ABI *)(void *, uint64_t, uint64_t, uint32_t,
void *, uint64_t, uint64_t);
using WriteKernelEvent = int (KYTY_SYSV_ABI *)(void *, uint64_t, uint64_t, uint64_t,
@@ -916,6 +924,9 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
const auto wait_counter = reinterpret_cast<WaitCounter>(find("cQb8Zr8Q0Y0"));
const auto write_counter = reinterpret_cast<WriteCounter>(find("jK+yuYCI7MA"));
const auto write_address = reinterpret_cast<WriteAddress>(find("sJXyWHjP-F8"));
const auto write_immediately = reinterpret_cast<WriteImmediately>(find("j0+3uJMxYJY"));
const auto measure_write_address = reinterpret_cast<MeasureWriteAddress>(find("C+IEj+BsAFM"));
const auto measure_write_immediately = reinterpret_cast<MeasureWriteImmediately>(find("4fgtGfXDrFc"));
const auto read_file = reinterpret_cast<ReadFile>(find("mQ16-QdKv7k"));
const auto write_event = reinterpret_cast<WriteKernelEvent>(find("H896Pt-yB4I"));
const auto submit_apr = reinterpret_cast<SubmitApr>(find("ASoW5WE-UPo"));
@@ -927,7 +938,7 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
std::array<uint32_t, 256> data {};
};
std::array<Buffer, 4> buffers;
const auto reset = [&](size_t apr_count) {
const auto reset = [&](size_t apr_count, uint32_t data_size = sizeof(Buffer::data)) {
for (size_t i = 0; i < buffers.size(); ++i) {
auto &buffer = buffers[i];
construct(buffer.header.data());
@@ -936,10 +947,60 @@ void CheckAmprOrdering(Loader::SymbolDatabase &symbols, uint32_t file_id) {
} else {
construct_amm(buffer.header.data());
}
Check(set_buffer(buffer.header.data(), buffer.data.data(), sizeof(buffer.data)) == OK,
Check(set_buffer(buffer.header.data(), buffer.data.data(), data_size) == OK,
"initialize AMPR command buffer");
}
};
struct LiteralWrite { uint64_t value; uint32_t size; };
constexpr std::array literal_writes {
LiteralWrite{0, 8}, LiteralWrite{3, 8}, LiteralWrite{4, 12},
LiteralWrite{(uint64_t{1} << 34) - 1, 12},
LiteralWrite{uint64_t{1} << 34, 16}, LiteralWrite{UINT64_MAX, 16}};
for (const auto &literal : literal_writes) {
for (const bool immediately : std::array{false, true}) {
reset(1, literal.size);
auto *command_buffer = buffers[0].header.data();
uint64_t fence = ~literal.value;
const auto append = [&](uint64_t value) {
return immediately ? write_immediately(command_buffer, &fence, value, 1)
: write_address(command_buffer, &fence, value);
};
Check(measure_write_address(&fence, literal.value) == literal.size &&
measure_write_immediately(&fence, literal.value, 1) == literal.size &&
append(literal.value) == OK && offset(command_buffer) == literal.size,
"literal write fits exactly its measured command size");
Check(append(~literal.value) == Libs::LibKernel::KERNEL_ERROR_EBUSY &&
offset(command_buffer) == literal.size,
"rejected write preserves the full command buffer offset");
Result result {1234, 5678};
uint32_t submission = 0;
Check(submit_apr(command_buffer, 3, &result, &submission) == OK &&
wait_apr(submission) == OK && result.result == OK &&
fence == literal.value,
"bounded write preserves all sixty-four literal bits");
}
}
reset(1, 768);
std::array<std::array<char, 3>, 16> batch_output {};
std::array<uint64_t, 16> batch_fences {};
auto *batch = buffers[0].header.data();
for (size_t i = 0; i < batch_fences.size(); ++i) {
Check(read_file(batch, reinterpret_cast<uint64_t>(&buffers[0].header[3]),
reinterpret_cast<uint64_t>(&buffers[0].header[4]), file_id,
batch_output[i].data(), batch_output[i].size(), 0) == OK &&
write_address(batch, &batch_fences[i], i + 1) == OK,
"sixteen APR read/fence pairs fit the guest's 768-byte command buffer");
}
Result batch_result {1234, 5678};
uint32_t batch_submission = 0;
Check(submit_apr(batch, 3, &batch_result, &batch_submission) == OK &&
wait_apr(batch_submission) == OK && batch_result.result == OK,
"complete the bounded APR read batch");
for (size_t i = 0; i < batch_fences.size(); ++i) {
Check(batch_fences[i] == i + 1 &&
std::memcmp(batch_output[i].data(), "APR", 3) == 0,
"every read publishes its data and completion, including requests fourteen and fifteen");
}
// WaitCompare order: ==, unsigned >/<, !=, wrapped >=, signed >/<.
struct Comparison { uint8_t compare; uint64_t blocked, reference, released; };
constexpr std::array comparisons {
@@ -1401,6 +1462,84 @@ void CheckSocketReceiveBuffer(int reader, int writer) {
}
#endif
#if defined(__linux__)
void KYTY_SYSV_ABI RecordNetCtlEvent(int event, void* arg) {
auto& events = *static_cast<std::array<int, 2>*>(arg);
++events[0];
events[1] = event;
}
void TestNetCtlHostIpv4() {
using namespace Libs::Network::NetCtl;
ifaddrs* interfaces = nullptr;
Check(getifaddrs(&interfaces) == 0, "enumerate host IPv4 addresses");
std::vector<uint32_t> host_addresses;
for (auto* interface = interfaces; interface != nullptr; interface = interface->ifa_next) {
if (interface->ifa_addr == nullptr || interface->ifa_addr->sa_family != AF_INET ||
(interface->ifa_flags & IFF_UP) == 0 || (interface->ifa_flags & IFF_RUNNING) == 0 ||
(interface->ifa_flags & IFF_LOOPBACK) != 0) {
continue;
}
const auto address = reinterpret_cast<const sockaddr_in*>(interface->ifa_addr)->sin_addr;
if (address.s_addr != 0 && (ntohl(address.s_addr) >> 24) != 127) {
host_addresses.push_back(address.s_addr);
}
}
freeifaddrs(interfaces);
Config::ConfigOptions options;
options.printf_direction = Config::LogDirection::Silent;
Check(!options.network_connected, "host networking requires explicit opt-in");
alignas(uint32_t) std::array<char, 256> storage {};
auto* info = reinterpret_cast<NetCtlInfo*>(storage.data());
constexpr int NotConnected = static_cast<int>(0x80412108u);
Check(NetCtlGetInfo(14, nullptr) == static_cast<int>(0x80412107u) &&
NetCtlGetInfo(0, info) == static_cast<int>(0x80412106u) &&
NetCtlGetInfo(22, info) == static_cast<int>(0x80412106u),
"NetCtl reports invalid output addresses and information codes");
for (bool enabled : {false, true}) {
options.network_connected = enabled;
Config::Load(options);
Check(NetCtlInit() == OK, "initialize NetCtl for selected network mode");
const bool connected = enabled && !host_addresses.empty();
int state = -1;
Check(NetCtlGetState(&state) == OK && state == (connected ? 3 : 0),
"NetCtl IPv4 state matches opt-in and host connectivity");
Check(NetCtlGetStateV6(&state) == OK && state == 0,
"IPv4 connectivity does not advertise IPv6 support");
Check(NetCtlGetInfo(14, info) == (connected ? OK : NotConnected),
"NetCtl IPv4 query respects network mode and host state");
in_addr address {};
if (connected) {
Check(inet_pton(AF_INET, storage.data(), &address) == 1 &&
std::find(host_addresses.begin(), host_addresses.end(), address.s_addr) !=
host_addresses.end(),
"NetCtl reports an IPv4 address assigned to an active host interface");
}
Check(NetCtlGetInfo(15, info) == (connected ? OK : NotConnected) &&
(!connected || inet_pton(AF_INET, storage.data(), &address) == 1),
"NetCtl returns the active IPv4 netmask only in connected mode");
const int gateway_result = NetCtlGetInfo(16, info);
Check(gateway_result == NotConnected ||
(connected && gateway_result == OK &&
inet_pton(AF_INET, storage.data(), &address) == 1),
"NetCtl reports a valid IPv4 gateway or its absence");
std::array<int, 2> events {};
int callback_id = -1;
Check(NetCtlRegisterCallback(RecordNetCtlEvent, &events, &callback_id) == OK &&
NetCtlCheckCallback() == OK && events[0] == 1 &&
events[1] == (connected ? 3 : 1) && NetCtlCheckCallback() == OK && events[0] == 1,
"NetCtl delivers the matching connection event once while state is unchanged");
Check(NetCtlUnregisterCallback(callback_id) == OK, "release NetCtl callback");
NetCtlTerm();
}
options.network_connected = false;
Config::Load(options);
}
#endif
void CheckEtherAddressFormatting() {
Loader::SymbolDatabase symbols;
Libs::LibNet::InitNet_1_Net(&symbols);
@@ -1732,6 +1871,9 @@ int main(int, char**) {
FileSystem::Shutdown();
CheckSocketWakeup();
CheckEtherAddressFormatting();
#if defined(__linux__)
TestNetCtlHostIpv4();
#endif
TestNpWebApi2Memory();
TestNpCommerceDialog();
graphics.reset();
+173
View File
@@ -168,6 +168,117 @@ void TestMappedSrtUsesDirectReaderByDefault() {
"cache rematerialization did not use the direct reader by default");
}
void TestNativeNestedSrtRefresh() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
std::array<uint32_t, 3> first{0u, 0x12345678u, 0xabcdef01u};
std::array<uint32_t, 3> second{0u, 0x87654321u, 0x10fedcbau};
const auto pointer_words = [](const void *pointer) {
const auto address = reinterpret_cast<uint64_t>(pointer);
return std::array<uint32_t, 2>{static_cast<uint32_t>(address) | 3u,
static_cast<uint32_t>(address >> 32u) | 0xabcd0000u};
};
auto table = pointer_words(first.data());
const auto user_data = pointer_words(table.data());
auto program = SrtProgram(0u);
program.user_data_base = 4;
program.user_data_count = 2;
auto &block = *program.blocks[0];
const auto emit = [&](ValueOpcode opcode, std::initializer_list<Value> args) {
return Value(&block.AppendNewInst(opcode, args));
};
const auto low = emit(ValueOpcode::GetUserData, {Value(ScalarReg(4))});
const auto high = emit(ValueOpcode::GetUserData, {Value(ScalarReg(5))});
const auto root = emit(ValueOpcode::GetAddressResource, {low, high});
const auto read = [&](Value address, uint32_t offset) {
const auto value = emit(ValueOpcode::LoadAddressU32,
{address, Value(offset), Value(0u), Value(true)});
value.Instruction()->SetFlags(MemoryFlags{.index = 0});
program.srt_reads.push_back(
{value, static_cast<uint32_t>(program.srt_reads.size())});
return value;
};
const auto pointer_low = read(root, 0u);
const auto pointer_high = read(root, 4u);
auto *leaf = program.srt_reads[0].value.Instruction();
leaf->Arg(0).Instruction()->SetArg(0, pointer_low);
leaf->Arg(0).Instruction()->SetArg(1, pointer_high);
leaf->SetArg(1, Value(7u));
read(leaf->Arg(0), 11u);
auto plan = ExtractResourcePlan(program);
Check(plan.srt_code != nullptr, "nested user-data tree did not compile an x86 walker");
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
const SrtRuntime runtime{.user_data = user_data};
{
SrtWalker walker(plan, runtime);
Check(walker.RefreshFlatBuffer(snapshot.flattened_srt),
"native SRT refresh failed");
const auto loaded = first[1];
first[1] ^= 0xffffffffu;
uint32_t value = 0;
Check(walker.Evaluate(plan.srt_reads[0].value, value) && value == loaded,
"descriptor evaluation reloaded a value already copied by the native walker");
first[1] = loaded;
}
for (const auto *data : {&first, &second}) {
table = pointer_words(data->data());
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
snapshot.flattened_srt ==
std::vector<uint32_t>{(*data)[1], table[0], table[1], (*data)[2]},
"native SRT refresh lost pointer masking, alignment, or current values");
}
Check(!MaterializeResources(plan, {.user_data = std::span(user_data).first(1)},
snapshot, specialization),
"native SRT walk accepted truncated user data");
const std::array<uint32_t, 2> fake_user_data{0x1003u, 0xabcd0000u};
uint32_t reads = 0;
const SrtRuntime callback_runtime{
.user_data = fake_user_data,
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
++*static_cast<uint32_t *>(data);
switch (address) {
case 0x1000u: words[0] = 0x2003u; break;
case 0x1004u: words[0] = 0xabcd0000u; break;
case 0x2004u: words[0] = 0x1234u; break;
case 0x2008u: words[0] = 0x5678u; break;
default: return false;
}
return true;
},
.userdata = &reads};
Check(MaterializeResources(plan, callback_runtime, snapshot, specialization) && reads == 4 &&
snapshot.flattened_srt ==
std::vector<uint32_t>{0x1234u, 0x2003u, 0xabcd0000u, 0x5678u},
"native-eligible SRT bypassed its callback or repeated nested pointer reads");
leaf->SetArg(0, root);
leaf->SetArg(1, Value(0u));
program.srt_reads.resize(1);
for (const int32_t offset : {-1, 7}) {
program.memory_info[0].offset = static_cast<uint32_t>(offset);
auto bounded = ExtractResourcePlan(program);
auto base = pointer_words(second.data() + 1);
Check(bounded.srt_code != nullptr &&
MaterializeResources(bounded, {.user_data = base}, snapshot, specialization) &&
snapshot.flattened_srt[0] == second[offset < 0 ? 0 : 2],
"native SRT applied a signed immediate offset incorrectly");
base = offset < 0 ? std::array<uint32_t, 2>{0u, 0u}
: std::array<uint32_t, 2>{0xfffffffcu, 0xffffu};
Check(!MaterializeResources(bounded, {.user_data = base}, snapshot, specialization),
"native SRT accepted address underflow or 48-bit overflow");
}
high.Instruction()->SetArg(0, Value(ScalarReg(6)));
Check(ExtractResourcePlan(program).srt_code == nullptr,
"native SRT accepted nonadjacent user-data pointer words");
high.Instruction()->SetArg(0, Value(ScalarReg(5)));
program.memory_info[0].offset = 0;
program.srt_reads.push_back({pointer_low, 1});
program.srt_reads.push_back({pointer_high, 2});
leaf->SetArg(0, emit(ValueOpcode::GetAddressResource, {pointer_low, pointer_high}));
pointer_high.Instruction()->SetArg(1, Value(8u));
Check(ExtractResourcePlan(program).srt_code == nullptr,
"native SRT accepted nonadjacent nested pointer words");
}
void TestIntegerRuntimeValueFollowsSrtReads() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
auto plan = SrtProgram(0x10000);
@@ -256,6 +367,8 @@ void TestSrtAliasesRetainReadPolicy() {
plan.srt_reads[1].value.Instruction()->Flags<SrtReadFlags>().clean == 0u &&
plan.srt_reads[2].value.Instruction()->Flags<SrtReadFlags>().clean == 1u,
"alias normalization lost read identity or retained discarded EXEC provenance");
Check(plan.srt_code == nullptr,
"captured strict reads compiled an unused x86 walker");
struct Reads { uint32_t strict = 0; uint32_t ordinary = 0; bool dirty = false; } reads;
const SrtRuntime runtime{
.read_memory = +[](void *data, uint64_t address, std::span<uint32_t> words) {
@@ -527,6 +640,64 @@ void TestFailedMaterializationRejectsStage() {
"missing runtime user data did not reject the cached stage");
}
void TestSharedControlPredicates() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
constexpr uint32_t depth = 8;
for (const auto operation : {ValueOpcode::LogicalAnd, ValueOpcode::LogicalOr}) {
Program program;
program.user_data_count = 1;
program.srt_plan_complete = program.resource_tracking_complete = true;
auto &block = AddValueBlock(program);
const auto emit = [&](ValueOpcode opcode, std::initializer_list<Value> args) {
return Value(&block.AppendNewInst(opcode, args));
};
const auto user = emit(ValueOpcode::GetUserData, {Value(ScalarReg(0))});
const auto uniform = emit(ValueOpcode::INotEqual32, {user, Value(0u)});
const auto lane = emit(ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
auto varying = emit(ValueOpcode::INotEqual32, {lane, Value(0u)});
// Both failed and partially uniform subgraphs share their operands exponentially.
for (uint32_t i = 0; i < depth; ++i)
varying = emit(ValueOpcode::LogicalOr, {varying, varying});
auto condition = emit(operation, {varying, uniform});
for (uint32_t i = 0; i < depth; ++i)
condition = emit(ValueOpcode::LogicalOr, {condition, condition});
condition = emit(ValueOpcode::LogicalNot, {condition});
condition = emit(ValueOpcode::ConditionRef, {condition});
for (uint32_t i = 0; i < 3; ++i) AddValueBlock(program);
for (uint32_t i = 0; i < 2; ++i) {
program.blocks[i]->condition = condition;
program.blocks[i]->terminator.kind = TerminatorKind::ConditionalBranch;
program.blocks[i]->terminator.true_block = program.blocks[i == 0 ? 1 : 3];
program.blocks[i]->terminator.false_block = program.blocks[2];
}
auto plan = ExtractResourcePlan(program);
Check(plan.value_storage.size() == depth + 5 &&
plan.control_flow.size() == 4 &&
plan.control_flow[0].condition == plan.control_flow[1].condition &&
!plan.capture_specialization_reads,
"shared predicates expanded into trees or retained varying dependencies");
for (const uint32_t flag : {0u, 1u}) {
const std::array<uint32_t, 1> user_data{flag};
SrtWalker walker(plan, {.user_data = user_data});
uint32_t result = 99;
const bool conjunction = operation == ValueOpcode::LogicalAnd;
const bool known = conjunction ? flag == 0 : flag != 0;
Check(walker.Evaluate(plan.control_flow[0].condition, result) == known &&
(!known || result == uint32_t{conjunction}),
"shared predicate changed its uniform factor or cached runtime values");
std::vector<uint32_t> flat;
Check(walker.RefreshFlatBuffer(flat) &&
plan.visited_blocks == (known
? (conjunction ? std::vector<uint8_t>{1, 1, 0, 1}
: std::vector<uint8_t>{1, 0, 1, 0})
: std::vector<uint8_t>{1, 1, 1, 1}),
"shared predicate skipped an unknown branch or followed a rejected branch");
}
}
}
void TestFiniteImageRefreshReusesScalarReads() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
@@ -671,6 +842,7 @@ void DbgExit(int) { std::abort(); }
int main() {
TestMappedSrtUsesDirectReaderByDefault();
TestNativeNestedSrtRefresh();
TestIntegerRuntimeValueFollowsSrtReads();
TestSrtAliasesRetainReadPolicy();
TestUniformVectorDescriptorRead();
@@ -679,6 +851,7 @@ int main() {
for (const bool memory_condition : {false, true})
TestWrittenDescriptorPredicateReads(memory_condition);
TestFailedMaterializationRejectsStage();
TestSharedControlPredicates();
TestFiniteImageRefreshReusesScalarReads();
TestMixedSamplerVariantsShareRuntimeDescriptor();
std::puts("ResourceMaterializationTests: all cases passed");
+22 -19
View File
@@ -270,7 +270,7 @@ MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t ta
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
const auto sampled = fixture->Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture->ImageAddress()},
{image, sampler, fixture->ImageAddress(), Value(true)},
fixture->AddMemory(sample, 0x10f0));
const auto sampled_x =
fixture->Emit(ValueOpcode::CompositeExtractU32x4, {sampled, Value(0u)});
@@ -486,7 +486,8 @@ void TestWaterfallImageTable() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
const auto value = fixture->Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture->ImageAddress()}, fixture->AddMemory(memory, 0x92cu), block);
{image, sampler, fixture->ImageAddress(), block == entry ? active : guard},
fixture->AddMemory(memory, 0x92cu), block);
fixture->Emit(ValueOpcode::ReferenceU32,
{fixture->Emit(ValueOpcode::CompositeExtractU32x4, {value, Value(0u)}, 0, block)}, 0, block);
};
@@ -641,7 +642,7 @@ void TestGuardedDirectImageTable() {
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x128));
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
@@ -989,7 +990,7 @@ void TestBoundedComputeImageLoop() {
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture.ImageAddress()},
{image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(sample, 0x29c));
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
@@ -1325,7 +1326,9 @@ void TestUniformizedMaterialImageKeys() {
MemoryInfo image_memory;
image_memory.kind = ResourceKind::Image;
image_memory.image_dimension = Decoder::ImageDimension::Dim2D;
const auto sampled = fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address},
const auto sampled = fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, address,
variant == Variant::EmptyFirstEntry ? material_guard : sample_guard},
fixture.AddMemory(image_memory, 0x1aecu), sample);
if (first_lane) {
const auto component = fixture.Emit(
@@ -1477,7 +1480,7 @@ void TestImageDescriptorFields() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_r128 = r128;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x80));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
@@ -1571,7 +1574,7 @@ void TestUniformScalarBufferImage() {
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture.ImageAddress()}, fixture.AddMemory(sample, 0x228));
{image, sampler, fixture.ImageAddress(), Value(true)}, fixture.AddMemory(sample, 0x228));
fixture.PlanAndTrack();
const auto plan = ExtractResourcePlan(fixture.program);
@@ -1866,7 +1869,7 @@ void TestImagesSamplersAndAliases() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_sample_flags = sample_flags;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, image_address},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, image_address, Value(true)},
fixture.AddMemory(memory, pc));
return std::pair{image, sampler};
};
@@ -1938,14 +1941,14 @@ void TestSampleAdjustSamplerScratch() {
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_sample_flags = Decoder::ImageSampleFlagAdjust;
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture.ImageAddress()},
{image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x1ec));
// PPSA24156's zero border payload leaves the reserved scratch shift as
// the entire DWORD after the frontend folds its OR with zero.
const auto shift_sampler = fixture.Sampler(
{Value(0x36u), Value(0xfff000u), Value(0x02500000u), scratch}, 0x200);
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, shift_sampler, fixture.ImageAddress()},
{image, shift_sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x200));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
@@ -1994,7 +1997,7 @@ void TestSampleAdjustSamplerScratch() {
rejected_memory.image_dimension = Decoder::ImageDimension::Dim2D;
rejected_memory.image_sample_flags = flags;
rejected.Emit(ValueOpcode::ImageSampleRaw,
{rejected_image, rejected_sampler, rejected.ImageAddress()},
{rejected_image, rejected_sampler, rejected.ImageAddress(), Value(true)},
rejected.AddMemory(rejected_memory, 0x200));
CheckFatal([&] { rejected.PlanAndTrack(); },
@@ -2112,7 +2115,7 @@ void TestGatherLodSamplerValidation() {
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]},
sampler_words[1], sampler_words[2], Value(true)},
fixture.AddMemory(memory, i * 4));
}
fixture.PlanAndTrack();
@@ -2514,7 +2517,7 @@ ResourcePlan ConditionalSamplerPlan(bool diamond, bool reverse, bool reverse_phi
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
const auto result =
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address, Value(true)},
fixture.AddMemory(sample, 0x4d4));
fixture.Emit(ValueOpcode::ReferenceU32,
{fixture.Emit(ValueOpcode::CompositeExtractU32x4,
@@ -2619,7 +2622,7 @@ void TestFiniteImagePhiCycle() {
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 4));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
@@ -2685,12 +2688,12 @@ void TestFiniteInlineSampler() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
if (format == F::k32Float)
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, previous, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, previous, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x440u));
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x1dd0u));
if (format == F::k32Float)
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, direct, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, direct, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x1dd8u));
}
fixture.PlanAndTrack();
@@ -2764,7 +2767,7 @@ void TestFiniteInlineSampler() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_sample_flags = flags;
adjusted.Emit(ValueOpcode::ImageSampleRaw, {image, shared, adjusted.ImageAddress()},
adjusted.Emit(ValueOpcode::ImageSampleRaw, {image, shared, adjusted.ImageAddress(), Value(true)},
adjusted.AddMemory(memory, 0x1dd0u));
}
CheckFatal([&] { adjusted.PlanAndTrack(); }, "not a valid runtime value",
@@ -2817,7 +2820,7 @@ void TestFiniteImageBitScanSentinel() {
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 4));
if (!nonzero) {
CheckFatal([&] { fixture.PlanAndTrack(); }, "not a valid runtime value",
+515 -62
View File
@@ -159,6 +159,8 @@ struct BufferCacheTestAccess {
static_assert(std::same_as<decltype(BufferCache::m_slot_buffers),
Common::SlotVector<Buffer>>);
static uint64_t GcTick(const BufferCache &cache) { return cache.m_gc_tick; }
static void SetGarbageCollectionThresholds(BufferCache &cache,
uint64_t trigger,
uint64_t critical) {
@@ -224,6 +226,8 @@ struct TextureCacheTestAccess {
static_assert(TextureCache::ImagePageTable::kAddressSpaceBits == 44);
static_assert(TextureCache::ImagePageTable::kFirstLevelBits == 14);
static uint64_t GcTick(const TextureCache &cache) { return cache.m_gc_tick; }
static std::unique_lock<TrackingSpinLock> Lock(TextureCache &cache) {
return std::unique_lock(cache.m_lock);
}
@@ -2614,6 +2618,31 @@ public:
packet[4] = value;
};
const auto cache_ticks = [&] {
std::pair<uint64_t, uint64_t> ticks;
gpu.SendCommandSync([&] {
ticks = {TextureCacheTestAccess::GcTick(context.GetTextureCache()),
BufferCacheTestAccess::GcTick(context.GetBufferCache())};
});
return ticks;
};
const auto initial_cache_ticks = cache_ticks();
uint32_t graphics_value = 0;
uint32_t async_value = 0;
std::array<uint32_t, 5> graphics_list{}, compute_list{};
write(graphics_list.data(), &graphics_value, 17);
write(compute_list.data(), &async_value, 29);
// A frame can contain many lists; completing them must not age either cache.
for (uint32_t i = 0; i < 32; ++i) {
gpu.Submit(graphics_list, {});
gpu.SubmitCompute(0x20, compute_list);
}
finish();
Require(name, "command-list cache lifetime",
graphics_value == 17 && async_value == 29 &&
cache_ticks() == initial_cache_ticks,
"individual graphics or compute lists aged the caches within a frame");
gpu.SendCommandSync([&] {
setup.BufferInit();
SubmitInfo blocked;
@@ -2648,6 +2677,10 @@ public:
parser.join();
Require(name, "marker insertion", parser_nonblocking,
"inserting a suspend point blocked the GPU command thread");
Require(name, "frame cache maintenance",
cache_ticks() == std::pair{initial_cache_ticks.first + 1,
initial_cache_ticks.second + 1},
"a suspend point did not age both caches exactly once");
std::binary_semaphore second_entered{0};
std::binary_semaphore second_returned{0};
@@ -2685,6 +2718,10 @@ public:
"or the host command lane");
Require(name, "frame count", gpu.GetFrameNum() == 2,
"suspend-point insertion did not advance the frame counter once");
Require(name, "async cache lifetime",
cache_ticks() == std::pair{initial_cache_ticks.first + 2,
initial_cache_ticks.second + 2},
"asynchronous work aged the caches between frame boundaries");
// A marker must stay behind a stalled graphics stream, then clear its
// predicate before the next stream. Async compute releases that stream.
@@ -6741,6 +6778,99 @@ public:
"to the image");
texture_cache.MarkGpuWritten(mirror_refresh);
// Unity copies BC mip regions and packed mip tails through integer images.
// The CPU allocation remains white; later array sampling must see GPU writes.
for (uint32_t format_index = 0; format_index < 3; ++format_index) {
constexpr std::array formats{vk::Format::eBc4UnormBlock,
vk::Format::eBc5UnormBlock,
vk::Format::eBc7UnormBlock};
constexpr std::array guest_formats{Prospero::BufferFormat::kBc4UNorm,
Prospero::BufferFormat::kBc5UNorm,
Prospero::BufferFormat::kBc7UNorm};
constexpr uint32_t width = 128, levels = 8, layers = 3;
const uint32_t block_bytes = format_index == 0 ? 8 : 16;
const uint64_t address = base + 0x1900000 + format_index * 0x40000;
auto array = MakeLinearDesc(address, 0, formats[format_index],
guest_formats[format_index], Prospero::ImageType::kColor2D,
{width, width, 1}, layers, block_bytes, 1);
array.info.resources.levels = levels;
array.info.tile_mode = Prospero::TileMode::kStandard4KB;
array.info.UpdateSize();
array.view_info.level_count = levels;
std::memset(memory + address - base, 0xff, array.info.data.size);
auto value_for = [&](uint32_t layer, uint64_t offset) {
return 0x7c000000u | (format_index << 16) | (layer << 8) |
static_cast<uint32_t>(offset / 4096);
};
auto write_raw = [&](uint32_t layer, uint32_t level, uint32_t value) {
const auto &mip = array.info.mip_layout[level];
auto raw = MakeLinearDesc(
address + layer * array.info.tiled_slice_stride + mip.offset, 0,
block_bytes == 8 ? vk::Format::eR32G32Uint
: vk::Format::eR32G32B32A32Uint,
block_bytes == 8 ? Prospero::BufferFormat::k32_32UInt
: Prospero::BufferFormat::k32_32_32_32UInt,
Prospero::ImageType::kColor2D, {mip.pitch, mip.height, 1},
1, block_bytes, 1);
raw.type = BindingType::Storage;
raw.info.tile_mode = array.info.tile_mode;
raw.info.UpdateSize();
raw.view_info.usage = vk::ImageUsageFlagBits::eStorage;
const auto producer = texture_cache.FindImage(raw);
(void)texture_cache.FindTexture(producer, raw);
vk::ClearValue clear{};
std::fill_n(clear.color.uint32.data(), 4, value);
TextureCacheTestAccess::ClearImage(texture_cache, scheduler.Current(),
producer, {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, clear);
};
for (const uint32_t layer : {0u, 2u}) {
uint64_t previous_offset = UINT64_MAX;
for (uint32_t level = 0; level < levels; ++level) {
const auto offset = array.info.mip_layout[level].offset;
if (offset == previous_offset) continue;
previous_offset = offset;
write_raw(layer, level, value_for(layer, offset));
}
}
std::vector<vk::BufferImageCopy> probes;
std::vector<uint32_t> expected;
for (uint32_t layer = 0; layer < layers; ++layer) {
for (uint32_t level = 0; level < levels; ++level) {
vk::BufferImageCopy copy{};
copy.bufferOffset = expected.size() * sizeof(uint32_t);
copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, level, layer, 1};
copy.imageExtent = {std::min(width >> level, 4u),
std::min(width >> level, 4u), 1};
probes.push_back(copy);
expected.insert(expected.end(), block_bytes / sizeof(uint32_t),
layer == 1 ? UINT32_MAX
: value_for(layer, array.info.mip_layout[level].offset));
}
}
auto verify_array = [&] {
const auto consumer = texture_cache.FindImage(array);
(void)texture_cache.FindTexture(consumer, array);
auto readback = CreateHostBuffer(name, expected.size() * sizeof(uint32_t),
vk::BufferUsageFlagBits::eTransferDst, std::vector<uint32_t>(expected.size()));
texture_cache.GetImage(consumer).Download(probes, readback.buffer, 0, readback.size);
HostReadBarrier(readback.buffer, readback.size, vk::PipelineStageFlagBits::eTransfer,
vk::AccessFlagBits::eTransferWrite);
scheduler.Finish();
Require(name, "raw image producers populate BC array",
ReadBuffer(name, readback, expected.size()) == expected,
"BC array lost GPU mip/tail writes or overwrote an untouched layer");
DestroyBuffer(&readback);
};
verify_array();
constexpr uint32_t replacement = 0x13579bdfu;
write_raw(2, array.info.first_tail_level, replacement);
for (uint32_t level = array.info.first_tail_level; level < levels; ++level) {
const auto word = (2 * levels + level) * block_bytes / sizeof(uint32_t);
std::fill_n(expected.begin() + word, block_bytes / sizeof(uint32_t), replacement);
}
verify_array();
}
// A write through a separate buffer must not replace untouched image
// texels with stale backing bytes. Exact-base writes still refresh it.
constexpr uint64_t buffer_alias_offset = 0x1a00000;
@@ -11083,6 +11213,78 @@ public:
context.ShutdownGpu();
LibKernel::Memory::InstallGpuResources(nullptr);
}
{
// PPSA01593 samples a DCC R32 color surface through a comparison sampler.
constexpr uint64_t comparison_dcc = base + 0x10000;
std::memset(mapped, 0, 0x10000);
std::memset(reinterpret_cast<void *>(comparison_dcc), 0xff, 0x1000);
RenderContext context(m_runtime_context);
context.InitializeGpu(nullptr);
LibKernel::Memory::InstallGpuResources(&context);
auto &scheduler = context.GetCommandScheduler();
HW::Context registers{};
HW::UserConfig user_config{};
HW::Shader shaders{};
scheduler.Begin(registers, user_config, shaders);
context.MapMemory(base, allocation_size);
auto &cache = context.GetTextureCache();
auto &executor = context.GetRenderExecutor();
const ShaderTextureResource descriptor{{static_cast<uint32_t>(base >> 8u),
0xc1600000u, 0x000fc00fu, 0x91b00204u, 0, 0x00700000u,
0x002b0000u | (static_cast<uint32_t>(comparison_dcc >> 8u) << 24u),
static_cast<uint32_t>(comparison_dcc >> 16u)}};
ShaderRecompiler::IR::DescriptorValue value{};
value.dword_count = 8;
std::copy_n(descriptor.fields, 8, value.dwords.begin());
ShaderRecompiler::IR::ImageResource resource{};
resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage;
resource.numeric_class = Prospero::TextureNumericClass::Float;
resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D;
resource.written = true;
const auto color = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
(void)cache.FindTexture(color.image_id, color.desc);
vk::ClearValue paint{};
paint.color.float32 = std::array{0.375f, 0.f, 0.f, 0.f};
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), color.image_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, paint);
resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled;
resource.written = false;
resource.read = true;
resource.depth_compare = true;
auto comparison = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
(void)cache.FindTexture(comparison.image_id, comparison.desc);
const auto &depth = cache.GetImage(comparison.image_id);
Require(name, "expanded DCC comparison alias",
comparison.image_id != color.image_id &&
depth.info.pixel_format == vk::Format::eD32Sfloat &&
depth.info.guest_format == Prospero::BufferFormat::k32Float &&
depth.info.tile_mode == Prospero::TileMode::kRenderTarget &&
depth.info.extent == vk::Extent3D{64, 64, 1} &&
depth.info.metadata.kind == ImageMetadataKind::Dcc &&
depth.info.metadata.range == GuestRange{comparison_dcc, 0x1000} &&
ReadCachedTexel(name, context, comparison.image_id) ==
std::vector<u32>{std::bit_cast<u32>(0.375f)},
"comparison promotion lost native color data or guest DCC layout");
for (const u32 code : {0x40404040u, 0x80808080u}) {
WriteMetadata(context, comparison_dcc, 0x1000, code);
comparison = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
(void)cache.FindTexture(comparison.image_id, comparison.desc);
Require(name, "DCC clear on a comparison backing",
ReadCachedTexel(name, context, comparison.image_id, {63, 63, 0}) ==
std::vector<u32>{code == 0x40404040u ? 0u : 0x3f800000u},
"DCC clear used the native depth format or color aspect");
std::array<uint8_t, 0x1000> metadata{};
Require(name, "comparison clear expands metadata",
LibKernel::Memory::TryReadBacking(comparison_dcc, metadata.data(),
metadata.size()) &&
std::ranges::all_of(metadata, [](uint8_t byte) { return byte == 0xff; }),
"comparison clear retained the native DCC clear key");
}
context.UnmapMemory(base, allocation_size);
scheduler.Finish();
context.ShutdownGpu();
LibKernel::Memory::InstallGpuResources(nullptr);
}
Require(name, "unmap",
Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0,
"captured scene mapping release failed");
@@ -16439,6 +16641,60 @@ public:
wave_ids[0] != wave_ids[1] && wave_keys[0] != wave_keys[1],
"wave32 and wave64 pixel programs shared a cache key");
static const auto waterfall_vertex = [&] {
std::vector<u32> code;
AppendVMovU32(&code, 9, 0);
// d3e1ae284c4dea3b reconstructs EXEC twice from packed NGG s3.
code.insert(code.end(), {0xbefe04c1u, 0x906a8803u, 0x81ea6a80u,
0x90fe6ac1u, 0x81ea0380u, 0x90fe6ac1u,
EncodeSop1(0x04, 10, 126)});
const auto loop = code.size();
code.push_back(EncodeSop1(0x14, 4, 10));
AppendVop3(&code, 0x360, 6, Vgpr(5), 4);
code.push_back(EncodeVopc(0xc2, 6, 5));
code.push_back(EncodeSop1(0x24, 18, 106));
AppendVMovLiteral(&code, 9, 0x3f800000u);
code.push_back(EncodeSop2(0x15, 10, 10, 106));
code.push_back(EncodeSop1(0x04, 126, 18));
code.push_back(EncodeSopp(0x05, static_cast<int16_t>(loop - code.size() - 1u)));
const auto base = code.size();
code.insert(code.end(), native_vertex.begin(), native_vertex.end());
const auto position = std::find(code.begin() + base, code.end(), EncodeExp0(0x0c, 0xf));
// Unprocessed vertices keep W=0, making waterfall progress observable.
*(position + 1) = EncodeExp1(3, 4, 0, 9);
return code;
}();
native_vertex_regs.es_regs.data_addr = reinterpret_cast<uint64_t>(waterfall_vertex.data());
ShaderMapUserData(native_vertex_regs.es_regs.data_addr,
{.type = Prospero::ShaderBinaryType::kGs, .user_data = &native_user_data,
.code_size_bytes = static_cast<u32>(waterfall_vertex.size() * sizeof(u32))});
registers.SetPsInControl(0x8008);
const auto saved_stages = registers.GetShaderStages();
for (const u32 wave : {32u, 64u}) {
registers.SetShaderStages((saved_stages & ~0x00400000u) |
(wave == 32u ? 0x00400000u : 0u));
const auto programs = context.GetPipelineCache().GetGraphicsPrograms(
native_vertex_regs, native_pixel_regs, registers.GetShaderRegisters(),
registers, user_config, export_mapping, true, native_vertex_info, pixel);
vertex_shader = programs.vertex[0];
pixel_shader = programs.pixel;
vertex = native_vertex_info[0];
Require(name, "native NGG waterfall wave",
vertex.stage.program->stage == ShaderType::Vertex &&
vertex.stage.program->wave_size == wave,
"NGG waterfall bypassed the native vertex path or lost its guest wave width");
for (const u32 count : {3u, 6u, 33u}) {
draw(pipeline(true, 2, 2), count);
const auto pixels = read_color();
for (size_t component = 0; component < pixels.size(); ++component)
Require(name, "NGG partial-wave waterfall output",
pixels[component] == (component % 4 == 3 ? 0x3f800000u : 0x3e800000u),
"native vertex waterfall did not terminate with every input vertex processed");
}
}
registers.SetShaderStages(saved_stages);
native_vertex_regs.es_regs.data_addr = vertex_address;
// Reversed RGBA targets must use logical Sa for RGB, with a separate
// zero or unit source factor for separate alpha attenuation or accumulation.
static const auto blend_pixel = [] {
@@ -17744,39 +18000,47 @@ public:
}
// Exercise the real texture-layout/info-building seam for every format
// admitted by each standard tile mode. Formats sharing a byte/block width
// admitted by each texture tile mode. Formats sharing a byte/block width
// intentionally share a shader, but this loop still validates their
// texel-to-element conversion (notably every BCn format).
struct StandardMode {
struct TextureMode {
Prospero::TileMode tile;
TileBlockFamily family;
};
constexpr StandardMode standard_modes[] = {
constexpr TextureMode texture_modes[] = {
{Prospero::TileMode::kStandard256B, TileBlockFamily::Standard256B},
{Prospero::TileMode::kStandard4KB, TileBlockFamily::Standard4KB},
{Prospero::TileMode::kStandard64KB, TileBlockFamily::Standard64KB},
{Prospero::TileMode::kPrt, TileBlockFamily::Prt64KB},
{Prospero::TileMode::kRenderTarget, TileBlockFamily::RenderTarget64KB},
{Prospero::TileMode::kDepth, TileBlockFamily::Depth64KB},
};
struct RenderTargetFormatCase {
struct TextureTileFormatCase {
Prospero::BufferFormat format;
u32 bytes_per_element;
bool depth_supported;
};
constexpr RenderTargetFormatCase render_target_formats[] = {
{Prospero::BufferFormat::k8Srgb, 1},
{Prospero::BufferFormat::k8_8Srgb, 2},
{Prospero::BufferFormat::k9_9_9_5Float, 0},
constexpr TextureTileFormatCase texture_tile_formats[] = {
{Prospero::BufferFormat::k8Srgb, 1, true},
{Prospero::BufferFormat::k8_8Srgb, 2, true},
{Prospero::BufferFormat::k9_9_9_5Float, 4, true},
{Prospero::BufferFormat::kBc1Srgb, 8, false},
{Prospero::BufferFormat::kBc7Srgb, 16, false},
{Prospero::BufferFormat::k32_32_32_32Float, 16, false},
};
for (const auto &test : render_target_formats) {
for (const auto &test : texture_tile_formats) {
for (const auto tile :
{Prospero::TileMode::kDepth, Prospero::TileMode::kRenderTarget}) {
TileTextureBlockLayout texture{};
const bool supported =
TileGetTextureBlockLayout(test.format, tile, false, texture);
Require(name, "RT format policy",
supported == (test.bytes_per_element != 0) &&
const bool expected = tile == Prospero::TileMode::kRenderTarget ||
test.depth_supported;
Require(name, "texture tile format policy",
supported == expected &&
(!supported || texture.block.bytes_per_element ==
test.bytes_per_element),
"RT/depth tile format support or element size is incorrect");
"texture tile policy used attachment format restrictions");
}
}
{
@@ -17809,7 +18073,7 @@ public:
if (Prospero::IsFmaskTextureFormat(format)) {
continue;
}
for (const auto &mode : standard_modes) {
for (const auto &mode : texture_modes) {
TileTextureBlockLayout texture{};
if (!TileGetTextureBlockLayout(format, mode.tile, false, texture)) {
continue;
@@ -17846,7 +18110,27 @@ public:
}
}
Require(name, "format coverage", format_cases != 0,
"no CPU-supported standard formats were tested");
"no CPU-supported texture formats were tested");
{
constexpr auto format = Prospero::BufferFormat::kBc1Srgb;
constexpr auto tile = Prospero::TileMode::kRenderTarget;
constexpr u32 layers = 8;
const auto captured =
MakeTilingImage(format, 2048, 2048, 1, layers, tile, 0, false);
Require(name, "PPSA19577 BC1 array footprint",
captured.data.size == 0x1000000 &&
captured.tiled_slice_stride == 0x200000 &&
captured.first_tail_level == 1 &&
captured.tiling.block.block_width == 128 &&
captured.tiling.block.block_height == 64 &&
captured.mip_layout[0].pitch == 512 &&
captured.mip_layout[0].height == 512,
"R64 BC1 texture lost its compressed blocks or array stride");
const auto small_array =
MakeTilingImage(format, 67, 51, 1, layers, tile, 0, false);
check_round_trip("R64 BC1 array", small_array.data.size, small_array);
}
{
constexpr auto format = Prospero::BufferFormat::k11_11_10UInt;
@@ -17949,7 +18233,7 @@ public:
check_round_trip("array", total.size, layout);
}
for (const auto &mode : standard_modes) {
for (const auto &mode : texture_modes) {
constexpr auto format = Prospero::BufferFormat::k32Float;
constexpr u32 levels = 2;
TileBlockLayout block{};
@@ -34142,10 +34426,11 @@ TestCase ImageSamplePackedUintConvertsSampleAndGather() {
return test;
}
template <bool rg> TestCase ImageSampleUScaled8() {
template <bool rg, bool snorm = false> TestCase ImageSamplePacked8() {
using O = ShaderOpcode;
TestCase test;
test.name = rg ? "ImageSampleRG8UScaled" : "ImageSampleR8UScaled";
test.name = snorm ? "ImageSampleR8SNorm"
: rg ? "ImageSampleRG8UScaled" : "ImageSampleR8UScaled";
AppendVMovLiteral(&test.code, 20, std::bit_cast<u32>(0.375f));
AppendVMovLiteral(&test.code, 21, std::bit_cast<u32>(0.375f));
test.code.push_back(EncodeMimg0(0x20, 0xf));
@@ -34158,26 +34443,37 @@ template <bool rg> TestCase ImageSampleUScaled8() {
AppendStoreVgpr(&test.code, component, component);
}
AppendEnd(&test.code);
// R varies horizontally; G varies vertically in the opposite direction.
// Bilinear samples must retain fractions despite host UNorm filter precision.
test.image_width = test.image_height = 2;
test.sampled_image_rgba = rg ? std::vector<u32>{0xffffff00u, 0x00ff0000u}
: std::vector<u32>{0xff00ff00u};
test.sampled_image_format = rg ? vk::Format::eR8G8Unorm : vk::Format::eR8Unorm;
test.sampled_image_rgba =
snorm ? std::vector<u32>{0x7fc08180u}
: rg ? std::vector<u32>{0xffffff00u, 0x00ff0000u}
: std::vector<u32>{0xff00ff00u};
test.sampled_image_format =
snorm ? vk::Format::eR8Snorm
: rg ? vk::Format::eR8G8Unorm : vk::Format::eR8Unorm;
test.sampled_image_dwords_per_pixel = 1;
test.sampler_filter = vk::Filter::eLinear;
test.expected_float_tolerance = 0.01f;
test.user_data = MakeSampledTextureData(rg ? Prospero::BufferFormat::k8_8UScaled
: Prospero::BufferFormat::k8UScaled);
test.sampler_filter = snorm ? vk::Filter::eNearest : vk::Filter::eLinear;
test.expected_float_tolerance = snorm ? 0.00002f : 0.01f;
test.user_data = MakeSampledTextureData(
snorm ? Prospero::BufferFormat::k8SNorm
: rg ? Prospero::BufferFormat::k8_8UScaled
: Prospero::BufferFormat::k8UScaled);
test.user_data[1] |= 1u << 30u;
test.user_data[2] = 1u << 14u;
test.user_data[50] = 12u * sizeof(u32);
test.user_data[51] = 3u << 28u;
test.has_user_data = true;
test.image_descriptor_swizzle = DstSel(rg ? 5 : 4, 4, 0, 1);
for (float value : {rg ? 191.25f : 63.75f, 63.75f, 0.0f, 1.0f,
0.0f, rg ? 0.0f : 255.0f, 255.0f, rg ? 255.0f : 0.0f,
1.0f, 1.0f, 1.0f, 1.0f}) {
test.image_descriptor_swizzle = snorm ? DstSel(4, 5, 6, 7)
: DstSel(rg ? 5 : 4, 4, 0, 1);
constexpr float midpoint = -64.0f / 127.0f;
const std::array<float, 12> expected = snorm
// -128 and -127 both clamp to -1; the interior signed value normalizes by 127.
? std::array<float, 12>{-1, 0, 0, 1, midpoint, 1, -1, -1, 1, 1, 1, 1}
// R varies horizontally; G varies vertically in the opposite direction.
: std::array<float, 12>{rg ? 191.25f : 63.75f, 63.75f, 0.0f, 1.0f,
0.0f, rg ? 0.0f : 255.0f, 255.0f, rg ? 255.0f : 0.0f,
1.0f, 1.0f, 1.0f, 1.0f};
for (float value : expected) {
test.expected.push_back(std::bit_cast<u32>(value));
}
test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::IMAGE_GATHER4_LZ,
@@ -34794,12 +35090,11 @@ TestCase ImageSampleOpcodeAliasUsesNormalCoords() {
return test;
}
TestCase ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu() {
template <u32 wave_size> TestCase ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovU32(&code, 20,
1); // Non-constant +1 X offset is not a SPIR-V ConstOffset.
AppendVMovU32(&code, 20, 1); // +1 X remains a full dword with A16 coordinates.
AppendVMovLiteral(&code, 21, 0x36003900u); // x=0.625, y=0.375 packed as f16.
AppendVMovU32(&code, 22, 0);
code.push_back(EncodeMimg0(0x30, 0xf));
@@ -34810,18 +35105,63 @@ TestCase ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu() {
AppendEnd(&code);
auto image = MakeRgbaImage(4, 4);
SetRgbaPixel(&image, 4, 2, 1, 0x3f800000u, 0x40000000u, 0x40400000u,
SetRgbaPixel(&image, 4, 2, 1, 0x3f000000u, 0, 0, 0);
SetRgbaPixel(&image, 4, 3, 1, 0x3f800000u, 0x40000000u, 0x40400000u,
0x40800000u);
TestCase test;
test.name = "ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu";
test.name = wave_size == 32 ? "ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu32"
: "ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu64";
test.code = code;
test.compute_info.wave_size = wave_size;
test.has_compute_info = true;
test.expected = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u};
test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.sampled_image_rgba = image;
test.required_spirv = {"UnpackHalf2x16"};
test.forbidden_spirv = {"OpBitFieldSExtract"};
test.required_spirv = {"UnpackHalf2x16", "Lod|ConstOffset"};
test.forbidden_spirv = {"OpBitFieldSExtract", "OpImageQuerySizeLod", "OpFDiv"};
return test;
}
template <u32 wave_size> TestCase ImageSampleOffsetsReadFourNeighbors() {
using O = ShaderOpcode;
TestCase test;
test.name = wave_size == 32 ? "ImageSampleOffsetsReadFourNeighbors32"
: "ImageSampleOffsetsReadFourNeighbors64";
test.compute_info.wave_size = wave_size;
test.has_compute_info = true;
constexpr std::array offsets{0x01u, 0x3fu, 0x3f00u, 0x0100u};
constexpr std::array opcodes{0x37u, 0x37u, 0x34u, 0x32u};
for (u32 sample = 0; sample < offsets.size(); ++sample) {
AppendVMovLiteral(&test.code, 20, offsets[sample]);
u32 coordinate = 21;
if (opcodes[sample] == 0x32u) {
for (float gradient : {0.25f, 0.f, 0.f, 0.25f}) {
AppendVMovLiteral(&test.code, coordinate++, std::bit_cast<u32>(gradient));
}
}
// All instructions address the center of texel (2, 1) before applying the offset.
AppendVMovLiteral(&test.code, coordinate, std::bit_cast<u32>(0.625f));
AppendVMovLiteral(&test.code, coordinate + 1, std::bit_cast<u32>(0.375f));
if (opcodes[sample] == 0x34u) AppendVMovU32(&test.code, coordinate + 2, 0);
test.code.push_back(EncodeMimg0(opcodes[sample], 0x1));
test.code.push_back(EncodeMimg1(sample, 20));
AppendStoreVgpr(&test.code, sample, sample);
}
AppendEnd(&test.code);
test.sampled_image_rgba = MakeRgbaImage(4, 4);
SetRgbaPixel(&test.sampled_image_rgba, 4, 2, 1, std::bit_cast<u32>(16.f), 0, 0, 0);
constexpr std::array<std::array<u32, 2>, 4> neighbors{{{3, 1}, {1, 1}, {2, 0}, {2, 2}}};
for (u32 sample = 0; sample < neighbors.size(); ++sample) {
const auto value = std::bit_cast<u32>(float(sample + 1));
SetRgbaPixel(&test.sampled_image_rgba, 4, neighbors[sample][0], neighbors[sample][1],
value, 0, 0, 0);
test.expected.push_back(value);
}
test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"Lod|ConstOffset", "Grad|ConstOffset"};
test.forbidden_spirv = {"OpImageQuerySizeLod", "OpFDiv", "OpBitFieldSExtract", "OpSwitch"};
return test;
}
@@ -34892,7 +35232,8 @@ TestCase ImageGatherCompareOpcodes() {
O::V_MOV_B32, O::IMAGE_GATHER4_C, O::IMAGE_GATHER4_C_LZ,
O::IMAGE_GATHER4_C_O, O::IMAGE_GATHER4_C_LZ_O, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.required_spirv = {"OpImageDrefGather", "OpBitFieldSExtract"};
test.required_spirv = {"OpImageDrefGather", "ConstOffset"};
test.forbidden_spirv = {"OpBitFieldSExtract"};
test.compile_only = true;
return test;
}
@@ -35444,7 +35785,7 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
std::memcpy(&memory_flag_bits, &memory_flags, sizeof(memory_flags));
auto &sample = block->AppendNewInst(
ValueOpcode::ImageSampleRaw,
{Value(&image), Value(&sampler), Value(&address)}, memory_flag_bits);
{Value(&image), Value(&sampler), Value(&address), Value(true)}, memory_flag_bits);
auto &sample_x = block->AppendNewInst(ValueOpcode::CompositeExtractU32x4,
{Value(&sample), Value(0u)});
block->AppendNewInst(ValueOpcode::ReferenceU32, {Value(&sample_x)});
@@ -35655,29 +35996,63 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
resource == 4u ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, layers));
}
const auto native_sampler = vulkan.CreateSampler(test.name);
for (const bool fetch : {false, true}) {
enum class Access { Sample, Gather, GatherHomogeneous, Fetch };
auto *access = &sample;
for (const auto mode : {Access::Sample, Access::Gather,
Access::GatherHomogeneous, Access::Fetch}) {
const bool fetch = mode == Access::Fetch;
const bool gather = mode == Access::Gather || mode == Access::GatherHomogeneous;
const bool homogeneous = mode == Access::GatherHomogeneous;
if (mode == Access::Gather) {
test.name = "IndirectImageGatherContiguousRuns";
const auto position = std::ranges::find_if(*block, [&](const Inst &inst) {
return &inst == access;
});
auto &read = *block->PrependNewInst(
position, ValueOpcode::ImageGatherRaw,
{Value(&image), Value(&sampler), Value(&address), Value(0u), Value(0u),
Value(0u), Value(0u), Value(true)}, memory_flag_bits);
access->ReplaceUsesWith(Value(&read), false);
access = &read;
compiled.program.info.images[4].cube = false;
auto &gather_memory = compiled.program.memory_info[0];
gather_memory.dmask = 1;
gather_memory.image_sample_flags = ShaderRecompiler::Decoder::ImageSampleFlagLevelZero |
ShaderRecompiler::Decoder::ImageSampleFlagOffset;
// GATHER4_LZ_O places its packed signed offset before the coordinates.
address.SetArg(0, Value(0x3fu));
address.SetArg(1, Value(std::bit_cast<u32>(0.75f)));
address.SetArg(2, Value(std::bit_cast<u32>(0.25f)));
address.SetArg(3, Value(0u));
}
auto &selected_resources = compiled.program.info.images[0].indirect_resources;
selected_resources = homogeneous ? std::vector<u32>{0u, 2u, 3u, 5u, 6u}
: root.indirect_resources;
if (homogeneous) test.name = "IndirectImageGatherHomogeneous";
compiled.resources.flattened_srt[6] = homogeneous ? 4u : ordinals[5];
if (fetch) {
test.name = "IndirectImageFetchContiguousRuns";
const auto position = std::ranges::find_if(*block, [&](const Inst &inst) {
return &inst == &sample;
return &inst == access;
});
auto &read = *block->PrependNewInst(
position, ValueOpcode::ImageRead,
{Value(&image), Value(&address), Value(true)}, memory_flag_bits);
sample.ReplaceUsesWith(Value(&read), false);
access->ReplaceUsesWith(Value(&read), false);
sampler.Invalidate();
compiled.program.info.samplers.clear();
compiled.program.info.sampled_pairs.clear();
compiled.program.info.images[4].cube = false;
compiled.program.memory_info[0].dmask = 0xf;
compiled.program.memory_info[0].image_sample_flags = 0;
compiled.program.memory_info[0].image_has_mip = true;
for (u32 component = 0; component < 4u; ++component)
address.SetArg(component, Value(0u));
// Native DIM=2DArray places LOD after the layer, even for a 2D candidate.
address.SetArg(3, Value(1u));
compiled.program.shader_info_complete = false;
CollectShaderInfo(compiled.program, {.compute = &compute});
}
compiled.program.shader_info_complete = false;
CollectShaderInfo(compiled.program, {.compute = &compute});
for (const u32 wave_size : {32u, 64u}) {
compute.wave_size = wave_size;
compute.host_subgroup_size = vulkan.SubgroupSize();
@@ -35688,31 +36063,38 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
compiled.packed_user_data.resize(compiled.program.bindings.ShaderDataDwords());
ValidateSpirv(test.name, compiled.spirv);
const auto halves = wave_size > compute.host_subgroup_size ? 2u : 1u;
const auto image_opcode = fetch ? "OpImageFetch" : gather ? "OpImageGather"
: "OpImageSampleExplicitLod";
Require(test.name, "native array image runs", tools.Disassemble(compiled.spirv, &text) &&
CountText(text, fetch ? "OpImageFetch" : "OpImageSampleExplicitLod") == 3u * halves,
CountText(text, image_opcode) == (homogeneous ? 1u : 3u) * halves,
"mixed runs expanded into per-image accesses");
if (fetch) {
if (homogeneous) {
Require(test.name, "homogeneous gather array", text.find("OpSwitch") == std::string::npos,
"homogeneous gather emitted a descriptor switch");
}
if (fetch || gather) {
std::vector<u32> decorated;
std::vector<u32> fetched_images;
std::vector<u32> image_operands;
for (size_t offset = 5; offset < compiled.spirv.size();) {
const auto words = std::span<const u32>(compiled.spirv).subspan(
offset, compiled.spirv[offset] >> 16u);
const auto opcode = static_cast<spv::Op>(words[0] & 0xffffu);
if (opcode == spv::OpDecorate && words[2] == spv::DecorationNonUniform)
decorated.push_back(words[1]);
if (opcode == spv::OpImageFetch) fetched_images.push_back(words[3]);
if (opcode == (fetch ? spv::OpImageFetch : spv::OpImageGather))
image_operands.push_back(words[3]);
offset += words.size();
}
Require(test.name, "nonuniform fetch operands",
std::ranges::count_if(fetched_images, [&](u32 operand) {
Require(test.name, "nonuniform image operands",
std::ranges::count_if(image_operands, [&](u32 operand) {
return std::ranges::find(decorated, operand) != decorated.end();
}) == 2u * halves,
"a dynamically selected fetch image lacks its nonuniform decoration");
}) == (homogeneous ? 1u : 2u) * halves,
"a dynamically selected image operand lacks its nonuniform decoration");
}
test.initial.assign(wave_size, 0xdeadbeefu);
test.expected.assign(wave_size, 0u);
for (u32 lane = 0; lane < ordinals.size(); ++lane) {
const auto resource = root.indirect_resources[ordinals[lane]];
const auto resource = selected_resources[compiled.resources.flattened_srt[1u + lane]];
test.expected[lane] = std::bit_cast<u32>(float(resource + (fetch && resource != 0u ? 10u : 0u)));
}
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size());
@@ -35720,7 +36102,7 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
fetch ? vk::Sampler{} : native_sampler, textures);
const auto actual = vulkan.ReadBuffer(test.name, buffer, test.expected.size());
vulkan.DestroyBuffer(&buffer);
CompareWords(test, "root, native slots, cube and mapping bounds", test.expected, actual);
CompareWords(test, "root, native slots, array runs and mapping bounds", test.expected, actual);
}
std::printf("[compute] %-32s ok\n", test.name);
}
@@ -37537,8 +37919,9 @@ std::vector<TestCase> MakeCases() {
AddCase(ImageLoadR32SintUsesSignedSampledImage);
AddCase(ImageLoadPackedUintUnpacksAndSwizzles);
AddCase(ImageSamplePackedUintConvertsSampleAndGather);
AddCase(ImageSampleUScaled8<false>);
AddCase(ImageSampleUScaled8<true>);
AddCase(ImageSamplePacked8<false>);
AddCase(ImageSamplePacked8<true>);
AddCase(ImageSamplePacked8<false, true>);
AddCase(ImageLoadR128IgnoresAdjacentMaskSgprs);
AddCase(ImageLoad1DUsesScalarCoordinate);
AddCase(ImageGather2DInstructionWith1DDescriptor);
@@ -37564,7 +37947,10 @@ std::vector<TestCase> MakeCases() {
AddCase(ImageD16GatherPacksHalfPairs);
AddCase(ImageSampleA16SamplerCoordsOnGpu);
AddCase(ImageSampleOpcodeAliasUsesNormalCoords);
AddCase(ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu);
AddCase(ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<32>);
AddCase(ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<64>);
AddCase(ImageSampleOffsetsReadFourNeighbors<32>);
AddCase(ImageSampleOffsetsReadFourNeighbors<64>);
AddCase(ImageSampleA16CompareBiasRdna2AddressOrder);
AddCase(ImageGatherCompareOpcodes);
AddCase(ImageStoreVariants);
@@ -39975,7 +40361,7 @@ void CheckImageSamplerSpecialization() {
sizeof(signed_memory_flags));
mixed_sampler_block->AppendNewInst(
ValueOpcode::ImageSampleRaw,
{Value(&image_handle), Value(&sampler_handle), Value(&image_address)},
{Value(&image_handle), Value(&sampler_handle), Value(&image_address), Value(true)},
signed_memory_flag_bits);
ShaderRecompiler::IR::DescriptorValue native_image_descriptor{};
@@ -41903,9 +42289,24 @@ void CheckAgcSystemTable(RenderContext &renderer) {
0xbf800000, 0x85ea807e, 0x88ea106a, 0xbf870003,
0xbefc03ff, 0x8a6ca000, 0xbf920009, 0xbefd210e};
LibKernel::Memory::InstallGpuResources(&renderer);
Require(name, "driver module startup", Gen5Driver::Initialize() == 0,
"driver module startup failed before guest initialization");
const auto direct_memory_size = LibKernel::Memory::KernelGetDirectMemorySize();
int64_t available_offset = -1;
size_t available_size = 0;
Require(name, "remaining direct pool",
LibKernel::Memory::KernelAvailableDirectMemorySize(
0, direct_memory_size, 0, &available_offset, &available_size) == 0 &&
available_size == direct_memory_size - 0x200000,
"driver startup did not reserve its backing in the reported direct pool");
int64_t guest_allocation = -1;
Require(name, "guest consumes remaining direct pool",
LibKernel::Memory::KernelAllocateDirectMemory(
0, direct_memory_size, available_size, 0x4000, 0, &guest_allocation) == 0,
"guest could not allocate all remaining direct memory");
uint32_t state = 0xabcdef01u;
Require(name, "driver startup", Gen5::AgcInit(&state, 13) == 0 && state == 0xabcdef01u,
"Agc startup failed or changed its ignored public state pointer");
"Agc startup required free guest memory or changed its ignored public state pointer");
LibKernel::Memory::VirtualQueryInfo mapping{};
Require(name, "shared driver backing",
LibKernel::Memory::KernelVirtualQuery(reinterpret_cast<void *>(driver_base), 0,
@@ -41932,10 +42333,14 @@ void CheckAgcSystemTable(RenderContext &renderer) {
Require(name, "default instrumentation", *instrumentation == 0,
"unused vertex validation was enabled at startup");
*instrumentation = 0x12340000u;
Require(name, "repeat startup", Gen5::AgcInit(nullptr, 13) == 0 &&
Require(name, "repeat startup", Gen5Driver::Initialize() == 0 &&
Gen5::AgcInit(nullptr, 13) == 0 &&
*instrumentation == 0x12340000u,
"repeat Agc initialization reset existing system state");
*instrumentation = 0;
Require(name, "release guest allocation",
LibKernel::Memory::KernelCheckedReleaseDirectMemory(guest_allocation, available_size) == 0,
"guest allocation release failed");
LibKernel::Memory::InstallGpuResources(nullptr);
std::printf("[host] %-32s ok\n", name);
}
@@ -42516,6 +42921,43 @@ void CheckPm4IndirectControlFlow(RenderContext &renderer) {
Gen5::AgcCbNop(reinterpret_cast<Gen5::CommandBuffer*>(&payload_dcb), 3) == payload.data() &&
payload == std::array<uint32_t, 3>{0xc0011000u, 0x12345678u, 0x9abcdef0u},
"NOP emitter overwrote the caller's reserved payload");
alignas(8) uint64_t released = 0;
std::array<uint32_t, 20> disabled_commands{};
const auto disabled_write = write(&returned, 99);
std::copy(disabled_write.begin(), disabled_write.end(), disabled_commands.begin());
disabled_commands[5] = KYTY_PM4(2, Pm4::IT_NUM_INSTANCES, 0);
disabled_commands[6] = 0x68750123u;
CommandBufferLayout release_dcb{disabled_commands.data() + 7, disabled_commands.data() + 15,
disabled_commands.data() + 7, disabled_commands.data() + 15, nullptr, nullptr, 0};
auto* release = Gen5::AgcCbReleaseMem(
reinterpret_cast<Gen5::CommandBuffer*>(&release_dcb), 0x28, 0, 0, 0,
reinterpret_cast<const volatile Gen5::Label*>(&released), 2, 77, 0, 0, 0, 0);
const auto enabled_write = write(&selected, 88);
std::copy(enabled_write.begin(), enabled_write.end(), disabled_commands.begin() + 15);
const auto unpatched_commands = disabled_commands;
Require(name, "SetNop packets",
release == disabled_commands.data() + 7 &&
Gen5::AgcSetNop(disabled_commands.data()) == 0 &&
Gen5::AgcSetNop(disabled_commands.data() + 5) == 0 && Gen5::AgcSetNop(release) == 0 &&
disabled_commands[0] == ((disabled_write[0] & 0xffff00ffu) | (Pm4::IT_NOP << 8u)) &&
disabled_commands[5] == KYTY_PM4(2, Pm4::IT_NOP, Pm4::R_ZERO) &&
disabled_commands[6] == 0x123u &&
release[0] == KYTY_PM4(8, Pm4::IT_NOP, Pm4::R_ZERO) &&
unpatched_commands[7] == KYTY_PM4(8, Pm4::IT_NOP, Pm4::R_RELEASE_MEM) &&
Gen5::AgcGetPacketSize(disabled_commands.data()) == disabled_write.size() &&
Gen5::AgcGetPacketSize(disabled_commands.data() + 5) == 2 &&
Gen5::AgcGetPacketSize(release) == 8 &&
std::equal(disabled_commands.begin() + 1, disabled_commands.begin() + 5,
unpatched_commands.begin() + 1) &&
std::equal(disabled_commands.begin() + 8, disabled_commands.end(),
unpatched_commands.begin() + 8),
"SetNop changed a packet length, kept its operation or marker, or touched its payload");
selected = returned = 0;
Pm4Execution disabled_execution;
Require(name, "SetNop execution",
processor.Process(disabled_execution, disabled_commands) == Pm4ProcessResult::Complete &&
returned == 0 && released == 0 && selected == 88,
"a disabled packet still executed or hid the packet after it");
const auto then_commands = write(&selected, 11);
const auto else_commands = write(&selected, 33);
const auto branch_suffix = write(&selected, 44);
@@ -43089,8 +43531,9 @@ int main(int argc, char **argv) {
}
if (argc == 2 && std::strcmp(argv[1], "--scaled-texture-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, ImageSampleUScaled8<false>());
RunCase(&vulkan, ImageSampleUScaled8<true>());
RunCase(&vulkan, ImageSamplePacked8<false>());
RunCase(&vulkan, ImageSamplePacked8<true>());
RunCase(&vulkan, ImageSamplePacked8<false, true>());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--packed-texture-only") == 0) {
@@ -43175,6 +43618,16 @@ int main(int argc, char **argv) {
RunCase(&vulkan, FlatVirtualAddressRebasesGuestAllocation());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--image-sample-offset-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, ImageSampleOffsetsReadFourNeighbors<32>());
RunCase(&vulkan, ImageSampleOffsetsReadFourNeighbors<64>());
RunCase(&vulkan, ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<32>());
RunCase(&vulkan, ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<64>());
RunCase(&vulkan, ImageSampleAndGather());
RunCase(nullptr, ImageGatherCompareOpcodes());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--image-gather-lod-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, ImageGatherExplicitLod());
+1 -1
View File
@@ -1316,7 +1316,7 @@ void TestShaderFeatureCollection() {
{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), Value(true)});
auto &shared = block->AppendNewInst(ValueOpcode::LoadSharedU32,
{Value(0u), Value(true)});
shared.SetFlags(MemoryFlags{.index = 0});