Compare commits

...
Author SHA1 Message Date
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
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
Kelvin Oliveiraandnmzik 1cfb9522a3 controller: use Back as the touchpad click on pads without a touchpad (#1355)
* controller: use Back as the touchpad click on pads without a touchpad

Gamepads without a touchpad (Switch, Xbox, 8BitDo, ...) cannot click it, so
games that require the touchpad can't be played with them. The keyboard
already emulates it (Backspace/Tab); gamepads had nothing.

For SDL_GAMEPAD_BUTTON_BACK on a pad that reports no touchpads, report
PAD_BUTTON_TOUCH_PAD plus a finger at the centre of the touchpad. Pads with a
real touchpad are unchanged.

Fixes #1298

* controller: establish touch before fallback click

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-10-10 19:20:34 +02:00
nmzik f63d3befdc Add startup splash screen 2026-10-10 19:07:06 +02:00
nmzik 307fdcf7ad add self updater (#1352)
* add self updater

* remove updater tests after validation
2026-10-10 05:42:32 +02:00
Mac 679be25c37 shader: recognize remaining disabled debug branches 2026-10-10 04:31:55 +02:00
fxpw fa56c5eae9 shader: add scalar zero-bit count and search operations 2026-10-10 04:31:55 +02:00
foufouadi 3b9e176b70 shader: support VOP1/VOP2 DPP8 fetch-inactive 2026-10-10 04:31:55 +02:00
Bartlomiej Wolynczyk 053f5fd3cc shader: emit DPP row_share instead of an identity move 2026-10-10 04:31:55 +02:00
HamsterCL f3836bb831 tests: fix the Linux build of ResourceMaterializationTests (#1333)
tests: avoid empty optional::emplace() on IndirectDescriptor (fixes Linux build)

clang with libstdc++ 14 does not treat std::optional<DescriptorSource::IndirectDescriptor>
as default-constructible (nested struct with default member initializers), so the
argument-less emplace() fails to resolve. Pass an explicit value-initialized
IndirectDescriptor, matching the form already used elsewhere in this file.
2026-10-10 03:47:38 +02:00
popcoder565andClaude Opus 5.5 2f4d0f383e shader: add SDWA support for V_MIN_I32 (#1342)
WWE 2K26 stops building a shader CFG with "unsupported family=VOP2
opcode=0x11 raw=[0x220404f9 0x00060605] reason=VOP2 SDWA modifier is not
supported for opcode".

V_MIN_I32 takes the same integer path as V_MAX_I32, so give it the same
full-destination SDWA profile. The test runs the game's encoding, which
takes the signed minimum with the zero-extended low byte of a register.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 03:43:08 +02:00
TheCruZ d247d3d60c shader: complete signed 64-bit integer comparisons 2026-10-10 03:36:25 +02:00
nmzik 93452b24e3 renderer: cache dynamic graphics state 2026-10-10 03:28:02 +02:00
nmzik 5559cfcbb2 renderer: optimize tiling and reuse centralized image layouts 2026-10-10 03:27:13 +02:00
nmzik 1c301c9aab Revert "shader: keep scalar payload and terminal conditions on the GPU" 2026-10-09 21:10:15 +02:00
nmzikandHector Mödinger S. 59183dbfa8 shader: add missing F64 VOPC compares
Co-authored-by: Hector Mödinger S. <hectormodinger@gmail.com>
2026-10-09 21:01:51 +02:00
71 changed files with 4772 additions and 2585 deletions

No files matched your search

+5 -1
View File
@@ -257,6 +257,7 @@ jobs:
"$install_dir/libMoltenVK.dylib" \
"$app_dir/Contents/MacOS/KytyPS5" \
"$app_dir/Contents/MacOS/kyty_emulator" \
"$app_dir/Contents/MacOS/kyty_updater" \
"$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do
file "$binary"
lipo "$binary" -verify_arch x86_64
@@ -266,6 +267,7 @@ jobs:
"$install_dir/libMoltenVK.dylib" \
"$app_dir/Contents/MacOS/KytyPS5" \
"$app_dir/Contents/MacOS/kyty_emulator" \
"$app_dir/Contents/MacOS/kyty_updater" \
"$app_dir/Contents/Frameworks/libMoltenVK.dylib"; do
codesign --verify --strict "$binary"
done
@@ -407,6 +409,7 @@ jobs:
run: |
file _Build/linux/install/launcher
file _Build/linux/install/kyty_emulator
test -x _Build/linux/install/kyty_updater
file _Build/linux/install/kyty_emulator | grep -q "ELF 64-bit LSB .*x86-64"
ldd _Build/linux/install/kyty_emulator > /dev/null
readelf -d _Build/linux/install/launcher | grep -q 'RPATH.*\$ORIGIN/lib'
@@ -495,7 +498,8 @@ jobs:
test -d "$linux_dir"
chmod a+x \
"$linux_dir/launcher" \
"$linux_dir/kyty_emulator"
"$linux_dir/kyty_emulator" \
"$linux_dir/kyty_updater"
tar -C "$linux_dir" -czf \
"$GITHUB_WORKSPACE/$RELEASE_NAME-Linux-x86_64.tar.gz" .
+2
View File
@@ -657,6 +657,8 @@ if(BUILD_TESTING)
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --context-state-only)
add_test(NAME gpu_tiler
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --gpu-tiler-only)
add_test(NAME image_transfer_layout
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --image-layout-only)
add_test(NAME texture_cache_layered_image
COMMAND $<TARGET_FILE:shader_recompiler_compute_tests> --layered-image-only)
+71 -20
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
@@ -338,6 +381,14 @@ folders recursively for game directories containing `eboot.bin` and ZArchive (`.
whose archive root contains `eboot.bin`. Select a detected game and run it from the game list.
ZArchive dumps are mounted read-only and streamed directly; they do not need to be extracted first.
Official release builds include a built-in updater. Leave **Check for updates on startup** enabled,
or click **Check for updates**, then choose **Install Update**. The launcher downloads the package
for your platform from GitHub, checks its SHA-256 checksum, and restarts after installation. Close
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 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:
@@ -393,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.
+1
View File
@@ -54,6 +54,7 @@ struct ConfigOptions {
int32_t gpu_index = -1;
bool fullscreen_enabled = false;
bool hide_cursor_enabled = false;
bool splash_enabled = true;
bool vr_enabled = false;
bool amd_cpu_enabled = false;
uint32_t vblank_frequency = 60;
+4
View File
@@ -11,6 +11,7 @@
#include "common/subsystems.h"
#include "common/systemInfo.h"
#include "common/threads.h"
#include "graphics/presentation/systemOverlay.h"
#include "graphics/presentation/window.h"
#include "kernel/fileSystem.h"
#include "kernel/memory.h"
@@ -138,6 +139,9 @@ static void Init(const Config::ConfigOptions& cfg, const std::filesystem::path&
Libs::LibKernel::Memory::SetFlexibleMemorySize(flexible_memory_size);
}
}
if (cfg.splash_enabled) {
Libs::Graphics::InitializeSplashScreen(param_json.parent_path());
}
// Initialization order is explicit; destruction is automatic and reversed.
subsystems.Initialize<Loader::Timer::Lifecycle>();
+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},
+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;
}
+138 -265
View File
@@ -110,37 +110,6 @@ constexpr uint64_t NumFramesBeforeRemoval = 32;
return true;
}
[[nodiscard]] std::vector<vk::BufferImageCopy> BuildDepthCopies(const ImageInfo& info,
uint64_t slice_stride,
vk::ImageAspectFlags aspect) {
std::vector<vk::BufferImageCopy> copies(info.resources.layers);
for (uint32_t layer = 0; layer < info.resources.layers; ++layer) {
auto& copy = copies[layer];
copy.bufferOffset = slice_stride * layer;
copy.bufferRowLength = info.pitch;
copy.bufferImageHeight = info.extent.height;
copy.imageSubresource = {aspect, 0, layer, 1};
copy.imageExtent = {info.extent.width, info.extent.height, 1};
}
return copies;
}
[[nodiscard]] std::vector<GpuTileInfo> BuildDepthTiles(const ImageInfo& info) {
TileBlockLayout block {};
EXIT_NOT_IMPLEMENTED(
!TileGetBlockLayout(TileBlockFamily::Depth64KB, info.bytes_per_block, block));
const auto full_slice_size = info.data.size / info.resources.layers;
std::vector<GpuTileInfo> tiles;
tiles.reserve(info.resources.layers);
for (uint32_t layer = 0; layer < info.resources.layers; ++layer) {
const auto offset = full_slice_size * layer;
tiles.push_back({block.family, block.bytes_per_element, offset, full_slice_size, offset,
full_slice_size, 0, info.extent.width, info.extent.height, 1, info.pitch});
tiles.back().surface_z = layer;
}
return tiles;
}
} // namespace
TextureCache::TextureCache(GraphicContext& graphics, CommandScheduler& scheduler,
@@ -570,20 +539,20 @@ bool TextureCache::CopyD16(Image& destination, Image& source) {
EXIT_IF(layers == 0 || depth_slice > UINT64_MAX / layers || color_slice > UINT64_MAX / layers);
const auto depth_size = depth_slice * layers;
const auto color_size = color_slice * layers;
std::vector<vk::BufferImageCopy> depth_copies(layers);
std::vector<vk::BufferImageCopy> color_copies(layers);
for (uint32_t layer = 0; layer < layers; layer++) {
depth_copies[layer].bufferOffset = depth_slice * layer;
depth_copies[layer].bufferRowLength = depth.info.pitch;
depth_copies[layer].bufferImageHeight = depth.info.extent.height;
depth_copies[layer].imageSubresource = {vk::ImageAspectFlagBits::eDepth, 0, layer, 1};
depth_copies[layer].imageExtent = depth.info.extent;
color_copies[layer].bufferOffset = color_slice * layer;
color_copies[layer].bufferRowLength = color.info.pitch;
color_copies[layer].bufferImageHeight = color.info.extent.height;
color_copies[layer].imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, layer, 1};
color_copies[layer].imageExtent = color.info.extent;
}
const std::array depth_copies {
vk::BufferImageCopy {0,
depth.info.pitch,
depth.info.extent.height,
{vk::ImageAspectFlagBits::eDepth, 0, 0, layers},
{},
depth.info.extent}};
const std::array color_copies {
vk::BufferImageCopy {0,
color.info.pitch,
color.info.extent.height,
{vk::ImageAspectFlagBits::eColor, 0, 0, layers},
{},
color.info.extent}};
auto depth_buffer = m_tiler.GetScratchBuffer(depth_size);
auto color_buffer = m_tiler.GetScratchBuffer(color_size, depth_buffer.buffer);
@@ -740,7 +709,11 @@ ImageId TextureCache::ResolveDepthOverlap(const ImageInfo& requested, BindingTyp
if (retain_cached_layout) {
info.data = cached.info.data;
info.resources = cached.info.resources;
info.mip_layout = cached.info.mip_layout;
info.mip_layout = cached.info.mip_layout;
info.tiling = cached.info.tiling;
info.tiled_slice_stride = cached.info.tiled_slice_stride;
info.linear_slice_stride = cached.info.linear_slice_stride;
info.first_tail_level = cached.info.first_tail_level;
}
info.htile_clear_mask = 0;
const auto replacement_id = InsertImage(info);
@@ -801,7 +774,14 @@ TextureCache::OverlapResult TextureCache::ResolveOverlap(const ImageInfo& reques
// a level into or out of the mip tail. These are separate guest layouts.
if (requested.tile_mode != cached.info.tile_mode ||
(requested.resources == cached.info.resources &&
requested.mip_layout != cached.info.mip_layout)) {
!std::equal(requested.mip_layout.begin(), requested.mip_layout.end(),
cached.info.mip_layout.begin(),
[](const ImageMipInfo& left, const ImageMipInfo& right) {
return left.offset == right.offset && left.size == right.size &&
left.pitch == right.pitch && left.height == right.height &&
left.tail_x == right.tail_x && left.tail_y == right.tail_y &&
left.surface_z == right.surface_z;
}))) {
if (safe_to_delete) {
FreeImage(cached_id);
}
@@ -913,168 +893,55 @@ ImageId TextureCache::ExpandImage(const ImageInfo& info, ImageId source_id) {
return expanded_id;
}
struct TextureCache::TextureTransfer {
TextureUploadLayout layout;
std::vector<vk::BufferImageCopy> regions;
std::vector<GpuTileInfo> tiles;
ColorTransform color_transform = ColorTransform::None;
bool valid = false;
[[nodiscard]] uint64_t LinearSize() const {
uint64_t size = 0;
for (const auto& tile: tiles) {
size = std::max(size, tile.linear_offset + tile.linear_size);
}
return size;
bool TextureCache::CanDownload(const Image& image) const {
const auto& info = image.info;
if (info.samples != 1 || image.backing.samples != 1 || info.resources.layers == 0 ||
info.data.size % info.resources.layers != 0) {
return false;
}
};
struct TextureCache::ImageDownload {
TextureTransfer texture;
bool depth_target = false;
bool valid = false;
};
TextureCache::TextureTransfer
TextureCache::BuildTextureTransfer(const Image& image, BindingType binding,
TransferDirection direction) const {
const auto& info = image.info;
const bool upload = direction == TransferDirection::Upload;
const bool render_target = binding == BindingType::RenderTarget;
const bool video_out = binding == BindingType::VideoOut;
uint32_t layers = info.TransferLayers();
bool volume = info.IsVolume();
const char* owner = "TextureCache readback";
TextureTransfer transfer;
transfer.color_transform = info.GetColorTransform();
if (upload) {
if ((render_target || video_out) &&
(info.resources.layers == 0 || info.data.size % info.resources.layers != 0 ||
info.samples != 1 || image.backing.samples != 1)) {
EXIT("TextureCache: invalid color-attachment upload\n");
}
owner = "TextureCache";
if (render_target) {
owner = "RenderTarget";
} else if (binding == BindingType::Storage) {
owner = "StorageTextureCache";
} else if (video_out) {
if (info.metadata.compression != VideoOutCompression::Uncompressed) {
EXIT("TextureCache: invalid color-attachment upload\n");
}
layers = info.resources.layers;
volume = false;
owner = "VideoOut";
}
if (UploadBinding(image) == BindingType::DepthTarget) {
return IsSupportedDepthPlaneReadback(info) &&
Prospero::NumBytesPerElement(info.guest_format) == info.bytes_per_block;
}
transfer.layout = TextureCalcUploadLayout(info.guest_format, info.extent.width,
info.extent.height, info.resources.levels, layers,
info.tile_mode, info.data.size, volume, owner);
transfer.regions = TextureBuildImageCopies(transfer.layout);
if (info.IsDepth()) {
for (auto& region: transfer.regions) {
region.imageSubresource.aspectMask = vk::ImageAspectFlagBits::eDepth;
}
}
if (transfer.layout.surface.description.tile_mode != Prospero::TileMode::kLinear) {
if (!TextureBuildGpuTileInfos(info.data.size, transfer.regions, transfer.layout,
info.resources.levels, transfer.tiles)) {
return transfer;
}
}
transfer.valid = true;
return transfer;
}
TextureCache::ImageDownload TextureCache::BuildDownload(const Image& image) const {
const auto& info = image.info;
const auto binding = UploadBinding(image);
ImageDownload transfer {.depth_target = binding == BindingType::DepthTarget};
if (info.samples != 1 || image.backing.samples != 1) {
return transfer;
}
if (transfer.depth_target) {
transfer.valid = IsSupportedDepthPlaneReadback(info) && info.resources.layers != 0 &&
info.data.size % info.resources.layers == 0 &&
Prospero::NumBytesPerElement(info.guest_format) == info.bytes_per_block;
return transfer;
}
if (info.metadata.compression != VideoOutCompression::Uncompressed) {
return transfer;
}
transfer.texture = BuildTextureTransfer(image, binding, TransferDirection::Download);
transfer.valid = transfer.texture.valid;
return transfer;
return info.metadata.compression == VideoOutCompression::Uncompressed &&
(!info.IsTiled() || !Prospero::IsFmaskTextureFormat(info.guest_format)) &&
info.LinearSize() != 0;
}
void TextureCache::UploadImage(Image& image, Buffer& source, uint64_t source_offset) {
auto& destination = image.depth_id ? m_slot_images[image.depth_id] : image;
const auto binding = image.depth_id ? BindingType::DepthTarget : UploadBinding(image);
const auto upload = [&](std::vector<vk::BufferImageCopy>& copies, TileManager::Result linear) {
for (auto& copy: copies) {
copy.bufferOffset += linear.offset;
}
destination.Upload(copies, linear.buffer, linear.offset, linear.size);
};
const auto& info = image.info;
const auto binding = image.depth_id ? BindingType::DepthTarget : UploadBinding(image);
if (binding != BindingType::DepthTarget) {
const auto& info = image.info;
auto transfer = BuildTextureTransfer(image, binding, TransferDirection::Upload);
if (!transfer.valid) {
EXIT("TextureCache: invalid texture upload: binding=%u addr=0x%016" PRIx64
" size=0x%016" PRIx64 " format=%u tile=%u family=%u extent=%ux%ux%u "
"pitch=%u levels=%u layers=%u samples=%u\n",
static_cast<uint32_t>(binding), info.data.address, info.data.size,
static_cast<uint32_t>(info.guest_format), static_cast<uint32_t>(info.tile_mode),
static_cast<uint32_t>(transfer.layout.surface.texture.block.family), info.extent.width,
info.extent.height, info.extent.depth, info.pitch, info.resources.levels,
info.resources.layers, info.samples);
if ((binding == BindingType::RenderTarget || binding == BindingType::VideoOut) &&
(info.resources.layers == 0 || info.data.size % info.resources.layers != 0 ||
info.samples != 1 || image.backing.samples != 1)) {
EXIT("TextureCache: invalid color-attachment upload\n");
}
TileManager::Result linear {source.Handle(), source_offset, info.data.size};
if (!transfer.tiles.empty()) {
linear = m_tiler.Detile(source.Handle(), source_offset, info.data.size,
transfer.LinearSize(), transfer.tiles, transfer.color_transform);
} else if (transfer.color_transform != ColorTransform::None) {
linear = m_tiler.TransformColor(linear, transfer.color_transform, true);
if (binding == BindingType::VideoOut &&
info.metadata.compression != VideoOutCompression::Uncompressed) {
EXIT("TextureCache: invalid video-out upload\n");
}
upload(transfer.regions, linear);
const auto linear = m_tiler.Detile(source.Handle(), source_offset, info);
const auto copies = info.BufferCopies(linear.offset);
destination.Upload(copies, linear.buffer, linear.offset, linear.size);
return;
}
// The stencil plane has its own row pitch and shares the native image
// with the depth plane.
auto info = destination.info;
if (image.depth_id) {
info.data = image.info.data;
info.resources = {image.stencil_subresources.level_count,
image.stencil_subresources.layer_count};
info.guest_format = Prospero::BufferFormat::k8UInt;
info.bytes_per_block = 1;
if (info.IsTiled()) info.pitch = TileGetDepthPitch(info.extent.width, 1, 0);
}
if (info.samples != 1 || destination.backing.samples != 1 ||
info.resources.layers == 0 || info.data.size % info.resources.layers != 0 ||
Prospero::NumBytesPerElement(info.guest_format) != info.bytes_per_block) {
EXIT("TextureCache: invalid depth upload\n");
}
const auto layers = info.resources.layers;
const auto full_slice_size = info.data.size / layers;
auto copies = BuildDepthCopies(info, full_slice_size, image.depth_id
? vk::ImageAspectFlagBits::eStencil
: vk::ImageAspectFlagBits::eDepth);
const auto layers = info.resources.layers;
const auto full_slice_size = info.linear_slice_stride;
auto linear = m_tiler.Detile(source.Handle(), source_offset, info);
auto copies = info.BufferCopies(linear.offset);
if (image.depth_id) {
for (auto& copy: copies) {
copy.imageSubresource.baseArrayLayer += image.stencil_subresources.base_layer;
}
}
TileManager::Result linear {source.Handle(), source_offset, source.Size() - source_offset};
if (info.IsTiled()) {
const auto tiles = BuildDepthTiles(info);
linear =
m_tiler.Detile(source.Handle(), source_offset, info.data.size, info.data.size, tiles);
}
const auto transfer_bytes = image.depth_id ? 1u : DepthAspectTransferBytes(info.pixel_format);
if (transfer_bytes != info.bytes_per_block) {
const uint64_t texels_per_slice = static_cast<uint64_t>(info.pitch) * info.extent.height;
@@ -1095,11 +962,14 @@ void TextureCache::UploadImage(Image& image, Buffer& source, uint64_t source_off
.source_slice_stride = full_slice_size,
.target_slice_stride = transfer_slice});
linear = promoted;
for (uint32_t layer = 0; layer < layers; layer++) {
copies[layer].bufferOffset = transfer_slice * layer;
}
copies.resize(1);
copies[0].bufferOffset = linear.offset;
copies[0].bufferRowLength = info.pitch;
copies[0].bufferImageHeight = info.extent.height;
copies[0].imageSubresource = {vk::ImageAspectFlagBits::eDepth, 0, 0, layers};
copies[0].imageExtent = info.extent;
}
upload(copies, linear);
destination.Upload(copies, linear.buffer, linear.offset, linear.size);
}
void TextureCache::InitializeImage(ImageId id) {
@@ -1257,8 +1127,25 @@ ImageId TextureCache::AssociateStencil(ImageId depth_id, GuestRange stencil) {
}
auto& record = m_slot_images[association];
TouchImage(record);
record.depth_id = depth_id;
record.stencil_subresources = depth.stencil_subresources;
if (record.depth_id != depth_id || record.stencil_subresources != depth.stencil_subresources) {
ImageInfo plane {};
plane.data = stencil;
plane.pixel_format = vk::Format::eS8Uint;
plane.guest_format = Prospero::BufferFormat::k8UInt;
plane.type = depth.info.type;
plane.extent = depth.info.extent;
plane.resources = {depth.stencil_subresources.level_count,
depth.stencil_subresources.layer_count};
plane.tile_mode = depth.info.tile_mode;
plane.pitch =
plane.IsTiled() ? TileGetDepthPitch(plane.extent.width, 1, 0) : depth.info.pitch;
plane.bytes_per_block = 1;
plane.UpdateSize();
record.info = plane;
record.depth_id = depth_id;
record.stencil_subresources = depth.stencil_subresources;
}
return association;
}
@@ -1282,6 +1169,19 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
for (const auto id: candidates) {
const auto& image = m_slot_images[id];
// Stencil descriptors name the associated plane; its depth owner supplies
// the native image and the canonical guest tiling.
if ((desc.type == BindingType::Texture || desc.type == BindingType::Storage) &&
image.depth_id && image.info.data == desc.info.data &&
m_slot_images[image.depth_id].info.samples == desc.info.samples &&
image.info.extent == desc.info.extent && image.info.type == desc.info.type &&
image.info.resources.levels >= desc.info.resources.levels &&
image.info.resources.layers >= desc.info.resources.layers &&
desc.info.bytes_per_block == 1 &&
desc.info.guest_format == Prospero::BufferFormat::k8UInt) {
result = id;
break;
}
if (SameBacking(image.info, desc.info, exact_format)) {
result = id;
}
@@ -1303,7 +1203,7 @@ ImageId TextureCache::FindImage(ImageDesc& desc, bool exact_format) {
}
}
if (result) {
if (result && !m_slot_images[result].depth_id) {
auto& resolved = m_slot_images[result];
if (exact_format && resolved.info.pixel_format != desc.info.pixel_format) {
result = {};
@@ -1685,7 +1585,7 @@ void TextureCache::InvalidateMemory(uint64_t address, uint64_t size) {
void TextureCache::DownloadDepth(Image& image, Buffer& destination, uint64_t destination_offset) {
const auto& info = image.info;
const auto layers = info.resources.layers;
const auto full_slice_size = info.data.size / layers;
const auto full_slice_size = info.linear_slice_stride;
const auto transfer_bytes = DepthAspectTransferBytes(info.pixel_format);
const uint64_t texels_per_slice = static_cast<uint64_t>(info.pitch) * info.extent.height;
EXIT_NOT_IMPLEMENTED(transfer_bytes == 0 || texels_per_slice > UINT32_MAX ||
@@ -1696,31 +1596,31 @@ void TextureCache::DownloadDepth(Image& image, Buffer& destination, uint64_t des
EXIT_NOT_IMPLEMENTED(transfer_slice > UINT64_MAX / layers);
const uint64_t transfer_size = transfer_slice * layers;
EXIT_NOT_IMPLEMENTED(guest_slice > full_slice_size);
auto copies = BuildDepthCopies(info, full_slice_size, vk::ImageAspectFlagBits::eDepth);
if (transfer_bytes == info.bytes_per_block) {
if (!info.IsTiled()) {
for (auto& copy: copies) {
copy.bufferOffset += destination_offset;
}
const auto copies = info.BufferCopies(destination_offset);
image.Download(copies, destination.Handle(), destination_offset, info.data.size);
return;
} else {
const auto copies = info.BufferCopies();
m_tiler.TileImage(image, copies, destination.Handle(), destination_offset,
info.data.size);
}
const auto tiles = BuildDepthTiles(info);
m_tiler.TileImage(image, copies, destination.Handle(), destination_offset, info.data.size,
info.data.size, tiles);
return;
}
EXIT_NOT_IMPLEMENTED(info.bytes_per_block != sizeof(uint16_t) ||
transfer_bytes != sizeof(uint32_t));
for (uint32_t layer = 0; layer < layers; layer++) {
copies[layer].bufferOffset = transfer_slice * layer;
}
const std::array copies {vk::BufferImageCopy {0,
info.pitch,
info.extent.height,
{vk::ImageAspectFlagBits::eDepth, 0, 0, layers},
{},
info.extent}};
auto host_linear = m_tiler.GetScratchBuffer(transfer_size);
image.Download(copies, host_linear.buffer, 0, host_linear.size);
const bool tiled = info.IsTiled();
auto guest_linear = tiled ? m_tiler.GetScratchBuffer(info.data.size, host_linear.buffer)
: TileManager::Result {destination.Handle(), destination_offset,
destination.Size() - destination_offset};
auto guest_linear = tiled ? m_tiler.GetScratchBuffer(info.LinearSize(), host_linear.buffer)
: TileManager::Result {destination.Handle(), destination_offset,
destination.Size() - destination_offset};
m_tiler.ConvertD16(host_linear, guest_linear, TileManager::D16Direction::Demote,
DepthAspectTransferFormat(info.pixel_format) == vk::Format::eD32Sfloat,
{.width = info.extent.width,
@@ -1730,47 +1630,39 @@ void TextureCache::DownloadDepth(Image& image, Buffer& destination, uint64_t des
.target_row_stride = static_cast<uint64_t>(info.pitch) * sizeof(uint16_t),
.source_slice_stride = transfer_slice,
.target_slice_stride = full_slice_size});
if (!tiled) {
return;
if (tiled) {
m_tiler.Tile(guest_linear.buffer, guest_linear.offset, guest_linear.size,
destination.Handle(), destination_offset, info.data.size, info);
}
const auto tiles = BuildDepthTiles(info);
m_tiler.Tile(guest_linear.buffer, guest_linear.offset, info.data.size, destination.Handle(),
destination_offset, info.data.size, tiles);
}
void TextureCache::DownloadImage(Image& image, Buffer& destination, uint64_t destination_offset,
uint64_t destination_size, ImageDownload transfer) {
if (!transfer.valid) {
EXIT("TextureCache: invalid image download transfer\n");
uint64_t destination_size, uint32_t levels) {
if (!CanDownload(image)) {
EXIT("TextureCache: invalid image download\n");
}
if (transfer.depth_target) {
if (destination_size != image.info.data.size) {
EXIT("TextureCache: partial depth image download is unsupported\n");
}
if (UploadBinding(image) == BindingType::DepthTarget) {
EXIT_IF(destination_size != image.info.data.size || levels != image.info.resources.levels);
DownloadDepth(image, destination, destination_offset);
return;
}
auto& texture = transfer.texture;
const auto transform = texture.color_transform;
if (texture.tiles.empty()) {
if (transform != ColorTransform::None) {
auto linear = m_tiler.GetScratchBuffer(destination_size);
image.Download(texture.regions, linear.buffer, 0, linear.size);
m_tiler.TransformColor(linear,
{destination.Handle(), destination_offset, destination_size},
transform, false);
return;
}
for (auto& copy: texture.regions) {
copy.bufferOffset += destination_offset;
}
image.Download(texture.regions, destination.Handle(), destination_offset, destination_size);
return;
const auto& info = image.info;
const auto transform = info.GetColorTransform();
if (info.IsTiled()) {
const auto copies = info.BufferCopies(0, levels);
m_tiler.TileImage(image, copies, destination.Handle(), destination_offset, destination_size,
levels);
} else if (transform != ColorTransform::None) {
auto linear = m_tiler.GetScratchBuffer(destination_size);
const auto copies = info.BufferCopies(0, levels);
image.Download(copies, linear.buffer, 0, linear.size);
m_tiler.TransformColor(linear, {destination.Handle(), destination_offset, destination_size},
transform, false);
} else {
const auto copies = info.BufferCopies(destination_offset, levels);
image.Download(copies, destination.Handle(), destination_offset, destination_size);
}
m_tiler.TileImage(image, texture.regions, destination.Handle(), destination_offset,
destination_size, texture.LinearSize(), texture.tiles, transform);
}
bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uint64_t size) {
@@ -1810,30 +1702,12 @@ bool BufferCache::SynchronizeBufferFromImage(Buffer& buffer, uint64_t vaddr, uin
if (copy_size == 0) {
return false;
}
auto transfer = m_texture_cache.BuildDownload(image);
if (!transfer.valid) {
if (!m_texture_cache.CanDownload(image) ||
(TextureCache::UploadBinding(image) == TextureCache::BindingType::DepthTarget &&
copy_size != image.info.data.size)) {
return false;
}
if (transfer.depth_target && copy_size != image.info.data.size) {
return false;
}
if (!transfer.depth_target && levels < image.info.resources.levels) {
auto& texture = transfer.texture;
std::erase_if(texture.regions, [levels](const vk::BufferImageCopy& region) {
return region.imageSubresource.mipLevel >= levels;
});
if (texture.regions.empty()) {
return false;
}
if (!texture.tiles.empty()) {
texture.tiles.clear();
if (!TextureBuildGpuTileInfos(copy_size, texture.regions, texture.layout, levels,
texture.tiles)) {
return false;
}
}
}
m_texture_cache.DownloadImage(image, buffer, buf_offset, copy_size, std::move(transfer));
m_texture_cache.DownloadImage(image, buffer, buf_offset, copy_size, levels);
return true;
}
@@ -1842,8 +1716,7 @@ bool TextureCache::DownloadImageMemory(ImageId id) {
if (image.depth_id) {
return false;
}
auto transfer = BuildDownload(image);
if (!transfer.valid || !image.SafeToDownload()) {
if (!image.SafeToDownload() || !CanDownload(image)) {
return false;
}
const auto range = image.info.data;
@@ -1859,7 +1732,7 @@ bool TextureCache::DownloadImageMemory(ImageId id) {
}
download.Flush(offset, range.size);
DownloadImage(image, download, offset, range.size, std::move(transfer));
DownloadImage(image, download, offset, range.size, image.info.resources.levels);
vk::BufferMemoryBarrier barrier {};
barrier.srcAccessMask = vk::AccessFlagBits::eMemoryWrite | vk::AccessFlagBits::eTransferWrite |
vk::AccessFlagBits::eShaderWrite;
+2 -8
View File
@@ -73,10 +73,6 @@ public:
void RunGarbageCollector();
private:
enum class TransferDirection { Upload, Download };
struct TextureTransfer;
struct ImageDownload;
struct MetaDataInfo {
enum class Type : uint8_t { CMask, FMask, HTile };
@@ -141,12 +137,10 @@ private:
void MaterializeColorClear(ImageId id, const ImageDesc& desc,
uint32_t metadata_base_layer);
void InitializeImage(ImageId id);
[[nodiscard]] TextureTransfer
BuildTextureTransfer(const Image& image, BindingType binding, TransferDirection direction) const;
[[nodiscard]] ImageDownload BuildDownload(const Image& image) const;
[[nodiscard]] bool CanDownload(const Image& image) const;
void UploadImage(Image& image, Buffer& source, uint64_t source_offset);
void DownloadImage(Image& image, Buffer& destination, uint64_t destination_offset,
uint64_t destination_size, ImageDownload transfer);
uint64_t destination_size, uint32_t levels);
void DownloadDepth(Image& image, Buffer& destination, uint64_t destination_offset);
void CommitGpuWrite(Image& image);
// Caller holds m_lock. Volume layer ranges select depth slices.
@@ -139,9 +139,7 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
// Nonlinear clear values are still stored as normalized components.
// Metadata clears are materialized during image discovery; render-pass loads preserve contents.
uint32_t width = 0;
uint32_t height = 0;
uint32_t pitch = 0;
uint64_t size = 0;
uint32_t height = 0;
bool tile = false;
const bool standard4 = rt.attrib3.tile_mode == Prospero::TileMode::kStandard4KB;
const bool standard64 = rt.attrib3.tile_mode == Prospero::TileMode::kStandard64KB;
@@ -164,6 +162,9 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
if (texture_tile && samples != 1) {
EXIT("texture-tiled color render targets do not support multisampling\n");
}
if (volume && !tile) {
EXIT("linear 3D render targets are unsupported\n");
}
width = rt.attrib2.width + 1;
height = rt.attrib2.height + 1;
@@ -198,80 +199,25 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
" layer=%u/%u\n",
rt.attrib3.dimension, rt.attrib3.depth, view.base_layer, view.image_layers);
}
if (samples == 1) {
pitch = TileGetTexturePitch(transfer_format, width, rt.attrib3.tile_mode);
} else {
pitch = TileGetRenderTargetPitch(width, bytes_per_element, rt.attrib.num_fragments);
}
if (pitch == 0) {
EXIT("unsupported render-target pitch: width=%u bytes=%u\n", width, bytes_per_element);
}
TileSizeOffset mip_sizes[16] {};
TilePaddedSize mip_padded[16] {};
TileSurfaceLayout volume_layout {};
uint64_t backing_size = 0;
if (volume) {
const TileSurfaceDescription description {transfer_format,
rt.attrib3.tile_mode,
TileSurfaceDimension::Dim3D,
width,
height,
depth,
levels,
1};
if (!tile || !TileGetTiledTextureLayout(description, volume_layout)) {
EXIT("unsupported 3D render-target layout: %ux%ux%u levels=%u tile=%u\n", width, height,
depth, levels, static_cast<uint32_t>(rt.attrib3.tile_mode));
}
size = volume_layout.block_slice_size;
backing_size = volume_layout.total_size;
} else {
TileSizeAlign layout {};
bool valid_layout = false;
if (samples == 1) {
TileGetTextureSize(transfer_format, width, height, levels, rt.attrib3.tile_mode,
&layout, mip_sizes, mip_padded);
valid_layout = layout.size != 0 && layout.align != 0 &&
(rt.attrib3.tile_mode != Prospero::TileMode::kRenderTarget ||
levels <= std::bit_width(std::max(width, height)));
} else {
valid_layout = TileGetRenderTargetSize(width, height, pitch, bytes_per_element,
layout, rt.attrib.num_fragments);
mip_sizes[0] = {layout.size, 0, 0, 0, 0, 0};
mip_padded[0] = {pitch, height};
}
if (!valid_layout) {
EXIT("unsupported render-target layout: %ux%u pitch=%u bytes=%u levels=%u\n", width,
height, pitch, bytes_per_element, levels);
}
size = layout.size;
}
if (size == 0 || (!volume && size > UINT64_MAX / view.image_layers)) {
EXIT("render-target memory footprint is invalid\n");
}
if (!volume) {
backing_size = size * view.image_layers;
}
if (backing_size == 0) {
EXIT("render-target backing is empty\n");
}
if (!GuestRange {rt.base.addr, backing_size}.Valid()) {
EXIT("render-target backing range is invalid\n");
}
auto& desc = r.desc;
desc.type = TextureCache::BindingType::RenderTarget;
desc.info.data = {rt.base.addr, backing_size};
desc.info.data = {rt.base.addr, 0};
desc.info.pixel_format = target_format.format;
desc.info.guest_format = target_format.guest_format;
desc.info.type = image_type;
desc.info.extent = {width, height, depth};
desc.info.resources = {levels, volume ? 1u : view.image_layers};
desc.info.pitch = pitch;
desc.info.resources = {levels, volume ? 1u : view.image_layers};
desc.info.bytes_per_block = bytes_per_element;
desc.info.samples = samples;
desc.info.tile_mode = rt.attrib3.tile_mode;
if (!volume && rt.attrib3.tile_mode == Prospero::TileMode::kRenderTarget &&
levels > std::bit_width(std::max(width, height))) {
EXIT("unsupported render-target mip count\n");
}
desc.info.UpdateSize();
if (!desc.info.data.Valid()) {
EXIT("render-target backing range is invalid\n");
}
const bool has_dcc = rt.info.dcc_compression_enable && rt.dcc_addr.addr != 0;
const bool has_cmask = !rt.info.dcc_compression_enable && rt.info.cmask_fast_clear_enable &&
rt.cmask.addr != 0 && samples == 1 &&
@@ -293,30 +239,6 @@ void RenderExecutor::ResolveRenderColorTarget(CommandBuffer& buffer, RenderColor
desc.info.metadata.clear_word = rt.clear_word0.word0;
desc.info.metadata.clear_register_valid = true;
}
for (uint32_t level = 0; level < levels; level++) {
if (volume) {
const auto& mip = volume_layout.mips[level];
desc.info.mip_layout[level] = {
mip.offset,
mip.size,
mip.padded_width,
mip.padded_height,
};
continue;
}
const auto level_offset =
mip_sizes[level].src_size != 0 ? mip_sizes[level].src_offset : mip_sizes[level].offset;
const auto level_size =
static_cast<uint64_t>(mip_sizes[level].src_size != 0 ? mip_sizes[level].src_size
: mip_sizes[level].size) *
view.image_layers;
desc.info.mip_layout[level] = {
level_offset,
level_size,
mip_padded[level].width,
mip_padded[level].height,
};
}
desc.view_info.format = target_format.format;
if (is_1d) {
desc.view_info.type =
@@ -334,6 +334,7 @@ CommandBuffer& CommandScheduler::BeginCommand() {
EXIT_IF(!m_command.IsInvalid());
m_command.m_buffer = m_command_pool.Commit();
m_command.Begin();
m_dynamic_state.Invalidate();
return m_command;
}
@@ -3,14 +3,13 @@
#include "common/common.h"
#include "common/uniqueFunction.h"
#include "graphics/host_gpu/renderer/dynamicState.h"
#include "graphics/host_gpu/renderer/masterSemaphore.h"
#include "graphics/host_gpu/renderer/render.h"
#include <condition_variable>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
@@ -50,6 +49,7 @@ public:
[[nodiscard]] uint64_t CurrentTick() const noexcept { return m_master.CurrentTick(); }
[[nodiscard]] bool IsFree(uint64_t tick);
[[nodiscard]] MasterSemaphore& GetMasterSemaphore() noexcept { return m_master; }
[[nodiscard]] DynamicState& GetDynamicState() noexcept { return m_dynamic_state; }
[[nodiscard]] RenderContext& Context() const noexcept { return m_context; }
[[nodiscard]] GraphicContext& Graphics() const noexcept { return m_graphics; }
@@ -92,6 +92,7 @@ private:
GraphicContext& m_graphics;
CommandPool m_command_pool;
CommandBuffer m_command;
DynamicState m_dynamic_state;
std::queue<PendingOperation> m_pending_operations;
std::queue<PendingOperation> m_priority_operations;
std::mutex m_operation_mutex;
@@ -239,7 +239,7 @@ static TextureCache::ImageDesc MakeDepthTargetDesc(const CommandBuffer& buffer,
desc.info.bytes_per_block = bytes;
desc.info.samples = samples;
desc.info.tile_mode = Prospero::TileMode::kDepth;
desc.info.mip_layout[0] = {0, depth_backing_size, pitch, height};
desc.info.UpdateSize();
desc.info.metadata.range =
has_htile ? GuestRange {z.htile_data_base_addr, htile_backing_size} : GuestRange {};
desc.info.metadata.kind = has_htile ? ImageMetadataKind::Htile : ImageMetadataKind::None;
@@ -0,0 +1,150 @@
#include "graphics/host_gpu/renderer/dynamicState.h"
#include "graphics/host_gpu/graphicContext.h"
namespace Libs::Graphics {
void DynamicState::Commit(const GraphicContext& graphics, vk::CommandBuffer cmdbuf) {
if (dirty_state.viewports) {
dirty_state.viewports = false;
cmdbuf.setViewportWithCount(num_viewports, viewports.data());
}
if (dirty_state.scissors) {
dirty_state.scissors = false;
cmdbuf.setScissorWithCount(num_scissors, scissors.data());
}
if (dirty_state.depth_test_enabled) {
dirty_state.depth_test_enabled = false;
cmdbuf.setDepthTestEnable(depth_test_enabled);
}
if (dirty_state.depth_write_enabled) {
dirty_state.depth_write_enabled = false;
// Note that this must be set in a command buffer even if depth test is disabled.
cmdbuf.setDepthWriteEnable(depth_write_enabled);
}
if (depth_test_enabled && dirty_state.depth_compare_op) {
dirty_state.depth_compare_op = false;
cmdbuf.setDepthCompareOp(depth_compare_op);
}
if (dirty_state.depth_bounds_test_enabled) {
dirty_state.depth_bounds_test_enabled = false;
#if !defined(__APPLE__)
cmdbuf.setDepthBoundsTestEnable(depth_bounds_test_enabled);
#endif
}
if (depth_bounds_test_enabled && dirty_state.depth_bounds) {
dirty_state.depth_bounds = false;
#if !defined(__APPLE__)
cmdbuf.setDepthBounds(depth_bounds_min, depth_bounds_max);
#endif
}
if (dirty_state.depth_bias_enabled) {
dirty_state.depth_bias_enabled = false;
cmdbuf.setDepthBiasEnable(depth_bias_enabled);
}
if (depth_bias_enabled && dirty_state.depth_bias) {
dirty_state.depth_bias = false;
cmdbuf.setDepthBias(depth_bias_constant, depth_bias_clamp, depth_bias_slope);
}
if (dirty_state.stencil_test_enabled) {
dirty_state.stencil_test_enabled = false;
cmdbuf.setStencilTestEnable(stencil_test_enabled);
}
if (stencil_test_enabled) {
if (dirty_state.stencil_front_ops && dirty_state.stencil_back_ops &&
stencil_front_ops == stencil_back_ops) {
dirty_state.stencil_front_ops = false;
dirty_state.stencil_back_ops = false;
cmdbuf.setStencilOp(vk::StencilFaceFlagBits::eFrontAndBack, stencil_front_ops.fail_op,
stencil_front_ops.pass_op, stencil_front_ops.depth_fail_op,
stencil_front_ops.compare_op);
} else {
if (dirty_state.stencil_front_ops) {
dirty_state.stencil_front_ops = false;
cmdbuf.setStencilOp(vk::StencilFaceFlagBits::eFront, stencil_front_ops.fail_op,
stencil_front_ops.pass_op, stencil_front_ops.depth_fail_op,
stencil_front_ops.compare_op);
}
if (dirty_state.stencil_back_ops) {
dirty_state.stencil_back_ops = false;
cmdbuf.setStencilOp(vk::StencilFaceFlagBits::eBack, stencil_back_ops.fail_op,
stencil_back_ops.pass_op, stencil_back_ops.depth_fail_op,
stencil_back_ops.compare_op);
}
}
if (dirty_state.stencil_front_reference && dirty_state.stencil_back_reference &&
stencil_front_reference == stencil_back_reference) {
dirty_state.stencil_front_reference = false;
dirty_state.stencil_back_reference = false;
cmdbuf.setStencilReference(vk::StencilFaceFlagBits::eFrontAndBack,
stencil_front_reference);
} else {
if (dirty_state.stencil_front_reference) {
dirty_state.stencil_front_reference = false;
cmdbuf.setStencilReference(vk::StencilFaceFlagBits::eFront,
stencil_front_reference);
}
if (dirty_state.stencil_back_reference) {
dirty_state.stencil_back_reference = false;
cmdbuf.setStencilReference(vk::StencilFaceFlagBits::eBack, stencil_back_reference);
}
}
if (dirty_state.stencil_front_write_mask && dirty_state.stencil_back_write_mask &&
stencil_front_write_mask == stencil_back_write_mask) {
dirty_state.stencil_front_write_mask = false;
dirty_state.stencil_back_write_mask = false;
cmdbuf.setStencilWriteMask(vk::StencilFaceFlagBits::eFrontAndBack,
stencil_front_write_mask);
} else {
if (dirty_state.stencil_front_write_mask) {
dirty_state.stencil_front_write_mask = false;
cmdbuf.setStencilWriteMask(vk::StencilFaceFlagBits::eFront,
stencil_front_write_mask);
}
if (dirty_state.stencil_back_write_mask) {
dirty_state.stencil_back_write_mask = false;
cmdbuf.setStencilWriteMask(vk::StencilFaceFlagBits::eBack, stencil_back_write_mask);
}
}
if (dirty_state.stencil_front_compare_mask && dirty_state.stencil_back_compare_mask &&
stencil_front_compare_mask == stencil_back_compare_mask) {
dirty_state.stencil_front_compare_mask = false;
dirty_state.stencil_back_compare_mask = false;
cmdbuf.setStencilCompareMask(vk::StencilFaceFlagBits::eFrontAndBack,
stencil_front_compare_mask);
} else {
if (dirty_state.stencil_front_compare_mask) {
dirty_state.stencil_front_compare_mask = false;
cmdbuf.setStencilCompareMask(vk::StencilFaceFlagBits::eFront,
stencil_front_compare_mask);
}
if (dirty_state.stencil_back_compare_mask) {
dirty_state.stencil_back_compare_mask = false;
cmdbuf.setStencilCompareMask(vk::StencilFaceFlagBits::eBack,
stencil_back_compare_mask);
}
}
}
if (dirty_state.blend_constants) {
dirty_state.blend_constants = false;
cmdbuf.setBlendConstants(blend_constants.data());
}
if (dirty_state.color_write_enables) {
dirty_state.color_write_enables = false;
#if !defined(__APPLE__)
if (num_color_write_enables != 0) {
cmdbuf.setColorWriteEnableEXT(num_color_write_enables, color_write_enables.data());
}
#endif
}
if (dirty_state.line_width) {
dirty_state.line_width = false;
cmdbuf.setLineWidth(line_width);
}
if (dirty_state.feedback_loop_aspects && graphics.attachment_feedback_loop_enabled) {
dirty_state.feedback_loop_aspects = false;
cmdbuf.setAttachmentFeedbackLoopEnableEXT(feedback_loop_aspects);
}
}
} // namespace Libs::Graphics
@@ -0,0 +1,258 @@
#ifndef EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_DYNAMICSTATE_H_
#define EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_DYNAMICSTATE_H_
#include "common/assert.h"
#include "graphics/host_gpu/renderer/renderTarget.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstring>
#include <span>
namespace Libs::Graphics {
struct GraphicContext;
struct StencilOps {
vk::StencilOp fail_op {};
vk::StencilOp pass_op {};
vk::StencilOp depth_fail_op {};
vk::CompareOp compare_op {};
bool operator==(const StencilOps& other) const {
return fail_op == other.fail_op && pass_op == other.pass_op &&
depth_fail_op == other.depth_fail_op && compare_op == other.compare_op;
}
};
struct DynamicState {
struct {
bool viewports : 1;
bool scissors : 1;
bool depth_test_enabled : 1;
bool depth_write_enabled : 1;
bool depth_compare_op : 1;
bool depth_bounds_test_enabled : 1;
bool depth_bounds : 1;
bool depth_bias_enabled : 1;
bool depth_bias : 1;
bool stencil_test_enabled : 1;
bool stencil_front_ops : 1;
bool stencil_front_reference : 1;
bool stencil_front_write_mask : 1;
bool stencil_front_compare_mask : 1;
bool stencil_back_ops : 1;
bool stencil_back_reference : 1;
bool stencil_back_write_mask : 1;
bool stencil_back_compare_mask : 1;
bool blend_constants : 1;
bool color_write_enables : 1;
bool line_width : 1;
bool feedback_loop_aspects : 1;
} dirty_state {};
std::array<vk::Viewport, 16> viewports {};
std::array<vk::Rect2D, 16> scissors {};
uint32_t num_viewports {};
uint32_t num_scissors {};
bool depth_test_enabled {};
bool depth_write_enabled {};
vk::CompareOp depth_compare_op {};
bool depth_bounds_test_enabled {};
float depth_bounds_min {};
float depth_bounds_max {};
bool depth_bias_enabled {};
float depth_bias_constant {};
float depth_bias_clamp {};
float depth_bias_slope {};
bool stencil_test_enabled {};
StencilOps stencil_front_ops {};
uint32_t stencil_front_reference {};
uint32_t stencil_front_write_mask {};
uint32_t stencil_front_compare_mask {};
StencilOps stencil_back_ops {};
uint32_t stencil_back_reference {};
uint32_t stencil_back_write_mask {};
uint32_t stencil_back_compare_mask {};
std::array<float, 4> blend_constants {};
std::array<vk::Bool32, RENDER_COLOR_ATTACHMENTS_MAX> color_write_enables {};
uint32_t num_color_write_enables {};
float line_width {};
vk::ImageAspectFlags feedback_loop_aspects {};
/// Commits the dynamic state to the provided command buffer.
void Commit(const GraphicContext& graphics, vk::CommandBuffer cmdbuf);
/// Invalidates all dynamic state to be flushed into the next command buffer.
void Invalidate() { std::memset(&dirty_state, 0xFF, sizeof(dirty_state)); }
void SetViewports(std::span<const vk::Viewport> viewports_) {
EXIT_IF(viewports_.size() > viewports.size());
if (!std::ranges::equal(std::span(viewports).first(num_viewports), viewports_)) {
std::ranges::copy(viewports_, viewports.begin());
num_viewports = static_cast<uint32_t>(viewports_.size());
dirty_state.viewports = true;
}
}
void SetScissors(std::span<const vk::Rect2D> scissors_) {
EXIT_IF(scissors_.size() > scissors.size());
if (!std::ranges::equal(std::span(scissors).first(num_scissors), scissors_)) {
std::ranges::copy(scissors_, scissors.begin());
num_scissors = static_cast<uint32_t>(scissors_.size());
dirty_state.scissors = true;
}
}
void SetDepthTestEnabled(const bool enabled) {
if (depth_test_enabled != enabled) {
depth_test_enabled = enabled;
dirty_state.depth_test_enabled = true;
}
}
void SetDepthWriteEnabled(const bool enabled) {
if (depth_write_enabled != enabled) {
depth_write_enabled = enabled;
dirty_state.depth_write_enabled = true;
}
}
void SetDepthCompareOp(const vk::CompareOp compare_op) {
if (depth_compare_op != compare_op) {
depth_compare_op = compare_op;
dirty_state.depth_compare_op = true;
}
}
void SetDepthBoundsTestEnabled(const bool enabled) {
if (depth_bounds_test_enabled != enabled) {
depth_bounds_test_enabled = enabled;
dirty_state.depth_bounds_test_enabled = true;
}
}
void SetDepthBounds(const float min, const float max) {
if (depth_bounds_min != min || depth_bounds_max != max) {
depth_bounds_min = min;
depth_bounds_max = max;
dirty_state.depth_bounds = true;
}
}
void SetDepthBiasEnabled(const bool enabled) {
if (depth_bias_enabled != enabled) {
depth_bias_enabled = enabled;
dirty_state.depth_bias_enabled = true;
}
}
void SetDepthBias(const float constant, const float clamp, const float slope) {
if (depth_bias_constant != constant || depth_bias_clamp != clamp ||
depth_bias_slope != slope) {
depth_bias_constant = constant;
depth_bias_clamp = clamp;
depth_bias_slope = slope;
dirty_state.depth_bias = true;
}
}
void SetStencilTestEnabled(const bool enabled) {
if (stencil_test_enabled != enabled) {
stencil_test_enabled = enabled;
dirty_state.stencil_test_enabled = true;
}
}
void SetStencilOps(const StencilOps& front_ops, const StencilOps& back_ops) {
if (stencil_front_ops != front_ops) {
stencil_front_ops = front_ops;
dirty_state.stencil_front_ops = true;
}
if (stencil_back_ops != back_ops) {
stencil_back_ops = back_ops;
dirty_state.stencil_back_ops = true;
}
}
void SetStencilReferences(const uint32_t front_reference, const uint32_t back_reference) {
if (stencil_front_reference != front_reference) {
stencil_front_reference = front_reference;
dirty_state.stencil_front_reference = true;
}
if (stencil_back_reference != back_reference) {
stencil_back_reference = back_reference;
dirty_state.stencil_back_reference = true;
}
}
void SetStencilWriteMasks(const uint32_t front_write_mask, const uint32_t back_write_mask) {
if (stencil_front_write_mask != front_write_mask) {
stencil_front_write_mask = front_write_mask;
dirty_state.stencil_front_write_mask = true;
}
if (stencil_back_write_mask != back_write_mask) {
stencil_back_write_mask = back_write_mask;
dirty_state.stencil_back_write_mask = true;
}
}
void SetStencilCompareMasks(const uint32_t front_compare_mask,
const uint32_t back_compare_mask) {
if (stencil_front_compare_mask != front_compare_mask) {
stencil_front_compare_mask = front_compare_mask;
dirty_state.stencil_front_compare_mask = true;
}
if (stencil_back_compare_mask != back_compare_mask) {
stencil_back_compare_mask = back_compare_mask;
dirty_state.stencil_back_compare_mask = true;
}
}
void SetBlendConstants(const std::array<float, 4> blend_constants_) {
if (blend_constants != blend_constants_) {
blend_constants = blend_constants_;
dirty_state.blend_constants = true;
}
}
void SetColorWriteEnables(std::span<const vk::Bool32> enables) {
EXIT_IF(enables.size() > color_write_enables.size());
if (!std::ranges::equal(std::span(color_write_enables).first(num_color_write_enables),
enables)) {
std::ranges::copy(enables, color_write_enables.begin());
num_color_write_enables = static_cast<uint32_t>(enables.size());
dirty_state.color_write_enables = true;
}
}
void SetLineWidth(const float width) {
if (line_width != width) {
line_width = width;
dirty_state.line_width = true;
}
}
void SetAttachmentFeedbackLoopAspects(const vk::ImageAspectFlags aspects) {
if (feedback_loop_aspects != aspects) {
feedback_loop_aspects = aspects;
dirty_state.feedback_loop_aspects = true;
}
}
};
} // namespace Libs::Graphics
#endif // EMULATOR_SRC_GRAPHICS_HOST_GPU_RENDERER_DYNAMICSTATE_H_
@@ -193,6 +193,8 @@ void BlitHelper::ReinterpretColorAsMsDepth(Image& source, Image& destination) {
command.setScissor(0, 1, &scissor);
command.draw(3, 1, 0, 0);
command.endRendering();
// The helper pipeline has static state that invalidates the guest draw's dynamic state.
m_scheduler.GetDynamicState().Invalidate();
}
} // namespace Libs::Graphics
@@ -0,0 +1,246 @@
#include "graphics/host_gpu/renderer/image/imageInfo.h"
#include "common/alignment.h"
namespace Libs::Graphics {
namespace {
uint64_t Multiply(uint64_t left, uint64_t right) {
EXIT_IF(right != 0 && left > UINT64_MAX / right);
return left * right;
}
uint64_t Add(uint64_t left, uint64_t right) {
EXIT_IF(right > UINT64_MAX - left);
return left + right;
}
bool IsDepthPlane(const ImageInfo& info) {
return (info.IsDepth() || info.pixel_format == vk::Format::eS8Uint) &&
(info.tile_mode == Prospero::TileMode::kDepth ||
info.tile_mode == Prospero::TileMode::kLinear);
}
} // namespace
vk::Extent3D ImageInfo::MipExtent(uint32_t level) const {
EXIT_IF(level >= resources.levels || level >= mip_layout.size());
return {std::max(extent.width >> level, 1u), std::max(extent.height >> level, 1u),
IsVolume() ? std::max(extent.depth >> level, 1u) : resources.layers};
}
void ImageInfo::UpdateSize() {
EXIT_IF(guest_format == Prospero::BufferFormat::kInvalid || extent.width == 0 ||
extent.height == 0 || extent.depth == 0 || resources.levels == 0 ||
resources.levels > mip_layout.size() || resources.layers == 0 || samples == 0 ||
!std::has_single_bit(samples));
TileTextureElementLayout element {};
EXIT_NOT_IMPLEMENTED(!TileGetTextureElementLayout(guest_format, element));
if (bytes_per_block == 0) {
bytes_per_block = element.bytes;
}
EXIT_IF(bytes_per_block != element.bytes);
mip_layout = {};
tiling = {};
tiling.texel_width = element.texel_width;
tiling.texel_height = element.texel_height;
tiling.block.bytes_per_element = element.bytes;
tiled_slice_stride = 0;
linear_slice_stride = 0;
first_tail_level = 16;
if (samples > 1) {
EXIT_NOT_IMPLEMENTED(IsVolume() || resources.levels != 1 ||
(tile_mode != Prospero::TileMode::kDepth &&
tile_mode != Prospero::TileMode::kRenderTarget));
const auto fragments = std::countr_zero(samples);
if (pitch == 0) {
pitch = tile_mode == Prospero::TileMode::kDepth
? TileGetDepthPitch(extent.width, bytes_per_block, fragments)
: TileGetRenderTargetPitch(extent.width, bytes_per_block, fragments);
}
EXIT_NOT_IMPLEMENTED(!TileGetTextureBlockLayout(guest_format, tile_mode, false, tiling));
if (data.size == 0) {
TileSizeAlign slice {};
EXIT_NOT_IMPLEMENTED(!TileGetRenderTargetSize(extent.width, extent.height, pitch,
bytes_per_block, slice, fragments));
data.size = Multiply(slice.size, resources.layers);
}
EXIT_IF(data.size % resources.layers != 0);
tiled_slice_stride = data.size / resources.layers;
mip_layout[0] = {0, data.size, pitch, extent.height};
return;
}
if (pitch == 0) {
pitch = TileGetTexturePitch(guest_format, extent.width, tile_mode);
}
EXIT_IF(pitch == 0);
if (IsDepthPlane(*this) && resources.levels == 1) {
EXIT_NOT_IMPLEMENTED(IsVolume());
if (IsTiled()) {
EXIT_NOT_IMPLEMENTED(
!TileGetTextureBlockLayout(guest_format, tile_mode, false, tiling));
}
if (data.size == 0) {
TileSizeAlign slice {};
TileGetTextureSize(guest_format, extent.width, extent.height, 1, tile_mode, &slice,
nullptr, nullptr);
data.size = Multiply(slice.size, resources.layers);
}
EXIT_IF(data.size % resources.layers != 0);
const auto slice = data.size / resources.layers;
EXIT_IF(Multiply(Multiply(pitch, extent.height), bytes_per_block) > slice);
linear_slice_stride = slice;
tiled_slice_stride = IsTiled() ? slice : 0;
mip_layout[0] = {0, data.size, pitch, extent.height, 0, slice, pitch};
return;
}
if (!IsTiled()) {
TileSizeAlign total {};
TileSizeOffset offsets[16] {};
TilePaddedSize padded[16] {};
TileGetTextureSize(guest_format, extent.width, extent.height, resources.levels, tile_mode,
&total, offsets, padded);
if (data.size == 0) {
data.size = Multiply(total.size, TransferLayers());
}
for (uint32_t level = 0; level < resources.levels; ++level) {
auto& mip = mip_layout[level];
mip.offset = offsets[level].offset;
mip.size = Multiply(offsets[level].size, MipExtent(level).depth);
mip.pitch = padded[level].width / element.texel_width;
mip.height = padded[level].height / element.texel_height;
mip.linear_offset = offsets[level].offset;
mip.linear_size = offsets[level].size;
mip.linear_pitch = mip.pitch;
linear_slice_stride =
std::max(linear_slice_stride, Add(mip.linear_offset, mip.linear_size));
}
if (TransferLayers() > 1 && data.size % TransferLayers() == 0) {
linear_slice_stride = std::max(linear_slice_stride, data.size / TransferLayers());
}
} else {
TileSurfaceLayout surface {};
const TileSurfaceDescription description {guest_format,
tile_mode,
IsVolume() ? TileSurfaceDimension::Dim3D
: TileSurfaceDimension::Dim2D,
extent.width,
extent.height,
IsVolume() ? extent.depth : 1u,
resources.levels,
resources.layers};
EXIT_NOT_IMPLEMENTED(!TileGetTiledTextureLayout(description, surface));
tiling = surface.texture;
first_tail_level = surface.first_tail_level;
tiled_slice_stride = surface.block_slice_size;
if (data.size == 0) {
data.size = surface.total_size;
}
if (!IsVolume() && resources.layers > 1 && data.size % resources.layers == 0) {
tiled_slice_stride = std::max(tiled_slice_stride, data.size / resources.layers);
}
uint64_t linear_offset = 0;
uint32_t mip_pitch = IsVolume() ? extent.width : pitch;
for (uint32_t level = 0; level < resources.levels; ++level) {
const auto& source = surface.mips[level];
auto& mip = mip_layout[level];
const auto active = MipExtent(level);
mip.offset = source.offset;
mip.size = Multiply(source.size, IsVolume() ? 1u : resources.layers);
mip.pitch = source.padded_width;
mip.height = source.padded_height;
mip.tail_x = source.tail_x;
mip.tail_y = source.tail_y;
mip.linear_pitch =
Common::AlignUp(mip_pitch, element.texel_width) / element.texel_width;
const auto rows =
Common::AlignUp(active.height, element.texel_height) / element.texel_height;
mip.linear_size = Multiply(Multiply(mip.linear_pitch, rows), element.bytes);
const auto alignment = std::max(element.bytes, 4u);
EXIT_IF(linear_offset > UINT64_MAX - (alignment - 1u));
mip.linear_offset = Common::AlignUp<uint64_t>(linear_offset, alignment);
linear_offset = Add(mip.linear_offset, Multiply(mip.linear_size, active.depth));
mip_pitch = std::max(mip_pitch >> 1u, 1u);
}
}
}
uint64_t ImageInfo::LinearSize(uint32_t levels) const {
levels = levels == 0 ? resources.levels : levels;
EXIT_IF(levels == 0 || levels > resources.levels || levels > mip_layout.size());
if (linear_slice_stride == 0) {
const auto& mip = mip_layout[levels - 1u];
return Add(mip.linear_offset, Multiply(mip.linear_size, MipExtent(levels - 1u).depth));
}
uint64_t size = 0;
for (uint32_t level = 0; level < levels; ++level) {
const auto& mip = mip_layout[level];
const auto slices = MipExtent(level).depth;
const auto stride = linear_slice_stride != 0 ? linear_slice_stride : mip.linear_size;
size = std::max(
size, Add(Add(mip.linear_offset, Multiply(slices - 1u, stride)), mip.linear_size));
}
return size;
}
std::vector<vk::BufferImageCopy> ImageInfo::BufferCopies(uint64_t buffer_offset,
uint32_t levels) const {
levels = levels == 0 ? resources.levels : levels;
EXIT_IF(levels > resources.levels || levels > mip_layout.size() || tiling.texel_width == 0 ||
tiling.texel_height == 0);
const auto aspect = IsDepth() ? vk::ImageAspectFlagBits::eDepth
: pixel_format == vk::Format::eS8Uint ? vk::ImageAspectFlagBits::eStencil
: vk::ImageAspectFlagBits::eColor;
std::vector<vk::BufferImageCopy> copies;
std::array<uint32_t, 16> image_heights {};
uint64_t count = 0;
for (uint32_t level = 0; level < levels; ++level) {
const auto& mip = mip_layout[level];
const auto active = MipExtent(level);
if (linear_slice_stride == 0) {
image_heights[level] = Common::AlignUp(active.height, tiling.texel_height);
} else {
const auto row_bytes = Multiply(mip.linear_pitch, tiling.block.bytes_per_element);
EXIT_IF(row_bytes == 0);
const auto rows = linear_slice_stride / row_bytes;
if (linear_slice_stride % row_bytes == 0 &&
rows >=
(uint64_t {active.height} + tiling.texel_height - 1u) / tiling.texel_height &&
rows <= UINT32_MAX / tiling.texel_height) {
image_heights[level] = static_cast<uint32_t>(rows) * tiling.texel_height;
}
}
count += image_heights[level] != 0 ? 1u : active.depth;
}
copies.reserve(static_cast<size_t>(count));
for (uint32_t level = 0; level < levels; ++level) {
const auto& mip = mip_layout[level];
const auto active = MipExtent(level);
vk::BufferImageCopy copy {};
copy.bufferOffset = Add(buffer_offset, mip.linear_offset);
copy.bufferRowLength = mip.linear_pitch * tiling.texel_width;
copy.bufferImageHeight = image_heights[level] != 0 ? image_heights[level]
: !IsTiled() ? mip.height * tiling.texel_height
: Common::AlignUp(active.height, tiling.texel_height);
copy.imageSubresource = {aspect, level, 0, 1};
copy.imageExtent = {active.width, active.height, 1};
if (image_heights[level] == 0) {
for (uint32_t slice = 0; slice < active.depth; ++slice) {
copy.bufferOffset = Add(Add(buffer_offset, mip.linear_offset),
Multiply(slice, linear_slice_stride));
copy.imageSubresource.baseArrayLayer = IsVolume() ? 0u : slice;
copy.imageOffset.z = IsVolume() ? static_cast<int32_t>(slice) : 0;
copies.push_back(copy);
}
} else {
copy.imageSubresource.layerCount = IsVolume() ? 1u : active.depth;
copy.imageExtent.depth = IsVolume() ? active.depth : 1u;
copies.push_back(copy);
}
}
return copies;
}
} // namespace Libs::Graphics
@@ -4,6 +4,7 @@
#include "common/assert.h"
#include "graphics/guest_gpu/gpu_defs.h"
#include "graphics/guest_gpu/gpu_format.h"
#include "graphics/guest_gpu/tile.h"
#include "graphics/host_gpu/regionDefinitions.h"
#include "graphics/host_gpu/vulkanCommon.h"
@@ -12,6 +13,7 @@
#include <bit>
#include <cmath>
#include <cstdint>
#include <vector>
namespace Libs::Graphics {
@@ -47,11 +49,19 @@ struct ImageSubresourceRange {
};
struct ImageMipInfo {
// Guest mip chains run backwards within each layer or volume block-slice.
// Tail levels share an offset and use tail_x/tail_y within that block.
uint64_t offset = 0;
uint64_t size = 0;
// Padded dimensions in storage elements (compressed blocks for BC formats).
uint32_t pitch = 0;
uint32_t height = 0;
uint64_t linear_offset = 0;
uint64_t linear_size = 0; // Per transfer slice.
uint32_t linear_pitch = 0; // Storage elements.
uint32_t tail_x = 0;
uint32_t tail_y = 0;
uint32_t surface_z = 0;
auto operator<=>(const ImageMipInfo&) const = default;
};
@@ -71,6 +81,17 @@ struct ImageInfo {
Prospero::TileMode tile_mode = Prospero::TileMode::kLinear;
bool bgra16 = false;
std::array<ImageMipInfo, 16> mip_layout {};
TileTextureBlockLayout tiling;
uint64_t tiled_slice_stride = 0;
// Nonzero for guest-linear and depth-plane transfers; tiled colors are mip-major.
uint64_t linear_slice_stride = 0;
uint32_t first_tail_level = 16;
void UpdateSize();
[[nodiscard]] uint64_t LinearSize(uint32_t levels = 0) const;
[[nodiscard]] vk::Extent3D MipExtent(uint32_t level) const;
[[nodiscard]] std::vector<vk::BufferImageCopy> BufferCopies(uint64_t buffer_offset = 0,
uint32_t levels = 0) const;
[[nodiscard]] ColorTransform GetColorTransform() const noexcept {
if (bgra16) return ColorTransform::SwapBgra16;
@@ -1,16 +1,13 @@
#include "graphics/host_gpu/renderer/image/textureCommon.h"
#include "common/alignment.h"
#include "common/assert.h"
#include "graphics/guest_gpu/gpu_defs.h"
#include "graphics/guest_gpu/gpu_format.h"
#include "graphics/host_gpu/renderer/image/tiler.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include "graphics/shader/shader.h"
#include <algorithm>
#include <array>
#include <cinttypes>
namespace Libs::Graphics {
namespace {
@@ -60,68 +57,6 @@ HostFormatInfo ResolveHostFormat(Prospero::BufferFormat guest_format,
}
}
uint32_t GetTextureLevelDepth(uint32_t depth, uint32_t level, bool volume_texture) {
return volume_texture ? std::max(depth >> level, 1u) : depth;
}
size_t GetTextureRegionCount(uint32_t depth, uint32_t levels, bool volume_texture) {
size_t count = 0;
for (uint32_t level = 0; level < levels; level++) {
count += GetTextureLevelDepth(depth, level, volume_texture);
}
return count;
}
uint64_t CalcTextureSliceStride(const TextureUploadMipLayout* mips, uint32_t levels,
uint64_t total_size, uint32_t depth) {
uint64_t stride = 0;
for (uint32_t mip_level = 0; mip_level < levels; ++mip_level) {
stride = std::max(stride, mips[mip_level].offset + mips[mip_level].size);
}
if (depth > 1 && total_size != 0 && total_size % depth == 0) {
stride = std::max(stride, total_size / depth);
}
return stride;
}
uint64_t CalcLinearUploadLevelSize(const TileTextureElementLayout& element, uint32_t pitch,
uint32_t height) {
const uint32_t element_columns =
std::max((pitch + element.texel_width - 1u) / element.texel_width, 1u);
const uint32_t element_rows =
std::max((height + element.texel_height - 1u) / element.texel_height, 1u);
return static_cast<uint64_t>(element_columns) * element_rows * element.bytes;
}
uint64_t SetLinearUploadLevels(TextureUploadMipLayout* mips,
const TileTextureElementLayout& element, uint32_t height,
uint32_t levels, uint32_t base_pitch) {
uint64_t offset = 0;
auto mip_pitch = base_pitch;
auto mip_height = height;
for (uint32_t mip_level = 0; mip_level < levels; ++mip_level) {
const auto size = CalcLinearUploadLevelSize(element, mip_pitch, mip_height);
mips[mip_level] = {offset, size};
offset += size;
if (mip_pitch > 1) {
mip_pitch /= 2;
}
if (mip_height > 1) {
mip_height /= 2;
}
}
return offset;
}
bool FitsBufferRange(uint64_t offset, uint64_t size, uint64_t capacity) {
return offset <= capacity && size <= capacity - offset;
}
} // namespace
RenderTargetFormatInfo TextureGetRenderTargetFormat(Prospero::ChannelLayout layout,
@@ -176,203 +111,4 @@ SurfaceFormatInfo TextureGetSurfaceFormatInfo(Prospero::BufferFormat format) {
EXIT("unknown format: fmt = %u\n", static_cast<uint32_t>(format));
}
TextureUploadLayout TextureCalcUploadLayout(Prospero::BufferFormat format, uint32_t width,
uint32_t height, uint32_t levels, uint32_t depth,
Prospero::TileMode tile_mode, uint64_t upload_size,
bool volume_texture, const char* owner) {
TextureUploadLayout layout {};
layout.surface.description = {
format,
tile_mode,
volume_texture ? TileSurfaceDimension::Dim3D : TileSurfaceDimension::Dim2D,
width,
height,
volume_texture ? depth : 1u,
levels,
volume_texture ? 1u : depth,
};
const auto& description = layout.surface.description;
TileTextureElementLayout element {};
if (format == Prospero::BufferFormat::kInvalid) {
EXIT("%s: legacy texture upload format unsupported: fmt=0 tile=%u size=%" PRIu64
" extent=%ux%u levels=%u\n",
owner, static_cast<uint32_t>(description.tile_mode), upload_size, width, height,
levels);
}
if (tile_mode == Prospero::TileMode::kLinear) {
if (!TileGetTextureElementLayout(format, element)) {
EXIT("%s: unsupported linear texture format: fmt=%u\n", owner,
static_cast<uint32_t>(format));
}
} else {
if (!TileGetTiledTextureLayout(description, layout.surface)) {
EXIT("%s: unsupported typed tiled upload: fmt=%u tile=%u "
"size=%" PRIu64 " extent=%ux%u levels=%u\n",
owner, static_cast<uint32_t>(format), static_cast<uint32_t>(tile_mode),
upload_size, width, height, levels);
}
element = {layout.surface.texture.block.bytes_per_element,
layout.surface.texture.texel_width, layout.surface.texture.texel_height};
}
layout.pitch = TileGetTexturePitch(format, width, description.tile_mode);
if (tile_mode == Prospero::TileMode::kLinear) {
TileSizeOffset level_sizes[16] {};
TilePaddedSize padded_sizes[16] {};
TileGetTextureSize(format, width, height, levels, description.tile_mode, nullptr,
level_sizes, padded_sizes);
for (uint32_t level = 0; level < levels; ++level) {
layout.mips[level] = {level_sizes[level].offset, level_sizes[level].size,
padded_sizes[level].width, padded_sizes[level].height};
}
} else if (volume_texture) {
layout.slice_stride = SetLinearUploadLevels(layout.mips, element, height, levels, width);
} else {
layout.source_slice_stride = layout.surface.block_slice_size;
if (depth > 1 && upload_size != 0 && upload_size % depth == 0) {
layout.source_slice_stride = std::max(layout.source_slice_stride, upload_size / depth);
}
SetLinearUploadLevels(layout.mips, element, height, levels, layout.pitch);
}
if (!volume_texture || tile_mode == Prospero::TileMode::kLinear) {
layout.slice_stride = CalcTextureSliceStride(layout.mips, levels, upload_size, depth);
}
return layout;
}
std::vector<vk::BufferImageCopy> TextureBuildImageCopies(const TextureUploadLayout& layout) {
const auto& description = layout.surface.description;
const bool volume_texture = description.dimension == TileSurfaceDimension::Dim3D;
const bool linear_texture = description.tile_mode == Prospero::TileMode::kLinear;
const auto depth = volume_texture ? description.depth : description.layers;
uint32_t mip_width = description.width;
uint32_t mip_height = description.height;
uint32_t mip_pitch = volume_texture && !linear_texture ? description.width : layout.pitch;
std::vector<vk::BufferImageCopy> regions;
regions.reserve(GetTextureRegionCount(depth, description.levels, volume_texture));
for (uint32_t mip_level = 0; mip_level < description.levels; ++mip_level) {
EXIT_NOT_IMPLEMENTED(layout.mips[mip_level].size == 0);
const auto mip_depth = GetTextureLevelDepth(depth, mip_level, volume_texture);
vk::BufferImageCopy region {};
region.imageSubresource = {vk::ImageAspectFlagBits::eColor, mip_level, 0, 1};
region.imageExtent = {mip_width, mip_height, 1};
if (linear_texture) {
region.bufferRowLength = layout.mips[mip_level].row_length;
region.bufferImageHeight = layout.mips[mip_level].image_height;
} else {
const auto texel_width = layout.surface.texture.texel_width;
const auto aligned_pitch = Common::AlignUp(mip_pitch, texel_width);
region.bufferRowLength =
aligned_pitch > Common::AlignUp(mip_width, texel_width) ? aligned_pitch : 0;
}
for (uint32_t slice_index = 0; slice_index < mip_depth; ++slice_index) {
region.bufferOffset = layout.mips[mip_level].offset + slice_index * layout.slice_stride;
region.imageSubresource.baseArrayLayer = volume_texture ? 0u : slice_index;
region.imageOffset.z = volume_texture ? static_cast<int>(slice_index) : 0;
regions.push_back(region);
}
if (mip_width > 1) {
mip_width /= 2;
}
if (mip_height > 1) {
mip_height /= 2;
}
if (mip_pitch > 1) {
mip_pitch /= 2;
}
}
return regions;
}
bool TextureBuildGpuTileInfos(uint64_t tiled_size, const std::vector<vk::BufferImageCopy>& regions,
const TextureUploadLayout& layout, uint32_t levels,
std::vector<GpuTileInfo>& out_tile_infos) {
const auto& description = layout.surface.description;
const bool volume_texture = description.dimension == TileSurfaceDimension::Dim3D;
const auto depth = volume_texture ? description.depth : description.layers;
if (tiled_size == 0 || levels == 0 || levels > 16 || depth == 0 ||
regions.size() != GetTextureRegionCount(depth, levels, volume_texture) ||
Prospero::IsFmaskTextureFormat(description.format)) {
return false;
}
const auto& surface = layout.surface;
const auto& texture = surface.texture;
const auto& block = texture.block;
if (surface.description.levels < levels || block.bytes_per_element == 0 ||
block.family == TileBlockFamily::Count) {
return false;
}
std::vector<GpuTileInfo> tile_infos;
tile_infos.reserve(regions.size());
const auto slices_per_copy = volume_texture ? block.block_depth : 1u;
size_t region_base = 0;
for (uint32_t mip_level = 0; mip_level < levels; ++mip_level) {
const auto& mip = surface.mips[mip_level];
const auto mip_depth = GetTextureLevelDepth(depth, mip_level, volume_texture);
for (uint32_t slice_index = 0; slice_index < mip_depth; slice_index += slices_per_copy) {
const auto& region = regions[region_base + slice_index];
GpuTileInfo tile_info {};
tile_info.family = block.family;
tile_info.bytes_per_element = block.bytes_per_element;
tile_info.linear_offset = region.bufferOffset;
tile_info.linear_size = layout.mips[mip_level].size;
tile_info.tiled_size = mip.size;
if (volume_texture) {
tile_info.depth = std::min(slices_per_copy, mip_depth - slice_index);
tile_info.linear_slice_stride = layout.slice_stride;
tile_info.linear_size += (tile_info.depth - 1u) * layout.slice_stride;
tile_info.tiled_offset =
mip.offset +
static_cast<uint64_t>(slice_index / slices_per_copy) * surface.block_slice_size;
tile_info.surface_z =
block.block_depth == 1 ? static_cast<uint32_t>(region.imageOffset.z) : 0;
} else {
const auto source_stride = layout.source_slice_stride != 0
? layout.source_slice_stride
: surface.block_slice_size;
if (slice_index > (UINT64_MAX - mip.offset) / source_stride) {
return false;
}
tile_info.tiled_offset =
mip.offset + static_cast<uint64_t>(slice_index) * source_stride;
tile_info.surface_z = block.family == TileBlockFamily::RenderTarget64KB ||
block.family == TileBlockFamily::Depth64KB
? region.imageSubresource.baseArrayLayer
: 0;
}
if (!FitsBufferRange(tile_info.linear_offset, tile_info.linear_size, UINT64_MAX) ||
!FitsBufferRange(tile_info.tiled_offset, tile_info.tiled_size, tiled_size)) {
return false;
}
const auto row_length =
region.bufferRowLength != 0 ? region.bufferRowLength : region.imageExtent.width;
const auto image_height = region.bufferImageHeight != 0 ? region.bufferImageHeight
: region.imageExtent.height;
tile_info.width = std::max(
(region.imageExtent.width + texture.texel_width - 1u) / texture.texel_width, 1u);
tile_info.height =
std::max((image_height + texture.texel_height - 1u) / texture.texel_height, 1u);
tile_info.pitch =
std::max((row_length + texture.texel_width - 1u) / texture.texel_width, 1u);
tile_info.tail = mip_level >= surface.first_tail_level;
tile_info.tail_x = tile_info.tail ? mip.tail_x : 0;
tile_info.tail_y = tile_info.tail ? mip.tail_y : 0;
tile_info.tiled_width = mip.padded_width;
tile_info.tiled_height = mip.padded_height;
tile_infos.push_back(tile_info);
}
region_base += mip_depth;
}
out_tile_infos = std::move(tile_infos);
return true;
}
} // namespace Libs::Graphics
@@ -2,15 +2,10 @@
#define EMULATOR_INCLUDE_EMULATOR_GRAPHICS_HOST_GPU_RENDERER_IMAGE_TEXTURECOMMON_H_
#include "graphics/guest_gpu/gpu_defs.h"
#include "graphics/guest_gpu/tile.h"
#include "graphics/host_gpu/vulkanCommon.h"
#include <vector>
namespace Libs::Graphics {
struct GpuTileInfo;
struct RenderTargetFormatInfo {
vk::Format format = vk::Format::eUndefined;
uint32_t bytes_per_element = 0;
@@ -24,35 +19,12 @@ struct SurfaceFormatInfo {
Prospero::ColorComponentMapping host_to_storage;
};
struct TextureUploadMipLayout {
uint64_t offset = 0;
uint64_t size = 0;
uint32_t row_length = 0;
uint32_t image_height = 0;
};
struct TextureUploadLayout {
uint32_t pitch = 0;
uint64_t slice_stride = 0;
uint64_t source_slice_stride = 0;
TileSurfaceLayout surface;
TextureUploadMipLayout mips[16] = {};
};
vk::ComponentMapping TextureGetComponentMapping(uint32_t swizzle,
Prospero::ColorComponentMapping host_to_storage);
SurfaceFormatInfo TextureGetSurfaceFormatInfo(Prospero::BufferFormat format);
RenderTargetFormatInfo TextureGetRenderTargetFormat(Prospero::ChannelLayout layout,
Prospero::ChannelType type,
Prospero::ChannelOrder order);
TextureUploadLayout TextureCalcUploadLayout(Prospero::BufferFormat format, uint32_t width,
uint32_t height, uint32_t levels, uint32_t depth,
Prospero::TileMode tile_mode, uint64_t upload_size,
bool volume_texture, const char* owner);
std::vector<vk::BufferImageCopy> TextureBuildImageCopies(const TextureUploadLayout& layout);
bool TextureBuildGpuTileInfos(uint64_t tiled_size, const std::vector<vk::BufferImageCopy>& regions,
const TextureUploadLayout& layout, uint32_t levels,
std::vector<GpuTileInfo>& out_tile_infos);
} // namespace Libs::Graphics
+155 -152
View File
@@ -31,7 +31,12 @@ TileManager::TileManager(GraphicContext& graphics, CommandScheduler& scheduler,
StreamBuffer& stream_buffer)
: m_graphics(graphics), m_scheduler(scheduler), m_stream_buffer(stream_buffer) {
static_assert(FamilyCount == 9);
static_assert(sizeof(Push) == 60);
static_assert(sizeof(Mip) == 48);
static_assert(offsetof(TilingParams, mips) == 16);
static_assert(sizeof(TilingParams) == 784);
static_assert(sizeof(TilingParams) <= UINT16_MAX && PipelineCount <= UINT16_MAX);
static_assert(sizeof(Tiling) == 16);
static_assert(sizeof(Conversion) == 32);
std::array<vk::DescriptorSetLayoutBinding, 3> bindings {};
for (uint32_t index = 0; index < 2; index++) {
bindings[index] = {index, vk::DescriptorType::eStorageBuffer, 1,
@@ -48,7 +53,8 @@ TileManager::TileManager(GraphicContext& graphics, CommandScheduler& scheduler,
&m_descriptor_layout),
"create TileManager descriptor layout");
const vk::PushConstantRange push_range {vk::ShaderStageFlagBits::eCompute, 0, sizeof(Push)};
const vk::PushConstantRange push_range {vk::ShaderStageFlagBits::eCompute, 0,
sizeof(Conversion)};
vk::PipelineLayoutCreateInfo layout_info {};
layout_info.setLayoutCount = 1;
layout_info.pSetLayouts = &m_descriptor_layout;
@@ -88,14 +94,25 @@ TileManager::~TileManager() {
}
}
void TileManager::Prepare(bool tile, uint64_t tiled_capacity, uint64_t linear_capacity,
std::span<const GpuTileInfo> infos, uint64_t source_base,
uint64_t target_base, std::vector<Dispatch>& dispatches,
ColorTransform transform) {
EXIT_IF(infos.empty() || tiled_capacity == 0 || linear_capacity == 0);
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
TileManager::Tiling TileManager::Prepare(bool tile, uint64_t tiled_capacity,
uint64_t linear_capacity, const ImageInfo& info,
uint32_t levels, uint64_t source_base,
uint64_t target_base) {
levels = levels != 0 ? levels : info.resources.levels;
EXIT_IF(levels == 0 || levels > MaxMipLevels || levels > info.resources.levels ||
tiled_capacity == 0 || linear_capacity == 0);
EXIT_NOT_IMPLEMENTED(tiled_capacity > UINT32_MAX || linear_capacity > UINT32_MAX);
const auto& block = info.tiling.block;
const auto transform = info.GetColorTransform();
EXIT_NOT_IMPLEMENTED(
!info.IsTiled() || info.samples != 1 || Prospero::IsFmaskTextureFormat(info.guest_format) ||
block.family >= TileBlockFamily::Count || !std::has_single_bit(block.bytes_per_element) ||
block.bytes_per_element > 16 || block.block_width == 0 || block.block_height == 0 ||
block.block_depth == 0 || block.block_size == 0 || info.tiling.texel_width == 0 ||
info.tiling.texel_height == 0 || info.tiled_slice_stride == 0);
EXIT_IF((transform == ColorTransform::SwapBgra16 && block.bytes_per_element != 8u) ||
(transform == ColorTransform::Reverse10_11_11 && block.bytes_per_element != 4u));
const auto checked_multiply = [](uint64_t left, uint64_t right, uint64_t& result) {
return (left == 0 || right <= UINT64_MAX / left) && (result = left * right, true);
};
@@ -106,112 +123,96 @@ void TileManager::Prepare(bool tile, uint64_t tiled_capacity, uint64_t linear_ca
return size != 0 && offset <= capacity && size <= capacity - offset;
};
dispatches.clear();
dispatches.reserve(infos.size());
for (const auto& info: infos) {
EXIT_IF((transform == ColorTransform::SwapBgra16 && info.bytes_per_element != 8u) ||
(transform == ColorTransform::Reverse10_11_11 && info.bytes_per_element != 4u));
TileBlockLayout block {};
const uint32_t tiled_width = info.tiled_width != 0 ? info.tiled_width : info.pitch;
const uint32_t tiled_height = info.tiled_height != 0 ? info.tiled_height : info.height;
const uint64_t groups_x = (static_cast<uint64_t>(info.width) + 7u) / 8u;
const uint64_t groups_y = (static_cast<uint64_t>(info.height) + 7u) / 8u;
EXIT_NOT_IMPLEMENTED(
!TileGetBlockLayout(info.family, info.bytes_per_element, block) || info.width == 0 ||
info.height == 0 || info.depth == 0 || info.pitch < info.width ||
groups_x > limits.maxComputeWorkGroupCount[0] ||
groups_y > limits.maxComputeWorkGroupCount[1] ||
info.depth > limits.maxComputeWorkGroupCount[2] ||
(!info.tail && (tiled_width < info.width || tiled_height < info.height)) ||
!valid_range(info.linear_offset, info.linear_size, linear_capacity) ||
!valid_range(info.tiled_offset, info.tiled_size, tiled_capacity) ||
(block.block_depth == 1 && info.depth != 1));
// ImageInfo owns the layout. Upload only the header and active mip records.
TilingParams params;
params.color_transform = static_cast<uint32_t>(transform);
for (uint32_t level = 0; level < levels; ++level) {
const auto& mip = info.mip_layout[level];
const auto extent = info.MipExtent(level);
const auto width = static_cast<uint32_t>(
(uint64_t {extent.width} + info.tiling.texel_width - 1u) / info.tiling.texel_width);
const auto height = static_cast<uint32_t>(
(uint64_t {extent.height} + info.tiling.texel_height - 1u) / info.tiling.texel_height);
const auto depth = extent.depth;
const bool tail = level >= info.first_tail_level;
EXIT_NOT_IMPLEMENTED(width == 0 || height == 0 || depth == 0 || mip.linear_pitch < width ||
mip.linear_size == 0 || mip.size == 0 ||
(!tail && (mip.pitch < width || mip.height < height)) ||
(!info.IsVolume() && mip.size % depth != 0));
uint64_t pitch_bytes = 0;
EXIT_NOT_IMPLEMENTED(!checked_multiply(info.pitch, info.bytes_per_element, pitch_bytes) ||
pitch_bytes > UINT32_MAX);
uint64_t slice_bytes = info.linear_slice_stride;
EXIT_NOT_IMPLEMENTED(
!checked_multiply(mip.linear_pitch, block.bytes_per_element, pitch_bytes) ||
pitch_bytes > UINT32_MAX);
const uint64_t slice_bytes =
info.linear_slice_stride != 0 ? info.linear_slice_stride : mip.linear_size;
uint64_t minimum_slice = 0;
EXIT_NOT_IMPLEMENTED(!checked_multiply(pitch_bytes, info.height, minimum_slice));
if (slice_bytes == 0) {
slice_bytes = minimum_slice;
}
uint64_t linear_used = 0;
uint64_t bytes = 0;
EXIT_NOT_IMPLEMENTED((info.depth > 1 && slice_bytes < minimum_slice) ||
!checked_multiply(info.depth - 1u, slice_bytes, bytes) ||
uint64_t bytes = 0;
uint64_t linear_span = 0;
EXIT_NOT_IMPLEMENTED(!checked_multiply(pitch_bytes, height, minimum_slice) ||
(depth > 1 && slice_bytes < minimum_slice) ||
!checked_multiply(height - 1u, pitch_bytes, linear_used) ||
!checked_multiply(width, block.bytes_per_element, bytes) ||
!checked_add(linear_used, bytes, linear_used) ||
!checked_multiply(info.height - 1u, pitch_bytes, bytes) ||
!checked_add(linear_used, bytes, linear_used) ||
!checked_multiply(info.width, info.bytes_per_element, bytes) ||
!checked_add(linear_used, bytes, linear_used) ||
linear_used > info.linear_size || slice_bytes > UINT32_MAX);
linear_used > mip.linear_size || slice_bytes > UINT32_MAX ||
!checked_multiply(depth - 1u, slice_bytes, linear_span) ||
!checked_add(linear_span, mip.linear_size, linear_span) ||
!valid_range(mip.linear_offset, linear_span, linear_capacity));
const uint64_t columns =
(static_cast<uint64_t>(tiled_width) + block.block_width - 1u) / block.block_width;
const uint64_t rows =
(static_cast<uint64_t>(tiled_height) + block.block_height - 1u) / block.block_height;
uint64_t blocks_per_slice = 0;
EXIT_NOT_IMPLEMENTED(!checked_multiply(columns, rows, blocks_per_slice) ||
columns > UINT32_MAX || blocks_per_slice > UINT32_MAX);
if (info.tail) {
(uint64_t {mip.pitch} + block.block_width - 1u) / block.block_width;
if (tail) {
EXIT_NOT_IMPLEMENTED(
info.family == TileBlockFamily::Standard256B || info.depth > block.block_depth ||
info.tail_x >= block.block_width || info.width > block.block_width - info.tail_x ||
info.tail_y >= block.block_height ||
info.height > block.block_height - info.tail_y ||
info.tiled_size < block.block_size);
} else {
const uint64_t slices =
(static_cast<uint64_t>(info.depth) + block.block_depth - 1u) / block.block_depth;
uint64_t tiled_used = 0;
EXIT_NOT_IMPLEMENTED(!checked_multiply(blocks_per_slice, slices, tiled_used) ||
!checked_multiply(tiled_used, block.block_size, tiled_used) ||
tiled_used > info.tiled_size);
block.family == TileBlockFamily::Standard256B || mip.tail_x >= block.block_width ||
width > block.block_width - mip.tail_x || mip.tail_y >= block.block_height ||
height > block.block_height - mip.tail_y);
}
const uint64_t tiled_plane = info.IsVolume() ? mip.size : mip.size / depth;
const uint64_t tiled_slice = info.tiled_slice_stride;
const uint64_t slices = (uint64_t {depth} + block.block_depth - 1u) / block.block_depth;
uint64_t tiled_span = 0;
EXIT_NOT_IMPLEMENTED(columns > UINT32_MAX || tiled_slice > UINT32_MAX ||
(slices > 1 && tiled_slice < tiled_plane) ||
!checked_multiply(slices - 1u, tiled_slice, tiled_span) ||
!checked_add(tiled_span, tiled_plane, tiled_span) ||
!valid_range(mip.offset, tiled_span, tiled_capacity));
const uint32_t alignment = std::min(info.bytes_per_element, 4u);
EXIT_NOT_IMPLEMENTED(((info.linear_offset | info.tiled_offset | pitch_bytes | slice_bytes) &
(alignment - 1u)) != 0);
const uint64_t src = source_base + (tile ? info.linear_offset : info.tiled_offset);
const uint64_t dst = target_base + (tile ? info.tiled_offset : info.linear_offset);
EXIT_NOT_IMPLEMENTED(src > UINT32_MAX || dst > UINT32_MAX);
const uint32_t family_index = static_cast<uint32_t>(info.family);
const uint32_t element_index = std::countr_zero(info.bytes_per_element);
EXIT_NOT_IMPLEMENTED(family_index >= FamilyCount || element_index >= BytesPerElementCount);
Dispatch dispatch {};
dispatch.pipeline_slot =
((tile ? FamilyCount : 0u) + family_index) * BytesPerElementCount + element_index;
dispatch.push.src_base = static_cast<uint32_t>(src);
dispatch.push.dst_base = static_cast<uint32_t>(dst);
dispatch.push.width = info.width;
dispatch.push.height = info.height;
dispatch.push.depth = info.depth;
dispatch.push.surface_z = info.surface_z;
dispatch.push.pitch_bytes = static_cast<uint32_t>(pitch_bytes);
dispatch.push.slice_bytes = static_cast<uint32_t>(slice_bytes);
dispatch.push.blocks_per_row = static_cast<uint32_t>(columns);
dispatch.push.blocks_per_slice = static_cast<uint32_t>(blocks_per_slice);
dispatch.push.tail_x = info.tail_x;
dispatch.push.tail_y = info.tail_y;
dispatch.push.tail = info.tail;
dispatch.push.color_transform = static_cast<uint32_t>(transform);
dispatches.push_back(dispatch);
const uint32_t alignment = std::min(block.bytes_per_element, 4u);
EXIT_NOT_IMPLEMENTED(
((mip.linear_offset | mip.offset | pitch_bytes | slice_bytes | tiled_slice) &
(alignment - 1u)) != 0);
uint64_t src = 0, dst = 0, texels = 0;
EXIT_NOT_IMPLEMENTED(
!checked_add(source_base, tile ? mip.linear_offset : mip.offset, src) ||
!checked_add(target_base, tile ? mip.offset : mip.linear_offset, dst) ||
src > UINT32_MAX || dst > UINT32_MAX || !checked_multiply(width, height, texels) ||
!checked_multiply(texels, depth, texels) || texels > UINT32_MAX - params.num_texels);
params.mips[level] = {static_cast<uint32_t>(src),
static_cast<uint32_t>(dst),
width,
height,
static_cast<uint32_t>(pitch_bytes),
static_cast<uint32_t>(slice_bytes),
static_cast<uint32_t>(tiled_slice),
static_cast<uint32_t>(columns),
mip.surface_z,
tail ? mip.tail_x : 0u,
tail ? mip.tail_y : 0u,
static_cast<uint32_t>(texels)};
params.num_texels += static_cast<uint32_t>(texels);
}
const uint64_t uniform_alignment =
std::max<uint64_t>(limits.minUniformBufferOffsetAlignment, 1);
const uint64_t stride = Common::AlignUp<uint64_t>(sizeof(Push), uniform_alignment);
EXIT_NOT_IMPLEMENTED(dispatches.size() > UINT64_MAX / stride);
const uint64_t bytes = dispatches.size() * stride;
auto [mapped, offset] = m_stream_buffer.Map(bytes, uniform_alignment);
EXIT_IF(mapped == nullptr);
for (size_t index = 0; index < dispatches.size(); index++) {
std::memcpy(mapped + index * stride, &dispatches[index].push, sizeof(Push));
dispatches[index].params_offset = offset + index * stride;
}
m_stream_buffer.Commit();
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
const auto params_size =
static_cast<uint16_t>(offsetof(TilingParams, mips) + levels * sizeof(Mip));
const auto offset = m_stream_buffer.Copy(
&params, params_size,
std::max<uint64_t>(limits.minUniformBufferOffsetAlignment, alignof(TilingParams)));
const auto slot =
((tile ? FamilyCount : 0u) + static_cast<uint32_t>(block.family)) * BytesPerElementCount +
std::countr_zero(block.bytes_per_element);
return {offset, params.num_texels, params_size, static_cast<uint16_t>(slot)};
}
vk::Pipeline TileManager::GetPipeline(uint32_t slot) {
@@ -257,10 +258,9 @@ vk::Pipeline TileManager::GetPipeline(uint32_t slot) {
return m_pipelines[slot];
}
void TileManager::Record(vk::Buffer source, uint64_t source_offset,
uint64_t source_capacity, vk::Buffer target, uint64_t target_offset,
uint64_t target_capacity, std::span<Dispatch> dispatches,
bool clear_target) {
void TileManager::Record(vk::Buffer source, uint64_t source_offset, uint64_t source_capacity,
vk::Buffer target, uint64_t target_offset, uint64_t target_capacity,
const Tiling& tiling, bool clear_target) {
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
const uint64_t descriptor_alignment =
std::max<uint64_t>(limits.minStorageBufferOffsetAlignment, 4);
@@ -295,9 +295,8 @@ void TileManager::Record(vk::Buffer source, uint64_t source_offset,
barriers[2].srcAccessMask = vk::AccessFlagBits::eHostWrite;
barriers[2].dstAccessMask = vk::AccessFlagBits::eUniformRead;
barriers[2].buffer = m_stream_buffer.Handle();
barriers[2].offset = dispatches.front().params_offset;
barriers[2].size =
dispatches.back().params_offset - dispatches.front().params_offset + sizeof(Push);
barriers[2].offset = tiling.params_offset;
barriers[2].size = tiling.params_size;
command.pipelineBarrier(
vk::PipelineStageFlagBits::eAllCommands | vk::PipelineStageFlagBits::eHost,
vk::PipelineStageFlagBits::eComputeShader | vk::PipelineStageFlagBits::eTransfer, {}, 0,
@@ -314,24 +313,26 @@ void TileManager::Record(vk::Buffer source, uint64_t source_offset,
const vk::DescriptorBufferInfo source_info {source, source_descriptor_offset, source_range};
const vk::DescriptorBufferInfo target_info {target, target_descriptor_offset, target_range};
for (auto& dispatch: dispatches) {
const vk::DescriptorBufferInfo params_info {m_stream_buffer.Handle(),
dispatch.params_offset, sizeof(Push)};
const vk::DescriptorBufferInfo infos[] {source_info, target_info, params_info};
std::array<vk::WriteDescriptorSet, 3> writes {};
for (uint32_t index = 0; index < writes.size(); index++) {
writes[index].dstBinding = index;
writes[index].descriptorCount = 1;
writes[index].descriptorType = index == 2 ? vk::DescriptorType::eUniformBuffer
: vk::DescriptorType::eStorageBuffer;
writes[index].pBufferInfo = &infos[index];
}
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, m_pipeline_layout, 0,
static_cast<uint32_t>(writes.size()), writes.data());
command.bindPipeline(vk::PipelineBindPoint::eCompute, GetPipeline(dispatch.pipeline_slot));
command.dispatch((dispatch.push.width + 7u) / 8u, (dispatch.push.height + 7u) / 8u,
dispatch.push.depth);
const vk::DescriptorBufferInfo params_info {m_stream_buffer.Handle(), tiling.params_offset,
tiling.params_size};
const vk::DescriptorBufferInfo infos[] {source_info, target_info, params_info};
std::array<vk::WriteDescriptorSet, 3> writes {};
for (uint32_t index = 0; index < writes.size(); index++) {
writes[index].dstBinding = index;
writes[index].descriptorCount = 1;
writes[index].descriptorType =
index == 2 ? vk::DescriptorType::eUniformBuffer : vk::DescriptorType::eStorageBuffer;
writes[index].pBufferInfo = &infos[index];
}
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, m_pipeline_layout, 0,
static_cast<uint32_t>(writes.size()), writes.data());
command.bindPipeline(vk::PipelineBindPoint::eCompute, GetPipeline(tiling.pipeline_slot));
const uint64_t groups = (uint64_t {tiling.num_texels} + 63u) / 64u;
const uint32_t groups_y = static_cast<uint32_t>(
(groups + limits.maxComputeWorkGroupCount[0] - 1u) / limits.maxComputeWorkGroupCount[0]);
const uint32_t groups_x = static_cast<uint32_t>((groups + groups_y - 1u) / groups_y);
EXIT_NOT_IMPLEMENTED(groups_y > limits.maxComputeWorkGroupCount[1]);
command.dispatch(groups_x, groups_y, 1);
barriers[1].srcAccessMask = vk::AccessFlagBits::eShaderWrite;
barriers[1].dstAccessMask = vk::AccessFlagBits::eTransferRead | vk::AccessFlagBits::eMemoryRead;
@@ -341,51 +342,53 @@ void TileManager::Record(vk::Buffer source, uint64_t source_offset,
}
TileManager::Result TileManager::Detile(vk::Buffer tiled, uint64_t tiled_offset,
uint64_t tiled_capacity, uint64_t linear_capacity,
std::span<const GpuTileInfo> infos,
ColorTransform transform) {
const ImageInfo& info) {
if (!info.IsTiled()) {
const Result source {tiled, tiled_offset, info.data.size};
return info.GetColorTransform() == ColorTransform::None
? source
: TransformColor(source, info.GetColorTransform(), true);
}
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
const uint64_t descriptor_alignment =
std::max<uint64_t>(limits.minStorageBufferOffsetAlignment, 4);
const uint64_t source_base = tiled_offset & (descriptor_alignment - 1);
std::vector<Dispatch> dispatches;
Prepare(false, tiled_capacity, linear_capacity, infos, source_base, 0, dispatches, transform);
const uint64_t source_base = tiled_offset & (descriptor_alignment - 1);
const auto linear_capacity = info.LinearSize();
const auto tiling = Prepare(false, info.data.size, linear_capacity, info, 0, source_base, 0);
auto scratch = GetScratchBuffer(linear_capacity, tiled);
Record(tiled, tiled_offset, tiled_capacity, scratch.buffer, 0, scratch.size, dispatches,
true);
Record(tiled, tiled_offset, info.data.size, scratch.buffer, 0, scratch.size, tiling, true);
return {scratch.buffer, 0, linear_capacity};
}
void TileManager::Tile(vk::Buffer linear, uint64_t linear_offset, uint64_t linear_capacity,
vk::Buffer tiled, uint64_t tiled_offset, uint64_t tiled_capacity,
std::span<const GpuTileInfo> infos) {
const ImageInfo& info, uint32_t levels) {
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
const uint64_t descriptor_alignment =
std::max<uint64_t>(limits.minStorageBufferOffsetAlignment, 4);
const uint64_t source_base = linear_offset & (descriptor_alignment - 1);
const uint64_t target_base = tiled_offset & (descriptor_alignment - 1);
std::vector<Dispatch> dispatches;
Prepare(true, tiled_capacity, linear_capacity, infos, source_base, target_base, dispatches);
Record(linear, linear_offset, linear_capacity, tiled, tiled_offset, tiled_capacity,
dispatches, false);
const uint64_t source_base = linear_offset & (descriptor_alignment - 1);
const uint64_t target_base = tiled_offset & (descriptor_alignment - 1);
const auto tiling =
Prepare(true, tiled_capacity, linear_capacity, info, levels, source_base, target_base);
Record(linear, linear_offset, linear_capacity, tiled, tiled_offset, tiled_capacity, tiling,
false);
}
void TileManager::TileImage(Image& image, std::span<const vk::BufferImageCopy> regions,
vk::Buffer tiled, uint64_t tiled_offset, uint64_t tiled_capacity,
uint64_t linear_capacity, std::span<const GpuTileInfo> infos,
ColorTransform transform) {
uint32_t levels) {
EXIT_IF(regions.empty());
const auto& limits = m_graphics.GetPhysicalDeviceProperties().limits;
const uint64_t descriptor_alignment =
std::max<uint64_t>(limits.minStorageBufferOffsetAlignment, 4);
const uint64_t target_base = tiled_offset & (descriptor_alignment - 1);
std::vector<Dispatch> dispatches;
const uint64_t target_base = tiled_offset & (descriptor_alignment - 1);
const auto linear_capacity = image.info.LinearSize(levels);
// Reserve stream parameters before recording the image download.
Prepare(true, tiled_capacity, linear_capacity, infos, 0, target_base, dispatches, transform);
const auto tiling =
Prepare(true, tiled_capacity, linear_capacity, image.info, levels, 0, target_base);
auto linear = GetScratchBuffer(linear_capacity, tiled);
image.Download(regions, linear.buffer, 0, linear.size);
Record(linear.buffer, 0, linear_capacity, tiled, tiled_offset, tiled_capacity,
dispatches, false);
Record(linear.buffer, 0, linear_capacity, tiled, tiled_offset, tiled_capacity, tiling, false);
}
TileManager::Result TileManager::GetScratchBuffer(uint64_t size, vk::Buffer input) {
@@ -563,13 +566,13 @@ void TileManager::ConvertD16(Result source, Result target, D16Direction directio
}
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, m_pipeline_layout, 0,
static_cast<uint32_t>(writes.size()), writes.data());
Push push {};
Conversion push {};
push.src_base = source_binding.base;
push.dst_base = target_binding.base;
push.width = layout.width;
push.height = rows;
push.pitch_bytes = static_cast<uint32_t>(layout.source_row_stride);
push.slice_bytes = static_cast<uint32_t>(layout.target_row_stride);
push.source_pitch = static_cast<uint32_t>(layout.source_row_stride);
push.target_pitch = static_cast<uint32_t>(layout.target_row_stride);
command.pushConstants(m_pipeline_layout, vk::ShaderStageFlagBits::eCompute, 0,
sizeof(push), &push);
command.dispatch(static_cast<uint32_t>(groups_x), rows, 1);
@@ -644,7 +647,7 @@ void TileManager::TransformColor(Result input, Result output, ColorTransform tra
command.bindPipeline(vk::PipelineBindPoint::eCompute, m_color_transform);
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eCompute, m_pipeline_layout, 0,
static_cast<uint32_t>(writes.size()), writes.data());
Push push {};
Conversion push {};
push.src_base = input_binding.base;
push.dst_base = output_binding.base;
push.width = pixels;
+35 -44
View File
@@ -9,7 +9,6 @@
#include <array>
#include <memory>
#include <span>
#include <vector>
namespace Libs::Graphics {
@@ -20,26 +19,6 @@ class StreamBuffer;
struct GraphicContext;
struct TileManagerTestAccess;
struct GpuTileInfo {
TileBlockFamily family = TileBlockFamily::Count;
uint32_t bytes_per_element = 0;
uint64_t linear_offset = 0;
uint64_t linear_size = 0;
uint64_t tiled_offset = 0;
uint64_t tiled_size = 0;
uint64_t linear_slice_stride = 0;
uint32_t width = 0;
uint32_t height = 0;
uint32_t depth = 1;
uint32_t pitch = 0;
uint32_t tail_x = 0;
uint32_t tail_y = 0;
bool tail = false;
uint32_t tiled_width = 0;
uint32_t tiled_height = 0;
uint32_t surface_z = 0;
};
class TileManager final {
public:
enum class D16Direction { Promote, Demote };
@@ -64,15 +43,12 @@ public:
KYTY_CLASS_NO_COPY(TileManager);
// Consume scratch results before the next acquisition, or pass their buffer as input.
[[nodiscard]] Result Detile(vk::Buffer tiled, uint64_t tiled_offset, uint64_t tiled_capacity,
uint64_t linear_capacity, std::span<const GpuTileInfo> infos,
ColorTransform transform = ColorTransform::None);
[[nodiscard]] Result Detile(vk::Buffer tiled, uint64_t tiled_offset, const ImageInfo& info);
void Tile(vk::Buffer linear, uint64_t linear_offset, uint64_t linear_capacity, vk::Buffer tiled,
uint64_t tiled_offset, uint64_t tiled_capacity, std::span<const GpuTileInfo> infos);
uint64_t tiled_offset, uint64_t tiled_capacity, const ImageInfo& info,
uint32_t levels = 0);
void TileImage(Image& image, std::span<const vk::BufferImageCopy> regions, vk::Buffer tiled,
uint64_t tiled_offset, uint64_t tiled_capacity, uint64_t linear_capacity,
std::span<const GpuTileInfo> infos,
ColorTransform transform = ColorTransform::None);
uint64_t tiled_offset, uint64_t tiled_capacity, uint32_t levels = 0);
[[nodiscard]] Result GetScratchBuffer(uint64_t size, vk::Buffer input = nullptr);
void ConvertD16(Result source, Result target, D16Direction direction, bool d32,
const D16Layout& layout);
@@ -86,29 +62,44 @@ private:
static constexpr uint32_t BytesPerElementCount = 5;
static constexpr uint32_t DirectionCount = 2;
static constexpr uint32_t PipelineCount = FamilyCount * BytesPerElementCount * DirectionCount;
static constexpr uint32_t MaxMipLevels = 16;
struct Push {
struct alignas(16) Mip {
uint32_t src_base;
uint32_t dst_base;
uint32_t width;
uint32_t height;
uint32_t depth;
uint32_t surface_z;
uint32_t pitch_bytes;
uint32_t slice_bytes;
uint32_t tiled_slice_bytes;
uint32_t blocks_per_row;
uint32_t blocks_per_slice;
uint32_t surface_z;
uint32_t tail_x;
uint32_t tail_y;
uint32_t tail;
uint32_t num_texels;
};
struct TilingParams {
uint32_t num_texels = 0;
uint32_t color_transform = 0;
std::array<uint32_t, 2> padding {};
std::array<Mip, MaxMipLevels> mips;
};
struct Tiling {
uint64_t params_offset;
uint32_t num_texels;
uint16_t params_size;
uint16_t pipeline_slot;
};
struct Conversion {
uint32_t src_base;
uint32_t dst_base;
uint32_t width;
uint32_t height;
uint32_t source_pitch;
uint32_t target_pitch;
uint32_t color_transform;
uint32_t to_host;
};
struct Dispatch {
Push push {};
uint32_t pipeline_slot = 0;
uint64_t params_offset = 0;
};
struct StorageBinding {
vk::DescriptorBufferInfo info;
uint32_t base = 0;
@@ -119,12 +110,12 @@ private:
uint64_t active, uint32_t remaining,
uint64_t alignment, uint64_t max_range,
uint32_t max_groups) noexcept;
void Prepare(bool tile, uint64_t tiled_capacity, uint64_t linear_capacity,
std::span<const GpuTileInfo> infos, uint64_t source_base, uint64_t target_base,
std::vector<Dispatch>& dispatches, ColorTransform transform = ColorTransform::None);
void Record(vk::Buffer source, uint64_t source_offset, uint64_t source_capacity,
vk::Buffer target, uint64_t target_offset, uint64_t target_capacity,
std::span<Dispatch> dispatches, bool clear_target);
[[nodiscard]] Tiling Prepare(bool tile, uint64_t tiled_capacity, uint64_t linear_capacity,
const ImageInfo& info, uint32_t levels, uint64_t source_base,
uint64_t target_base);
void Record(vk::Buffer source, uint64_t source_offset, uint64_t source_capacity,
vk::Buffer target, uint64_t target_offset, uint64_t target_capacity,
const Tiling& tiling, bool clear_target);
[[nodiscard]] vk::Pipeline GetPipeline(uint32_t slot);
GraphicContext& m_graphics;
@@ -378,51 +378,6 @@ static TextureCache::ImageDesc NullTextureDesc(const ShaderRecompiler::IR::Image
return desc;
}
static void PopulateTextureMipLayout(ImageInfo& info) {
if (info.IsVolume() && info.tile_mode != Prospero::TileMode::kLinear) {
TileSurfaceLayout surface {};
const TileSurfaceDescription description {
info.guest_format, info.tile_mode, TileSurfaceDimension::Dim3D, info.extent.width,
info.extent.height, info.extent.depth, info.resources.levels, 1};
if (!TileGetTiledTextureLayout(description, surface)) {
EXIT("unsupported normalized volume texture layout\n");
}
for (uint32_t level = 0; level < info.resources.levels; level++) {
const auto& mip = surface.mips[level];
info.mip_layout[level] = {
mip.offset,
mip.size,
mip.padded_width,
mip.padded_height,
};
}
return;
}
TileSizeOffset levels[16] {};
TilePaddedSize padded[16] {};
TileGetTextureSize(info.guest_format, info.extent.width, info.extent.height,
info.resources.levels, info.tile_mode, nullptr, levels, padded);
const auto texel_shift = info.IsBlock() ? 2u : 0u;
for (uint32_t level = 0; level < info.resources.levels; level++) {
const auto offset =
levels[level].src_size != 0 ? levels[level].src_offset : levels[level].offset;
auto size = static_cast<uint64_t>(levels[level].src_size != 0 ? levels[level].src_size
: levels[level].size);
if (info.IsVolume()) {
size *= std::max(info.extent.depth >> level, 1u);
} else {
size *= info.resources.layers;
}
info.mip_layout[level] = {
offset,
size,
padded[level].width >> texel_shift,
padded[level].height >> texel_shift,
};
}
}
static ImageViewInfo TextureViewInfo(const ShaderRecompiler::IR::ImageResource& resource,
const ShaderTextureResource& descriptor, vk::Format format,
const SurfaceFormatInfo& surface_format, bool storage,
@@ -490,45 +445,54 @@ static ImageViewInfo TextureViewInfo(const ShaderRecompiler::IR::ImageResource&
return view;
}
static bool ResolveTextureMipView(const TileSurfaceDescription& description, bool metadata,
uint32_t view_levels, uint32_t& levels, uint32_t& base_level) {
TileSurfaceLayout physical {};
TileSurfaceLayout view {};
auto view_description = description;
view_description.levels = levels;
if (!TileGetTiledTextureLayout(description, physical) ||
!TileGetTiledTextureLayout(view_description, view)) {
static bool ResolveTextureMipView(ImageInfo& info, uint32_t physical_levels, bool metadata,
uint32_t view_levels, uint32_t& base_level) {
if (!info.IsTiled()) {
return false;
}
if (physical.first_tail_level == view.first_tail_level &&
physical.block_slice_size == view.block_slice_size &&
physical.total_size == view.total_size &&
std::equal(std::begin(physical.mips), std::begin(physical.mips) + description.levels,
std::begin(view.mips))) {
return true;
ImageInfo physical = info;
physical.resources.levels = physical_levels;
physical.UpdateSize();
info.UpdateSize();
const auto same_mip = [&](uint32_t target, uint32_t source) {
const auto& left = physical.mip_layout[target];
const auto& right = info.mip_layout[source];
return left.offset == right.offset && left.size == right.size &&
left.pitch == right.pitch && left.height == right.height &&
left.tail_x == right.tail_x && left.tail_y == right.tail_y &&
left.surface_z == right.surface_z &&
physical.MipExtent(target) == info.MipExtent(source);
};
if (physical.first_tail_level == info.first_tail_level &&
physical.tiled_slice_stride == info.tiled_slice_stride &&
physical.data.size == info.data.size) {
bool matches = true;
for (uint32_t level = 0; level < physical_levels; ++level) {
matches &= same_mip(level, level);
}
if (matches) {
return true;
}
}
if (metadata || ((description.layers > 1 || description.depth > 1) &&
physical.block_slice_size != view.block_slice_size)) {
if (metadata ||
(info.TransferLayers() > 1 && physical.tiled_slice_stride != info.tiled_slice_stride)) {
return false;
}
// T# addresses the last mip. A view can select the same stored subresources
// with different mip indices; inaccessible mips need no host representation.
for (uint32_t base = 0; base + view_levels <= description.levels; ++base) {
for (uint32_t base = 0; base + view_levels <= physical_levels; ++base) {
bool matches = true;
for (uint32_t i = 0; i < view_levels; ++i) {
const auto source = base_level + i;
const auto target = base + i;
if (physical.mips[target] != view.mips[source] ||
(target >= physical.first_tail_level) != (source >= view.first_tail_level) ||
std::max(description.width >> target, 1u) != std::max(description.width >> source, 1u) ||
std::max(description.height >> target, 1u) != std::max(description.height >> source, 1u) ||
std::max(description.depth >> target, 1u) != std::max(description.depth >> source, 1u)) {
if (!same_mip(target, source) ||
(target >= physical.first_tail_level) != (source >= info.first_tail_level)) {
matches = false;
break;
}
}
if (matches) {
levels = description.levels;
info = std::move(physical);
base_level = base;
return true;
}
@@ -613,40 +577,6 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
type == Prospero::ImageType::kColor2DMsaaArray;
const auto image_layers = layered ? depth : 1u;
auto view_base = static_cast<uint32_t>(base_level);
if (levels > physical_levels) {
const TileSurfaceDescription physical {
format, tile, volume ? TileSurfaceDimension::Dim3D : TileSurfaceDimension::Dim2D,
width, height, volume ? depth : 1u, physical_levels, image_layers};
if (!ResolveTextureMipView(physical, !resource.r128 && descriptor.MetaCompress(),
view_levels, levels, view_base)) {
EXIT("unsupported texture mip view changes physical layout: base=%u last=%u max=%u "
"extent=%ux%ux%u tile=%u\n",
base_level, last_level, max_mip, width, height, depth,
static_cast<uint32_t>(tile));
}
}
uint32_t pitch = 0;
TileSizeAlign size {};
if (multisampled) {
const auto bytes = Prospero::NumBytesPerElement(format);
pitch = depth_tile ? TileGetDepthPitch(width, bytes, last_level)
: TileGetRenderTargetPitch(width, bytes, last_level);
if (pitch == 0 || !TileGetRenderTargetSize(width, height, pitch, bytes, size, last_level) ||
size.size > UINT32_MAX / image_layers) {
EXIT("unsupported multisample texture layout\n");
}
size.size *= image_layers;
} else {
pitch = TileGetTexturePitch(format, width, tile);
TileGetTextureTotalSize(format, width, height, volume ? depth : image_layers,
physical_levels, tile, volume, size);
}
EXIT_NOT_IMPLEMENTED(size.size == 0 || size.align == 0 ||
(address & (static_cast<uint64_t>(size.align) - 1u)) != 0);
if (storage) {
ValidateStorageTexture(resource, descriptor, size.size);
}
auto pixel_format = surface_format.vk_format;
if (resource.depth_compare) {
if (const auto* depth_format = FindGuestDepthFormatPolicy(format)) {
@@ -663,17 +593,35 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
: pixel_format;
const auto block_bytes = Prospero::BlockCompressedBytesPerBlock(format);
TextureCache::ImageDesc desc {};
desc.info.data = {address, size.size};
desc.info.data = {address, 0};
desc.info.pixel_format = pixel_format;
desc.info.guest_format = format;
desc.info.type = TextureBaseType(type);
desc.info.extent = {width, height, volume ? depth : 1u};
desc.info.resources = {levels, image_layers};
desc.info.pitch = pitch;
desc.info.bytes_per_block =
block_bytes != 0 ? block_bytes : Prospero::NumBytesPerElement(format);
desc.info.samples = samples;
desc.info.tile_mode = tile;
if (levels > physical_levels) {
if (!ResolveTextureMipView(desc.info, physical_levels,
!resource.r128 && descriptor.MetaCompress(), view_levels,
view_base)) {
EXIT("unsupported texture mip view changes physical layout: base=%u last=%u max=%u "
"extent=%ux%ux%u tile=%u\n",
base_level, last_level, max_mip, width, height, depth,
static_cast<uint32_t>(tile));
}
} else {
desc.info.UpdateSize();
}
const auto alignment =
tile == Prospero::TileMode::kLinear ? 256u : desc.info.tiling.block.block_size;
EXIT_NOT_IMPLEMENTED(desc.info.data.size == 0 || alignment == 0 ||
(address & (static_cast<uint64_t>(alignment) - 1u)) != 0);
if (storage) {
ValidateStorageTexture(resource, descriptor, desc.info.data.size);
}
if (!resource.r128 && descriptor.MetaCompress() && tile != Prospero::TileMode::kDepth &&
!desc.info.IsDepth()) {
TileSizeAlign metadata_size {};
@@ -684,11 +632,6 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
desc.info.metadata.range = {descriptor.MetaAddr() << 8u, metadata_size.size};
desc.info.metadata.dcc_alpha_msb = descriptor.DccAlphaPos();
}
if (samples > 1) {
desc.info.mip_layout[0] = {0, size.size, pitch, height};
} else {
PopulateTextureMipLayout(desc.info);
}
desc.view_info = TextureViewInfo(resource, descriptor, view_format, surface_format, storage,
view_levels, desc.info.resources.layers);
desc.view_info.base_level = view_base;
@@ -704,7 +647,8 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
if (storage) {
EXIT("depth target cannot be bound as a storage image\n");
}
ValidateSampledDepthBinding(resource, descriptor, *image, pixel_format, size.size);
ValidateSampledDepthBinding(resource, descriptor, *image, pixel_format,
desc.info.data.size);
} else if (storage) {
ValidateStorageColorView(image->info.pixel_format, view_format, descriptor.DstSelXYZW());
} else {
+28 -30
View File
@@ -312,9 +312,9 @@ static void LogDrawInputState(const CommandBuffer& buffer, const RenderColorInfo
}
}
static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuffer vk_buffer,
const ShaderVertexInputInfo& vs_input_info,
const RenderDepthInfo& depth, const RenderState& rendering) {
static void UpdateGraphicsDynamicState(const CommandBuffer& buffer, DynamicState& dynamic_state,
const ShaderVertexInputInfo& vs_input_info,
const RenderDepthInfo& depth, const RenderState& rendering) {
KYTY_PROFILER_FUNCTION();
const auto& ctx = buffer.GetRegisters();
@@ -358,8 +358,8 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
scissor.extent = {0, 0};
}
}
vk_buffer.setViewportWithCount(viewport_count, viewports.data());
vk_buffer.setScissorWithCount(viewport_count, scissors.data());
dynamic_state.SetViewports(std::span {viewports}.first(viewport_count));
dynamic_state.SetScissors(std::span {scissors}.first(viewport_count));
float line_width = ctx.GetLineWidth();
if (line_width != 1.0f) {
@@ -372,17 +372,17 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
}
line_width = 1.0f;
}
vk_buffer.setLineWidth(line_width);
dynamic_state.SetLineWidth(line_width);
const auto& blend = ctx.GetBlendColor();
const std::array blend_constants {blend.red, blend.green, blend.blue, blend.alpha};
vk_buffer.setBlendConstants(blend_constants.data());
vk_buffer.setDepthTestEnable(depth.depth_test_enable ? VK_TRUE : VK_FALSE);
vk_buffer.setDepthWriteEnable(depth.depth_write_enable ? VK_TRUE : VK_FALSE);
vk_buffer.setDepthCompareOp(depth.depth_compare_op);
dynamic_state.SetBlendConstants(blend_constants);
dynamic_state.SetDepthTestEnabled(depth.depth_test_enable);
dynamic_state.SetDepthWriteEnabled(depth.depth_write_enable);
dynamic_state.SetDepthCompareOp(depth.depth_compare_op);
#if !defined(__APPLE__)
vk_buffer.setDepthBoundsTestEnable(depth.depth_bounds_test_enable ? VK_TRUE : VK_FALSE);
dynamic_state.SetDepthBoundsTestEnabled(depth.depth_bounds_test_enable);
if (depth.depth_bounds_test_enable) {
vk_buffer.setDepthBounds(ctx.GetDepthBoundsMin(), ctx.GetDepthBoundsMax());
dynamic_state.SetDepthBounds(ctx.GetDepthBoundsMin(), ctx.GetDepthBoundsMax());
}
#endif
@@ -391,7 +391,7 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
const bool use_front = mode.poly_offset_front_enable && !mode.cull_front;
const bool use_back = mode.poly_offset_back_enable && !mode.cull_back;
const bool depth_bias_enable = use_front || use_back;
vk_buffer.setDepthBiasEnable(depth_bias_enable ? VK_TRUE : VK_FALSE);
dynamic_state.SetDepthBiasEnabled(depth_bias_enable);
if (depth_bias_enable) {
// Vulkan has one bias for both faces. Prefer a visible front face when both are enabled.
const float guest_constant_factor =
@@ -400,19 +400,18 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
guest_constant_factor, poly_offset, depth.desc.view_info.format);
const float slope_factor =
(use_front ? poly_offset.front_scale : poly_offset.back_scale) / 16.0f;
vk_buffer.setDepthBias(constant_factor, poly_offset.clamp, slope_factor);
dynamic_state.SetDepthBias(constant_factor, poly_offset.clamp, slope_factor);
}
vk_buffer.setStencilTestEnable(depth.stencil_test_enable ? VK_TRUE : VK_FALSE);
dynamic_state.SetStencilTestEnabled(depth.stencil_test_enable);
if (depth.stencil_test_enable) {
const auto set_stencil = [&](vk::StencilFaceFlagBits face, const vk::StencilOpState& state) {
vk_buffer.setStencilOp(face, state.failOp, state.passOp, state.depthFailOp, state.compareOp);
vk_buffer.setStencilCompareMask(face, state.compareMask);
vk_buffer.setStencilWriteMask(face, state.writeMask);
vk_buffer.setStencilReference(face, state.reference);
};
set_stencil(vk::StencilFaceFlagBits::eFront, depth.stencil_front);
set_stencil(vk::StencilFaceFlagBits::eBack, depth.stencil_back);
const auto& front = depth.stencil_front;
const auto& back = depth.stencil_back;
dynamic_state.SetStencilOps({front.failOp, front.passOp, front.depthFailOp, front.compareOp},
{back.failOp, back.passOp, back.depthFailOp, back.compareOp});
dynamic_state.SetStencilCompareMasks(front.compareMask, back.compareMask);
dynamic_state.SetStencilWriteMasks(front.writeMask, back.writeMask);
dynamic_state.SetStencilReferences(front.reference, back.reference);
}
#if defined(__APPLE__)
@@ -423,9 +422,7 @@ static void SetGraphicsDynamicParams(const CommandBuffer& buffer, vk::CommandBuf
for (uint32_t slot = 0; slot < rendering.num_color_attachments; slot++) {
enable[slot] = rendering.color_attachments[slot].image_view != nullptr;
}
if (rendering.num_color_attachments != 0) {
vk_buffer.setColorWriteEnableEXT(rendering.num_color_attachments, enable);
}
dynamic_state.SetColorWriteEnables(std::span {enable}.first(rendering.num_color_attachments));
#endif
}
@@ -1139,10 +1136,11 @@ void RenderExecutor::ExecutePreparedDraw(uint64_t submit_id, CommandBuffer& buff
CommitIndexBuffer(vk_buffer, index_binding);
}
SetGraphicsDynamicParams(buffer, vk_buffer, vertex_stages.back(), state.depth_info, rendering);
if (m_context.GetGraphics().attachment_feedback_loop_enabled) {
vk_buffer.setAttachmentFeedbackLoopEnableEXT(feedback_aspects);
}
auto& dynamic_state = m_context.GetCommandScheduler().GetDynamicState();
UpdateGraphicsDynamicState(buffer, dynamic_state, vertex_stages.back(), state.depth_info,
rendering);
dynamic_state.SetAttachmentFeedbackLoopAspects(feedback_aspects);
dynamic_state.Commit(m_context.GetGraphics(), vk_buffer);
LogDrawPhase(draw.Name(), "BeginRendering");
if (!draw.IsIndexed()) {
@@ -6,20 +6,13 @@ layout(local_size_x = 64) in;
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } input_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint data[]; } output_buffer;
layout(push_constant, std430) uniform Push {
layout(push_constant, std430) uniform Conversion {
uint src_base;
uint dst_base;
uint width;
uint height;
uint depth;
uint surface_z;
uint pitch_bytes;
uint slice_bytes;
uint blocks_per_row;
uint blocks_per_slice;
uint tail_x;
uint tail_y;
uint tail;
uint source_pitch;
uint target_pitch;
uint color_transform;
uint to_host;
} params;
@@ -1,66 +1,84 @@
layout(local_size_x = 8, local_size_y = 8) in;
#extension GL_EXT_shader_8bit_storage : require
#extension GL_EXT_shader_16bit_storage : require
layout(local_size_x = 64) in;
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } input_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint data[]; } output_buffer;
layout(set = 0, binding = 0, std430) readonly buffer Input8 { uint8_t data[]; } input_buffer8;
layout(set = 0, binding = 1, std430) buffer Output8 { uint8_t data[]; } output_buffer8;
layout(set = 0, binding = 0, std430) readonly buffer Input16 { uint16_t data[]; } input_buffer16;
layout(set = 0, binding = 1, std430) buffer Output16 { uint16_t data[]; } output_buffer16;
layout(set = 0, binding = 2, std140) uniform Push {
struct Mip {
uint src_base;
uint dst_base;
uint width;
uint height;
uint depth;
uint surface_z;
uint pitch_bytes;
uint slice_bytes;
uint tiled_slice_bytes;
uint blocks_per_row;
uint blocks_per_slice;
uint surface_z;
uint tail_x;
uint tail_y;
uint tail;
uint num_texels;
};
layout(set = 0, binding = 2, std140) uniform TilingInfo {
uint num_texels;
uint color_transform;
} params;
Mip mips[16];
} info;
#include "gpu_tiler_color.inc"
void copy_element(uint src, uint dst) {
if ((ELEMENT_BYTES == 4u || ELEMENT_BYTES == 8u) && params.color_transform != 0u) {
if ((ELEMENT_BYTES == 4u || ELEMENT_BYTES == 8u) && info.color_transform != 0u) {
uvec2 value = uvec2(input_buffer.data[src >> 2], 0u);
if (ELEMENT_BYTES == 8u) value.y = input_buffer.data[(src >> 2) + 1u];
value = transform_color(value, params.color_transform, TILE == 0u);
value = transform_color(value, info.color_transform, TILE == 0u);
output_buffer.data[dst >> 2] = value.x;
if (ELEMENT_BYTES == 8u) output_buffer.data[(dst >> 2) + 1u] = value.y;
} else if (ELEMENT_BYTES >= 4) {
for (uint i = 0; i < ELEMENT_BYTES; i += 4) {
output_buffer.data[(dst + i) >> 2] = input_buffer.data[(src + i) >> 2];
}
} else if (ELEMENT_BYTES == 2) {
output_buffer16.data[dst >> 1] = input_buffer16.data[src >> 1];
} else {
uint mask = ELEMENT_BYTES == 1 ? 0xffu : 0xffffu;
uint value = (input_buffer.data[src >> 2] >> ((src & 3u) * 8u)) & mask;
uint shift = (dst & 3u) * 8u;
atomicAnd(output_buffer.data[dst >> 2], ~(mask << shift));
atomicOr(output_buffer.data[dst >> 2], value << shift);
output_buffer8.data[dst] = input_buffer8.data[src];
}
}
void main() {
uvec3 p = gl_GlobalInvocationID;
if (p.x >= params.width || p.y >= params.height || p.z >= params.depth) {
uint group = gl_WorkGroupID.y * gl_NumWorkGroups.x + gl_WorkGroupID.x;
// Reject padding groups before multiplying: the active range may approach 4 GiB.
if (group > (info.num_texels - 1u) / 64u) {
return;
}
uint texel = group * 64u + gl_LocalInvocationID.x;
if (texel >= info.num_texels) {
return;
}
uint mip = 0u;
uint mip_texels = info.mips[mip].num_texels;
while (texel >= mip_texels) {
texel -= mip_texels;
mip_texels = info.mips[++mip].num_texels;
}
Mip params = info.mips[mip];
uint row = texel / params.width;
uint z = row / params.height;
uvec3 p = uvec3(texel - row * params.width, row - z * params.height, z);
uvec3 extent = block_extent();
uvec3 swizzle = p;
uvec3 block = uvec3(0);
if (params.tail != 0) {
swizzle.xy += uvec2(params.tail_x, params.tail_y);
} else {
block = p / extent;
}
uvec3 block = p / extent;
uvec3 swizzle = p + uvec3(params.tail_x, params.tail_y, params.surface_z);
uint block_index = block.z * params.blocks_per_slice +
block.y * params.blocks_per_row + block.x;
uint tiled = block_index * BLOCK_BYTES +
block_offset(uvec3(swizzle.xy, swizzle.z + params.surface_z));
uint block_index = block.y * params.blocks_per_row + block.x;
uint tiled = block.z * params.tiled_slice_bytes + block_index * BLOCK_BYTES +
block_offset(swizzle);
uint linear = p.z * params.slice_bytes + p.y * params.pitch_bytes +
p.x * ELEMENT_BYTES;
if (TILE == 0) {
@@ -6,20 +6,15 @@ layout(local_size_x = 64) in;
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } input_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint data[]; } output_buffer;
layout(push_constant, std430) uniform Push {
layout(push_constant, std430) uniform Conversion {
uint src_base;
uint dst_base;
uint width;
uint height;
uint depth;
uint surface_z;
uint pitch_bytes;
uint slice_bytes;
uint blocks_per_row;
uint blocks_per_slice;
uint tail_x;
uint tail_y;
uint tail;
uint source_pitch;
uint target_pitch;
uint color_transform;
uint to_host;
} params;
uint decode(uint value) {
@@ -35,8 +30,8 @@ void main() {
if (x >= params.width || y >= params.height) {
return;
}
uint src = params.src_base + y * params.pitch_bytes + x * 4u;
uint dst = params.dst_base + y * params.slice_bytes + x * 2u;
uint src = params.src_base + y * params.source_pitch + x * 4u;
uint dst = params.dst_base + y * params.target_pitch + x * 2u;
uint shift = (dst & 2u) * 8u;
uint mask = 0xffffu << shift;
uint word = dst >> 2u;
@@ -6,20 +6,15 @@ layout(local_size_x = 64) in;
layout(set = 0, binding = 0, std430) readonly buffer Input { uint data[]; } input_buffer;
layout(set = 0, binding = 1, std430) buffer Output { uint data[]; } output_buffer;
layout(push_constant, std430) uniform Push {
layout(push_constant, std430) uniform Conversion {
uint src_base;
uint dst_base;
uint width;
uint height;
uint depth;
uint surface_z;
uint pitch_bytes;
uint slice_bytes;
uint blocks_per_row;
uint blocks_per_slice;
uint tail_x;
uint tail_y;
uint tail;
uint source_pitch;
uint target_pitch;
uint color_transform;
uint to_host;
} params;
void main() {
@@ -28,12 +23,12 @@ void main() {
if (x >= params.width || y >= params.height) {
return;
}
uint src = params.src_base + y * params.pitch_bytes + x * 2u;
uint src = params.src_base + y * params.source_pitch + x * 2u;
uint packed = input_buffer.data[src >> 2u];
uint value = (packed >> ((src & 2u) * 8u)) & 0xffffu;
uint encoded = D32 != 0u
? floatBitsToUint(float(value) / 65535.0)
: value * 256u + (value + 128u) / 257u;
uint dst = params.dst_base + y * params.slice_bytes + x * 4u;
uint dst = params.dst_base + y * params.target_pitch + x * 4u;
output_buffer.data[dst >> 2u] = encoded;
}
+137
View File
@@ -0,0 +1,137 @@
#include "graphics/presentation/ddsImage.h"
#include "common/assert.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "imgui_impl_vulkan.h"
#include <array>
namespace Libs::Graphics {
DdsImage::~DdsImage() {
if (m_descriptor != VK_NULL_HANDLE) {
ImGui_ImplVulkan_RemoveTexture(m_descriptor);
}
m_graphics.device.destroyImageView(m_view, nullptr);
if (m_image.image != nullptr) {
m_graphics.DeleteImage(m_image);
}
if (m_staging != VK_NULL_HANDLE) {
vmaDestroyBuffer(m_graphics.allocator, m_staging, m_staging_allocation);
}
}
bool DdsImage::Load(const std::filesystem::path& path) {
EXIT_IF(m_image.image != nullptr || m_staging != VK_NULL_HANDLE);
Common::File file(path, Common::File::Mode::Read);
if (file.IsInvalid()) {
return false;
}
std::array<uint32_t, 37> header {};
uint32_t read = 0;
file.Read(header.data(), sizeof(header), &read);
const auto width = header[4];
const auto height = header[3];
if (read != sizeof(header) || header[0] != 0x20534444 || header[1] != 124 || header[19] != 32 ||
header[20] != 4 || header[21] != 0x30315844 || header[32] != 98 || header[33] != 3 ||
header[34] != 0 || header[35] != 1 || header[6] != 0 || header[7] != 1 || header[28] != 0 ||
width == 0 || height == 0 || width > 3840 || height > 2160) {
return false;
}
const uint32_t size = ((width + 3) / 4) * ((height + 3) / 4) * 16;
if (file.Size() != sizeof(header) + size) {
return false;
}
constexpr auto format = vk::Format::eBc7UnormBlock;
constexpr auto features = vk::FormatFeatureFlagBits::eSampledImage |
vk::FormatFeatureFlagBits::eSampledImageFilterLinear |
vk::FormatFeatureFlagBits::eTransferDst;
if ((m_graphics.GetFormatProperties(format).optimalTilingFeatures & features) != features) {
LOGF("[Splash] host does not support BC7 textures\n");
return false;
}
vk::BufferCreateInfo buffer {};
buffer.size = size;
buffer.usage = vk::BufferUsageFlagBits::eTransferSrc;
VmaAllocationCreateInfo allocation {};
allocation.usage = VMA_MEMORY_USAGE_AUTO_PREFER_HOST;
allocation.flags =
VMA_ALLOCATION_CREATE_MAPPED_BIT | VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
VmaAllocationInfo mapped {};
if (vmaCreateBuffer(m_graphics.allocator, static_cast<const VkBufferCreateInfo*>(buffer),
&allocation, &m_staging, &m_staging_allocation, &mapped) != VK_SUCCESS) {
LOGF("[Splash] could not allocate DDS upload buffer\n");
return false;
}
file.Read(mapped.pMappedData, size, &read);
if (read != size) {
return false;
}
RequireVulkanSuccess(static_cast<vk::Result>(vmaFlushAllocation(m_graphics.allocator,
m_staging_allocation, 0, size)),
"flush splash image upload");
vk::ImageCreateInfo image {};
image.imageType = vk::ImageType::e2D;
image.extent = {width, height, 1};
image.mipLevels = 1;
image.arrayLayers = 1;
image.format = format;
image.tiling = vk::ImageTiling::eOptimal;
image.usage = vk::ImageUsageFlagBits::eTransferDst | vk::ImageUsageFlagBits::eSampled;
image.samples = vk::SampleCountFlagBits::e1;
if (!m_graphics.CreateImage(image, m_image)) {
LOGF("[Splash] could not allocate DDS image\n");
return false;
}
vk::ImageViewCreateInfo view {};
view.image = m_image.image;
view.viewType = vk::ImageViewType::e2D;
view.format = format;
view.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
RequireVulkanSuccess(m_graphics.device.createImageView(&view, nullptr, &m_view),
"create splash image view");
m_descriptor = ImGui_ImplVulkan_AddTexture(static_cast<VkImageView>(m_view),
VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL);
return true;
}
void DdsImage::Upload(vk::CommandBuffer command) {
if (m_image.state.layout == vk::ImageLayout::eShaderReadOnlyOptimal) {
return;
}
vk::ImageMemoryBarrier barrier {};
barrier.dstAccessMask = vk::AccessFlagBits::eTransferWrite;
barrier.oldLayout = vk::ImageLayout::eUndefined;
barrier.newLayout = vk::ImageLayout::eTransferDstOptimal;
barrier.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED;
barrier.image = m_image.image;
barrier.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1};
command.pipelineBarrier(vk::PipelineStageFlagBits::eTopOfPipe,
vk::PipelineStageFlagBits::eTransfer, {}, 0, nullptr, 0, nullptr, 1,
&barrier);
vk::BufferImageCopy copy {};
copy.imageSubresource = {vk::ImageAspectFlagBits::eColor, 0, 0, 1};
copy.imageExtent = m_image.extent;
command.copyBufferToImage(m_staging, m_image.image, vk::ImageLayout::eTransferDstOptimal, 1,
&copy);
barrier.srcAccessMask = vk::AccessFlagBits::eTransferWrite;
barrier.dstAccessMask = vk::AccessFlagBits::eShaderRead;
barrier.oldLayout = vk::ImageLayout::eTransferDstOptimal;
barrier.newLayout = vk::ImageLayout::eShaderReadOnlyOptimal;
command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer,
vk::PipelineStageFlagBits::eFragmentShader, {}, 0, nullptr, 0, nullptr,
1, &barrier);
m_image.state.layout = vk::ImageLayout::eShaderReadOnlyOptimal;
}
ImTextureRef DdsImage::Texture() const {
return ImTextureRef {static_cast<ImTextureID>(reinterpret_cast<uintptr_t>(m_descriptor))};
}
} // namespace Libs::Graphics
+32
View File
@@ -0,0 +1,32 @@
#ifndef EMULATOR_SRC_GRAPHICS_PRESENTATION_DDSIMAGE_H_
#define EMULATOR_SRC_GRAPHICS_PRESENTATION_DDSIMAGE_H_
#include "graphics/host_gpu/graphicContext.h"
#include "imgui.h"
#include <filesystem>
namespace Libs::Graphics {
class DdsImage final {
public:
explicit DdsImage(GraphicContext& graphics): m_graphics(graphics) {}
~DdsImage();
KYTY_CLASS_NO_COPY(DdsImage);
bool Load(const std::filesystem::path& path);
void Upload(vk::CommandBuffer command);
[[nodiscard]] ImTextureRef Texture() const;
private:
GraphicContext& m_graphics;
VulkanImage m_image;
vk::ImageView m_view = nullptr;
VkDescriptorSet m_descriptor = VK_NULL_HANDLE;
VkBuffer m_staging = VK_NULL_HANDLE;
VmaAllocation m_staging_allocation = nullptr;
};
} // namespace Libs::Graphics
#endif // EMULATOR_SRC_GRAPHICS_PRESENTATION_DDSIMAGE_H_
+110 -10
View File
@@ -3,9 +3,12 @@
#include <SDL3/SDL.h>
#include "common/assert.h"
#include "common/emulatorConfig.h"
#include "common/file.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/presentation/ddsImage.h"
#include "imgui.h"
#include "imgui_internal.h"
#include "imgui_impl_vulkan.h"
@@ -13,6 +16,7 @@
#include "libs/dialog.h"
#include "libs/ime.h"
#include "libs/imeDialog.h"
#include "loader/systemContent.h"
#include "stb_image.h"
#include <algorithm>
@@ -23,6 +27,7 @@
#include <cstdint>
#include <cstring>
#include <deque>
#include <fmt/format.h>
#include <limits>
#include <memory>
#include <mutex>
@@ -37,6 +42,8 @@ namespace Libs::Graphics {
namespace {
using Clock = std::chrono::steady_clock;
namespace CoreIme = Libs::Ime;
namespace DialogIme = Libs::Dialog::ImeDialog;
namespace SystemDialog = Libs::Dialog::SystemDialog;
@@ -194,6 +201,10 @@ struct TrophyNotification {
std::mutex g_trophy_notification_mutex;
std::deque<TrophyNotification> g_trophy_notifications;
std::atomic<bool> g_trophy_notification_active {false};
std::array<std::filesystem::path, 2> g_splash_paths;
std::atomic<bool> g_splash_active {false};
std::atomic<Clock::duration::rep> g_splash_hidden_at {0};
bool g_splash_fast = false;
std::vector<std::string_view> WrapTrophyTitle(std::string_view text, float font_size, float width) {
std::vector<std::string_view> lines;
@@ -464,6 +475,29 @@ void CheckVulkanResult(VkResult result) {
} // namespace
void InitializeSplashScreen(const std::filesystem::path& sce_sys) {
const auto language = Config::GetConsoleLanguage();
for (size_t i = 0; i < g_splash_paths.size(); ++i) {
auto& path = g_splash_paths[i];
path = sce_sys / fmt::format("pic{}_{:02}.dds", i + 1, language);
if (!Common::File::IsFileExisting(path)) {
path = sce_sys / fmt::format("pic{}.dds", i + 1);
}
}
int32_t attribute = 0;
Loader::SystemContentParamSfoGetInt("ATTRIBUTE3", &attribute);
g_splash_fast = (attribute & 0x1000) != 0;
g_splash_hidden_at.store(0, std::memory_order_release);
g_splash_active.store(Common::File::IsFileExisting(g_splash_paths[0]) &&
Common::File::IsFileExisting(g_splash_paths[1]),
std::memory_order_release);
}
void HideSplashScreen() {
Clock::duration::rep expected = 0;
g_splash_hidden_at.compare_exchange_strong(expected, Clock::now().time_since_epoch().count());
}
void InitializeSystemOverlayInput(SDL_Window* window) {
EXIT_IF(window == nullptr);
const Uint32 type = SDL_RegisterEvents(1);
@@ -481,6 +515,7 @@ void InitializeSystemOverlayInput(SDL_Window* window) {
}
void ShutdownSystemOverlayInput() {
g_splash_active.store(false, std::memory_order_release);
CoreIme::SetVisibilityCallback(nullptr);
DialogIme::SetVisibilityCallback(nullptr);
SystemDialog::SetVisibilityCallback(nullptr);
@@ -515,8 +550,10 @@ SystemOverlayVisualState GetSystemOverlayVisualState() noexcept {
const auto dialog = DialogIme::GetVisualState();
const auto system = SystemDialog::GetVisualState();
const bool trophy_active = g_trophy_notification_active.load(std::memory_order_acquire);
return {core.active || dialog.active || system.active || trophy_active,
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u)};
const bool splash = g_splash_active.load(std::memory_order_acquire);
return {core.active || dialog.active || system.active || trophy_active || splash,
core.revision + dialog.revision + system.revision + (trophy_active ? 1u : 0u) +
(splash ? 2u : 0u)};
}
bool ProcessSystemOverlayInput(const SDL_Event& event) {
@@ -656,8 +693,6 @@ void NotifyTrophyUnlocked(std::string_view name, int32_t grade,
}
struct SystemOverlay::Impl {
using Clock = std::chrono::steady_clock;
explicit Impl(GraphicContext& context): graphics(context) {}
~Impl() {
@@ -770,6 +805,65 @@ struct SystemOverlay::Impl {
return texture;
}
void FinishSplash() {
g_splash_active.store(false, std::memory_order_release);
if (splash_dds[0] || splash_dds[1]) {
Common::LockGuard lock(graphics.queue_mutex);
RequireVulkanSuccess(graphics.queue.waitIdle(), "retire splash images");
splash_dds = {};
}
}
void DrawSplash(vk::Extent2D frame_extent) {
if (!g_splash_active.load(std::memory_order_acquire)) {
return;
}
for (size_t i = 0; i < g_splash_paths.size(); ++i) {
if (splash_dds[i]) {
continue;
}
const auto& path = g_splash_paths[i];
auto image = std::make_unique<DdsImage>(graphics);
if (!image->Load(path)) {
LOGF("[Splash] could not load %s\n", path.string().c_str());
FinishSplash();
return;
}
splash_dds[i] = std::move(image);
}
const auto now = Clock::now();
if (splash_born == Clock::time_point {}) {
splash_born = now;
}
const auto hidden = g_splash_hidden_at.load(std::memory_order_acquire);
const float fade =
hidden == 0
? 0.0f
: std::chrono::duration<float>(now.time_since_epoch() - Clock::duration {hidden})
.count() /
0.3f;
const float alpha = 1.0f - std::clamp(fade, 0.0f, 1.0f);
if (alpha == 0.0f) {
FinishSplash();
return;
}
const float age = std::chrono::duration<float>(now - splash_born).count();
const float logo = g_splash_fast ? 1.0f : std::clamp((age - 1.4f) / 2.2f, 0.0f, 1.0f);
const ImVec2 screen {static_cast<float>(frame_extent.width),
static_cast<float>(frame_extent.height)};
const float fit = std::min(screen.x / 3840.0f, screen.y / 2160.0f);
const ImVec2 size {3840.0f * fit, 2160.0f * fit};
const ImVec2 begin {(screen.x - size.x) * 0.5f, (screen.y - size.y) * 0.5f};
const ImVec2 end {begin.x + size.x, begin.y + size.y};
auto* draw = ImGui::GetBackgroundDrawList();
draw->AddRectFilled({0, 0}, screen, IM_COL32(0, 0, 0, static_cast<int>(255.0f * alpha)));
for (size_t i = 0; i < g_splash_paths.size(); ++i) {
draw->AddImage(
splash_dds[i]->Texture(), begin, end, {0, 0}, {1, 1},
IM_COL32(255, 255, 255, static_cast<int>(255.0f * alpha * (i == 0 ? 1.0f : logo))));
}
}
void EnsureVulkan(vk::Format format, uint32_t image_count) {
EnsureContext();
if (vulkan_initialized) {
@@ -1215,7 +1309,8 @@ struct SystemOverlay::Impl {
OverlaySnapshot snapshot;
const bool has_overlay = GetOverlaySnapshot(&snapshot);
const auto now = Clock::now();
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire)) {
if (!has_overlay && !g_trophy_notification_active.load(std::memory_order_acquire) &&
!g_splash_active.load(std::memory_order_acquire)) {
return false;
}
const auto prepared_session = snapshot.session;
@@ -1245,12 +1340,9 @@ struct SystemOverlay::Impl {
last_frame = now;
ImGui_ImplVulkan_NewFrame();
ImGui::NewFrame();
DrawSplash(frame_extent);
GetOverlaySnapshot(&snapshot);
if (snapshot.session != prepared_session) {
ImGui::EndFrame();
return false;
}
if (has_overlay) {
if (has_overlay && snapshot.session == prepared_session) {
if (snapshot.session.kind == OverlayKind::Dialog) {
DrawDialog(snapshot.dialog, frame_extent);
} else {
@@ -1264,6 +1356,11 @@ struct SystemOverlay::Impl {
}
void Record(vk::CommandBuffer command, vk::ImageView target) {
for (const auto& image: splash_dds) {
if (image) {
image->Upload(command);
}
}
vk::RenderingAttachmentInfo color {};
color.sType = vk::StructureType::eRenderingAttachmentInfo;
color.imageView = target;
@@ -1290,6 +1387,7 @@ struct SystemOverlay::Impl {
return;
}
ImGui::SetCurrentContext(imgui_context);
splash_dds = {};
ImGui_ImplVulkan_Shutdown();
vulkan_initialized = false;
}
@@ -1309,6 +1407,8 @@ struct SystemOverlay::Impl {
std::optional<TrophyNotification> trophy_notification;
Clock::time_point trophy_notification_born;
std::vector<std::unique_ptr<ImTextureData>> retired_trophy_images;
std::array<std::unique_ptr<DdsImage>, 2> splash_dds;
Clock::time_point splash_born;
SDL_AudioStream* trophy_sound_stream = nullptr;
uint8_t* trophy_sound_data = nullptr;
uint32_t trophy_sound_size = 0;
@@ -5,6 +5,7 @@
#include "graphics/host_gpu/vulkanCommon.h"
#include <cstddef>
#include <filesystem>
#include <memory>
#include <span>
#include <string_view>
@@ -21,6 +22,8 @@ struct SystemOverlayVisualState {
uint64_t revision;
};
void InitializeSplashScreen(const std::filesystem::path& sce_sys);
void HideSplashScreen();
void InitializeSystemOverlayInput(SDL_Window* window);
void ShutdownSystemOverlayInput();
bool ProcessSystemOverlayInput(const SDL_Event& event);
+7 -13
View File
@@ -566,29 +566,23 @@ Graphics::ImageInfo BufferAttributeGroup::ImageInfo(const VideoOutBuffer& buffer
"pixels unchanged (format=0x{:016x}).\n", attribute.pixel_format));
}
}
const auto tile_mode = Graphics::Prospero::TileMode::kRenderTarget;
const auto pitch =
Graphics::TileGetTexturePitch(pixel_format.guest_format, attribute.width, tile_mode);
Graphics::TileSizeAlign total {};
Graphics::TileGetTextureTotalSize(pixel_format.guest_format, attribute.width, attribute.height,
1, 1, tile_mode, false, total);
if (total.size == 0 || total.align != 65536 ||
(buffer.data_address & (total.align - 1u)) != 0) {
EXIT("invalid video-out surface footprint or alignment\n");
}
const auto tile_mode = Graphics::Prospero::TileMode::kRenderTarget;
Graphics::ImageInfo info {};
info.data = {buffer.data_address, total.size};
info.data = {buffer.data_address, 0};
info.pixel_format = pixel_format.format;
info.guest_format = pixel_format.guest_format;
info.type = Graphics::Prospero::ImageType::kColor2D;
info.extent = {attribute.width, attribute.height, 1};
info.resources = {1, 1};
info.pitch = pitch;
info.bytes_per_block = pixel_format.bytes_per_element;
info.samples = 1;
info.tile_mode = tile_mode;
info.bgra16 = pixel_format.bgra16;
info.mip_layout[0] = {0, total.size, pitch, attribute.height};
info.UpdateSize();
if (info.data.size == 0 || info.tiling.block.block_size != 65536 ||
(buffer.data_address & 65535u) != 0) {
EXIT("invalid video-out surface footprint or alignment\n");
}
if (compression != Graphics::VideoOutCompression::Uncompressed) {
Graphics::TileSizeAlign dcc_size {};
if (!Graphics::TileGetDccSize(attribute.width, attribute.height, 1,
@@ -326,6 +326,19 @@ static void GameEventController([[maybe_unused]] const EventController& f) {
const auto button = ControllerButtonToPadButton(f.button);
if (button != 0) {
Controller::SetButton(f.id, button, f.down);
} else if (f.button == SDL_GAMEPAD_BUTTON_BACK) {
// Pads without a touchpad (Switch, Xbox, ...) have no way to click it, which
// blocks games that require it. Use their Back/Select button as a centered click.
auto* pad = SDL_GetGamepadFromID(f.id);
if (pad != nullptr && SDL_GetNumGamepadTouchpads(pad) == 0) {
if (f.down) {
Controller::SetTouchPad(f.id, 0, true, 0.5f, 0.5f);
}
Controller::SetButton(f.id, Controller::PAD_BUTTON_TOUCH_PAD, f.down);
if (f.up) {
Controller::SetTouchPad(f.id, 0, false, 0.5f, 0.5f);
}
}
}
}
@@ -115,6 +115,16 @@ DppTargetLane EmitDppTargetLane(EmitterState& state, const IR::DppMoveFlags& fla
if (control == 0x141u) {
return EmitDppMirrorTargetLane(state, subid, true);
}
// row_share:N reads lane N of the caller's row of 16.
if (control >= 0x150u && control <= 0x15fu) {
const auto row = state.builder.AllocateId();
const auto target = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitwiseAnd, TypeU32(state), row, subid,
ConstantU32(state, 0xfffffff0u));
state.builder.AddFunction(spv::OpBitwiseOr, TypeU32(state), target, row,
ConstantU32(state, control & 0xfu));
return {target, ConstantBool(state, true)};
}
if (control >= 0x160u && control <= 0x16fu) {
const auto target = state.builder.AllocateId();
state.builder.AddFunction(spv::OpBitwiseXor, TypeU32(state), target, subid,
@@ -146,6 +146,15 @@ inline constexpr auto EmitFPOrdEqual32 = EmitFloatCompare32<spv::OpFOrdEqual>;
EMIT_NATIVE(FPOrdEqual64, OpFOrdEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdLessThanEqual64, OpFOrdLessThanEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdGreaterThanEqual64, OpFOrdGreaterThanEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdNotEqual64, OpFOrdNotEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdLessThan64, OpFOrdLessThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPOrdGreaterThan64, OpFOrdGreaterThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordEqual64, OpFUnordEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordNotEqual64, OpFUnordNotEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordLessThan64, OpFUnordLessThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordGreaterThan64, OpFUnordGreaterThan, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordLessThanEqual64, OpFUnordLessThanEqual, U1, uint32_t, uint32_t)
EMIT_NATIVE(FPUnordGreaterThanEqual64, OpFUnordGreaterThanEqual, U1, uint32_t, uint32_t)
inline constexpr auto EmitFPUnordEqual32 = EmitFloatCompare32<spv::OpFUnordEqual>;
inline constexpr auto EmitFPOrdNotEqual32 = EmitFloatCompare32<spv::OpFOrdNotEqual>;
inline constexpr auto EmitFPUnordNotEqual32 = EmitFloatCompare32<spv::OpFUnordNotEqual>;
@@ -158,6 +167,7 @@ inline constexpr auto EmitFPUnordLessThanEqual32 = EmitFloatCompare32<spv::OpFUn
inline constexpr auto EmitFPOrdGreaterThanEqual32 = EmitFloatCompare32<spv::OpFOrdGreaterThanEqual>;
inline constexpr auto EmitFPUnordGreaterThanEqual32 = EmitFloatCompare32<spv::OpFUnordGreaterThanEqual>;
EMIT_NATIVE(FPIsNan32, OpIsNan, U1, uint32_t)
EMIT_NATIVE(FPIsNan64, OpIsNan, U1, uint32_t)
inline constexpr auto EmitFPCmpClass32 = EmitClassMaskF32;
inline constexpr auto EmitFPCmpClass16 = EmitClassMaskF16;
EMIT_NATIVE(FPAdd32, OpFAdd, F32, uint32_t, uint32_t)
@@ -47,8 +47,12 @@ constexpr OpcodeMap SOP1_OPCODE_LIST[] = {
{0x0au, Opcode::S_WQM_B64},
{0x0bu, Opcode::S_BREV_B32},
{0x0cu, Opcode::S_BREV_B64},
{0x0du, Opcode::S_BCNT0_I32_B32},
{0x0eu, Opcode::S_BCNT0_I32_B64},
{0x0fu, Opcode::S_BCNT1_I32_B32},
{0x10u, Opcode::S_BCNT1_I32_B64},
{0x11u, Opcode::S_FF0_I32_B32},
{0x12u, Opcode::S_FF0_I32_B64},
{0x13u, Opcode::S_FF1_I32_B32},
{0x14u, Opcode::S_FF1_I32_B64},
{0x15u, Opcode::S_FLBIT_I32_B32},
@@ -112,7 +116,9 @@ constexpr OpcodeMap SOPP_OPCODE_LIST[] = {
{0x12u, Opcode::S_TRAP},
{0x16u, Opcode::S_TTRACEDATA},
{0x17u, Opcode::S_CBRANCH_CDBGSYS},
{0x18u, Opcode::S_CBRANCH_CDBGUSER},
{0x19u, Opcode::S_CBRANCH_CDBGSYS_OR_USER},
{0x1au, Opcode::S_CBRANCH_CDBGSYS_AND_USER},
{0x20u, Opcode::S_INST_PREFETCH},
{0x23u, Opcode::S_WAITCNT_DEPCTR},
};
@@ -186,7 +186,9 @@ bool IsConditionalBranch(Opcode opcode) {
switch (opcode) {
// Conditional shader debugging is disabled.
case Opcode::S_CBRANCH_CDBGSYS:
case Opcode::S_CBRANCH_CDBGSYS_OR_USER: return false;
case Opcode::S_CBRANCH_CDBGUSER:
case Opcode::S_CBRANCH_CDBGSYS_OR_USER:
case Opcode::S_CBRANCH_CDBGSYS_AND_USER: return false;
case Opcode::S_CBRANCH_SCC0:
case Opcode::S_CBRANCH_SCC1:
case Opcode::S_CBRANCH_VCCZ:
@@ -510,6 +512,10 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::S_SEXT_I32_I16:
case Opcode::S_BREV_B32:
case Opcode::S_BREV_B64:
case Opcode::S_BCNT0_I32_B32:
case Opcode::S_BCNT0_I32_B64:
case Opcode::S_FF0_I32_B32:
case Opcode::S_FF0_I32_B64:
case Opcode::S_BCNT1_I32_B32:
case Opcode::S_FLBIT_I32_B32:
case Opcode::S_FF1_I32_B32:
@@ -571,7 +577,9 @@ std::string InstructionToString(const Instruction& inst) {
case Opcode::S_CBRANCH_EXECZ:
case Opcode::S_CBRANCH_EXECNZ:
case Opcode::S_CBRANCH_CDBGSYS:
case Opcode::S_CBRANCH_CDBGUSER:
case Opcode::S_CBRANCH_CDBGSYS_OR_USER:
case Opcode::S_CBRANCH_CDBGSYS_AND_USER:
return WithUnsupportedReason(inst, fmt::format("0x{:08x}: {} 0x{:08x}", inst.pc,
magic_enum::enum_name(inst.opcode),
inst.branch_target));
@@ -50,8 +50,12 @@ enum class Opcode {
S_ABSDIFF_I32,
S_BREV_B32,
S_BREV_B64,
S_BCNT0_I32_B32,
S_BCNT0_I32_B64,
S_BCNT1_I32_B32,
S_BCNT1_I32_B64,
S_FF0_I32_B32,
S_FF0_I32_B64,
S_FF1_I32_B32,
S_FF1_I32_B64,
S_FLBIT_I32_B32,
@@ -374,10 +378,38 @@ enum class Opcode {
V_CMP_NEQ_F32,
V_CMP_NLT_F32,
V_CMP_TRU_F32,
V_CMP_F_F64,
V_CMP_LT_F64,
V_CMP_EQ_F64,
V_CMP_LE_F64,
V_CMP_GT_F64,
V_CMP_LG_F64,
V_CMP_GE_F64,
V_CMP_O_F64,
V_CMP_U_F64,
V_CMP_NGE_F64,
V_CMP_NLG_F64,
V_CMP_NGT_F64,
V_CMP_NLE_F64,
V_CMP_NEQ_F64,
V_CMP_NLT_F64,
V_CMP_TRU_F64,
V_CMPX_F_F64,
V_CMPX_LT_F64,
V_CMPX_EQ_F64,
V_CMPX_LE_F64,
V_CMPX_GT_F64,
V_CMPX_LG_F64,
V_CMPX_GE_F64,
V_CMPX_O_F64,
V_CMPX_U_F64,
V_CMPX_NGE_F64,
V_CMPX_NLG_F64,
V_CMPX_NGT_F64,
V_CMPX_NLE_F64,
V_CMPX_NEQ_F64,
V_CMPX_NLT_F64,
V_CMPX_TRU_F64,
V_CMPX_LT_F32,
V_CMPX_EQ_F32,
V_CMPX_LE_F32,
@@ -459,11 +491,14 @@ enum class Opcode {
V_CMP_NE_U32,
V_CMP_GE_U32,
V_CMP_T_U32,
// Keep V_CMP_F_I64 to V_CMPX_T_U64 contiguous: VectorAluOps.cpp tests this range.
V_CMP_F_I64,
V_CMP_EQ_I64,
V_CMP_LT_I64,
V_CMP_EQ_I64,
V_CMP_LE_I64,
V_CMP_GT_I64,
V_CMP_NE_I64,
V_CMP_GE_I64,
V_CMP_T_I64,
V_CMP_F_U64,
V_CMP_LT_U64,
@@ -477,7 +512,9 @@ enum class Opcode {
V_CMPX_LT_I64,
V_CMPX_EQ_I64,
V_CMPX_LE_I64,
V_CMPX_GT_I64,
V_CMPX_NE_I64,
V_CMPX_GE_I64,
V_CMPX_T_I64,
V_CMPX_F_U64,
V_CMPX_LT_U64,
@@ -677,7 +714,9 @@ enum class Opcode {
S_CBRANCH_EXECZ,
S_CBRANCH_EXECNZ,
S_CBRANCH_CDBGSYS,
S_CBRANCH_CDBGUSER,
S_CBRANCH_CDBGSYS_OR_USER,
S_CBRANCH_CDBGSYS_AND_USER,
S_SENDMSG,
S_GETREG_B32,
S_SETREG_B32,
@@ -39,7 +39,7 @@ constexpr Vop2OpcodeInfo VOP2_OPCODE_LIST[] = {
{0x0bu, Opcode::V_MUL_U32_U24, Vop2SdwaProfile::IntegerPartialDestination},
{0x0fu, Opcode::V_MIN_F32},
{0x10u, Opcode::V_MAX_F32},
{0x11u, Opcode::V_MIN_I32},
{0x11u, Opcode::V_MIN_I32, Vop2SdwaProfile::IntegerFullDestination},
{0x12u, Opcode::V_MAX_I32, Vop2SdwaProfile::IntegerFullDestination},
{0x13u, Opcode::V_MIN_U32, Vop2SdwaProfile::IntegerPartialDestination},
{0x14u, Opcode::V_MAX_U32, Vop2SdwaProfile::IntegerFullDestination},
@@ -236,8 +236,23 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0x19u, Opcode::V_CMPX_NGE_F32}, {0x1au, Opcode::V_CMPX_NLG_F32},
{0x1bu, Opcode::V_CMPX_NGT_F32}, {0x1cu, Opcode::V_CMPX_NLE_F32},
{0x1du, Opcode::V_CMPX_NEQ_F32}, {0x1eu, Opcode::V_CMPX_NLT_F32},
{0x22u, Opcode::V_CMP_EQ_F64, false}, {0x23u, Opcode::V_CMP_LE_F64, false},
{0x33u, Opcode::V_CMPX_LE_F64, false}, {0x36u, Opcode::V_CMPX_GE_F64, false},
{0x20u, Opcode::V_CMP_F_F64, false}, {0x21u, Opcode::V_CMP_LT_F64, false},
{0x22u, Opcode::V_CMP_EQ_F64, false}, {0x23u, Opcode::V_CMP_LE_F64, false},
{0x24u, Opcode::V_CMP_GT_F64, false}, {0x25u, Opcode::V_CMP_LG_F64, false},
{0x26u, Opcode::V_CMP_GE_F64, false}, {0x27u, Opcode::V_CMP_O_F64, false},
{0x28u, Opcode::V_CMP_U_F64, false}, {0x29u, Opcode::V_CMP_NGE_F64, false},
{0x2au, Opcode::V_CMP_NLG_F64, false}, {0x2bu, Opcode::V_CMP_NGT_F64, false},
{0x2cu, Opcode::V_CMP_NLE_F64, false}, {0x2du, Opcode::V_CMP_NEQ_F64, false},
{0x2eu, Opcode::V_CMP_NLT_F64, false}, {0x2fu, Opcode::V_CMP_TRU_F64, false},
{0x30u, Opcode::V_CMPX_F_F64, false},
{0x31u, Opcode::V_CMPX_LT_F64, false}, {0x32u, Opcode::V_CMPX_EQ_F64, false},
{0x33u, Opcode::V_CMPX_LE_F64, false}, {0x34u, Opcode::V_CMPX_GT_F64, false},
{0x35u, Opcode::V_CMPX_LG_F64, false}, {0x36u, Opcode::V_CMPX_GE_F64, false},
{0x37u, Opcode::V_CMPX_O_F64, false}, {0x38u, Opcode::V_CMPX_U_F64, false},
{0x39u, Opcode::V_CMPX_NGE_F64, false}, {0x3au, Opcode::V_CMPX_NLG_F64, false},
{0x3bu, Opcode::V_CMPX_NGT_F64, false}, {0x3cu, Opcode::V_CMPX_NLE_F64, false},
{0x3du, Opcode::V_CMPX_NEQ_F64, false}, {0x3eu, Opcode::V_CMPX_NLT_F64, false},
{0x3fu, Opcode::V_CMPX_TRU_F64, false},
{0x80u, Opcode::V_CMP_F_I32}, {0x81u, Opcode::V_CMP_LT_I32},
{0x82u, Opcode::V_CMP_EQ_I32}, {0x83u, Opcode::V_CMP_LE_I32},
{0x84u, Opcode::V_CMP_GT_I32}, {0x85u, Opcode::V_CMP_NE_I32},
@@ -263,25 +278,26 @@ constexpr VopcOpcodeInfo VOPC_OPCODE_LIST[] = {
{0xc1u, Opcode::V_CMP_LT_U32}, {0xc2u, Opcode::V_CMP_EQ_U32},
{0xc3u, Opcode::V_CMP_LE_U32}, {0xc4u, Opcode::V_CMP_GT_U32},
{0xc5u, Opcode::V_CMP_NE_U32}, {0xc6u, Opcode::V_CMP_GE_U32},
{0xc7u, Opcode::V_CMP_T_U32}, {0xa2u, Opcode::V_CMP_EQ_I64, false},
{0xa1u, Opcode::V_CMP_LT_I64, false}, {0xa3u, Opcode::V_CMP_LE_I64, false},
{0xa5u, Opcode::V_CMP_NE_I64, false},
{0xa0u, Opcode::V_CMP_F_I64, false}, {0xa7u, Opcode::V_CMP_T_I64, false},
{0xb0u, Opcode::V_CMPX_F_I64, false}, {0xb1u, Opcode::V_CMPX_LT_I64, false},
{0xb2u, Opcode::V_CMPX_EQ_I64, false}, {0xb3u, Opcode::V_CMPX_LE_I64, false},
{0xb7u, Opcode::V_CMPX_T_I64, false},
{0xb5u, Opcode::V_CMPX_NE_I64, false}, {0xd1u, Opcode::V_CMPX_LT_U32},
{0xc7u, Opcode::V_CMP_T_U32}, {0xd1u, Opcode::V_CMPX_LT_U32},
{0xd2u, Opcode::V_CMPX_EQ_U32}, {0xd3u, Opcode::V_CMPX_LE_U32},
{0xd4u, Opcode::V_CMPX_GT_U32}, {0xd5u, Opcode::V_CMPX_NE_U32},
{0xd6u, Opcode::V_CMPX_GE_U32}, {0xe1u, Opcode::V_CMP_LT_U64, false},
{0xe0u, Opcode::V_CMP_F_U64, false}, {0xe7u, Opcode::V_CMP_T_U64, false},
{0xf0u, Opcode::V_CMPX_F_U64, false}, {0xf1u, Opcode::V_CMPX_LT_U64, false},
{0xf2u, Opcode::V_CMPX_EQ_U64, false}, {0xf4u, Opcode::V_CMPX_GT_U64, false},
{0xf6u, Opcode::V_CMPX_GE_U64, false}, {0xf7u, Opcode::V_CMPX_T_U64, false},
{0xe2u, Opcode::V_CMP_EQ_U64, false}, {0xe3u, Opcode::V_CMP_LE_U64, false},
{0xe4u, Opcode::V_CMP_GT_U64, false}, {0xe5u, Opcode::V_CMP_NE_U64, false},
{0xe6u, Opcode::V_CMP_GE_U64, false}, {0xf3u, Opcode::V_CMPX_LE_U64, false},
{0xf5u, Opcode::V_CMPX_NE_U64, false}, {0xc9u, Opcode::V_CMP_LT_F16},
{0xd6u, Opcode::V_CMPX_GE_U32}, {0xa0u, Opcode::V_CMP_F_I64, false},
{0xa1u, Opcode::V_CMP_LT_I64, false}, {0xa2u, Opcode::V_CMP_EQ_I64, false},
{0xa3u, Opcode::V_CMP_LE_I64, false}, {0xa4u, Opcode::V_CMP_GT_I64, false},
{0xa5u, Opcode::V_CMP_NE_I64, false}, {0xa6u, Opcode::V_CMP_GE_I64, false},
{0xa7u, Opcode::V_CMP_T_I64, false}, {0xb0u, Opcode::V_CMPX_F_I64, false},
{0xb1u, Opcode::V_CMPX_LT_I64, false}, {0xb2u, Opcode::V_CMPX_EQ_I64, false},
{0xb3u, Opcode::V_CMPX_LE_I64, false}, {0xb4u, Opcode::V_CMPX_GT_I64, false},
{0xb5u, Opcode::V_CMPX_NE_I64, false}, {0xb6u, Opcode::V_CMPX_GE_I64, false},
{0xb7u, Opcode::V_CMPX_T_I64, false}, {0xe0u, Opcode::V_CMP_F_U64, false},
{0xe1u, Opcode::V_CMP_LT_U64, false}, {0xe2u, Opcode::V_CMP_EQ_U64, false},
{0xe3u, Opcode::V_CMP_LE_U64, false}, {0xe4u, Opcode::V_CMP_GT_U64, false},
{0xe5u, Opcode::V_CMP_NE_U64, false}, {0xe6u, Opcode::V_CMP_GE_U64, false},
{0xe7u, Opcode::V_CMP_T_U64, false}, {0xf0u, Opcode::V_CMPX_F_U64, false},
{0xf1u, Opcode::V_CMPX_LT_U64, false}, {0xf2u, Opcode::V_CMPX_EQ_U64, false},
{0xf3u, Opcode::V_CMPX_LE_U64, false}, {0xf4u, Opcode::V_CMPX_GT_U64, false},
{0xf5u, Opcode::V_CMPX_NE_U64, false}, {0xf6u, Opcode::V_CMPX_GE_U64, false},
{0xf7u, Opcode::V_CMPX_T_U64, false}, {0xc9u, Opcode::V_CMP_LT_F16},
{0xcau, Opcode::V_CMP_EQ_F16}, {0xcbu, Opcode::V_CMP_LE_F16},
{0xccu, Opcode::V_CMP_GT_F16}, {0xcdu, Opcode::V_CMP_LG_F16},
{0xceu, Opcode::V_CMP_GE_F16}, {0xe9u, Opcode::V_CMP_NGE_F16},
@@ -760,9 +776,10 @@ void DecodeVop1Sdwa(uint32_t pc, std::span<const uint32_t> code, uint32_t word_i
void ApplyDppModifier(Operand& operand, uint32_t modifier, uint32_t encoding) {
operand.dpp = true;
if (encoding == 233u) {
operand.dpp8 = true;
operand.dpp_ctrl = modifier >> 8u;
if (encoding == 233u || encoding == 234u) {
operand.dpp8 = true;
operand.dpp_ctrl = modifier >> 8u;
operand.dpp_fetch_inactive = encoding == 234u;
return;
}
operand.negate = ((modifier >> 20u) & 0x1u) != 0u;
@@ -879,10 +896,38 @@ bool IsVopcFloatCompareOpcode(Opcode opcode) {
case Opcode::V_CMP_F_F32:
case Opcode::V_CMP_LT_F32:
case Opcode::V_CMP_EQ_F32:
case Opcode::V_CMP_F_F64:
case Opcode::V_CMP_LT_F64:
case Opcode::V_CMP_EQ_F64:
case Opcode::V_CMP_LE_F64:
case Opcode::V_CMP_GT_F64:
case Opcode::V_CMP_LG_F64:
case Opcode::V_CMP_GE_F64:
case Opcode::V_CMP_O_F64:
case Opcode::V_CMP_U_F64:
case Opcode::V_CMP_NGE_F64:
case Opcode::V_CMP_NLG_F64:
case Opcode::V_CMP_NGT_F64:
case Opcode::V_CMP_NLE_F64:
case Opcode::V_CMP_NEQ_F64:
case Opcode::V_CMP_NLT_F64:
case Opcode::V_CMP_TRU_F64:
case Opcode::V_CMPX_F_F64:
case Opcode::V_CMPX_LT_F64:
case Opcode::V_CMPX_EQ_F64:
case Opcode::V_CMPX_LE_F64:
case Opcode::V_CMPX_GT_F64:
case Opcode::V_CMPX_LG_F64:
case Opcode::V_CMPX_GE_F64:
case Opcode::V_CMPX_O_F64:
case Opcode::V_CMPX_U_F64:
case Opcode::V_CMPX_NGE_F64:
case Opcode::V_CMPX_NLG_F64:
case Opcode::V_CMPX_NGT_F64:
case Opcode::V_CMPX_NLE_F64:
case Opcode::V_CMPX_NEQ_F64:
case Opcode::V_CMPX_NLT_F64:
case Opcode::V_CMPX_TRU_F64:
case Opcode::V_CMP_LE_F32:
case Opcode::V_CMP_GT_F32:
case Opcode::V_CMP_LG_F32:
@@ -1202,10 +1247,10 @@ VopcSdwaFields DecodeVopcSdwaFields(uint32_t modifier) {
}
bool SupportsVopcSdwa(Opcode opcode) {
return opcode != Opcode::UNSUPPORTED && opcode != Opcode::V_CMP_NE_I64 &&
opcode != Opcode::V_CMP_EQ_F64 &&
opcode != Opcode::V_CMP_LE_F64 && opcode != Opcode::V_CMPX_LE_F64 &&
opcode != Opcode::V_CMPX_GE_F64;
// SDWA operand handling is not implemented for 64-bit compares.
return opcode != Opcode::UNSUPPORTED &&
(opcode < Opcode::V_CMP_F_F64 || opcode > Opcode::V_CMPX_TRU_F64) &&
(opcode < Opcode::V_CMP_F_I64 || opcode > Opcode::V_CMPX_T_U64);
}
void DecodeVopcSdwa(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index,
@@ -1538,8 +1583,22 @@ void ApplyNativeVop3SourceModifiers(Instruction& inst, uint32_t abs, uint32_t ne
bool IsVopcCompareExec(Opcode opcode) {
switch (opcode) {
case Opcode::V_CMPX_F_F64:
case Opcode::V_CMPX_LT_F64:
case Opcode::V_CMPX_EQ_F64:
case Opcode::V_CMPX_LE_F64:
case Opcode::V_CMPX_GT_F64:
case Opcode::V_CMPX_LG_F64:
case Opcode::V_CMPX_GE_F64:
case Opcode::V_CMPX_O_F64:
case Opcode::V_CMPX_U_F64:
case Opcode::V_CMPX_NGE_F64:
case Opcode::V_CMPX_NLG_F64:
case Opcode::V_CMPX_NGT_F64:
case Opcode::V_CMPX_NLE_F64:
case Opcode::V_CMPX_NEQ_F64:
case Opcode::V_CMPX_NLT_F64:
case Opcode::V_CMPX_TRU_F64:
case Opcode::V_CMPX_LT_F32:
case Opcode::V_CMPX_EQ_F32:
case Opcode::V_CMPX_LE_F32:
@@ -1572,7 +1631,9 @@ bool IsVopcCompareExec(Opcode opcode) {
case Opcode::V_CMPX_LT_I64:
case Opcode::V_CMPX_EQ_I64:
case Opcode::V_CMPX_LE_I64:
case Opcode::V_CMPX_GT_I64:
case Opcode::V_CMPX_NE_I64:
case Opcode::V_CMPX_GE_I64:
case Opcode::V_CMPX_T_I64:
case Opcode::V_CMPX_F_U64:
case Opcode::V_CMPX_LT_U64:
@@ -1626,7 +1687,8 @@ void DecodeVop2(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
return;
}
switch (src0) {
case 233u: DecodeVop2Dpp(pc, code, word_index, opcode, vdst, vsrc1, inst); return;
case 233u:
case 234u: DecodeVop2Dpp(pc, code, word_index, opcode, vdst, vsrc1, inst); return;
case 249u: DecodeVop2Sdwa(pc, code, word_index, opcode, vdst, vsrc1, inst); return;
case 250u: DecodeVop2Dpp(pc, code, word_index, opcode, vdst, vsrc1, inst); return;
default: break;
@@ -1661,7 +1723,8 @@ void DecodeVop1(uint32_t pc, std::span<const uint32_t> code, uint32_t word_index
return;
}
switch (src0) {
case 233u: DecodeVop1Dpp(pc, code, word_index, opcode, vdst, inst); return;
case 233u:
case 234u: DecodeVop1Dpp(pc, code, word_index, opcode, vdst, inst); return;
case 249u: DecodeVop1Sdwa(pc, code, word_index, opcode, vdst, inst); return;
case 250u: DecodeVop1Dpp(pc, code, word_index, opcode, vdst, inst); return;
default: break;
@@ -1,6 +1,7 @@
#include "graphics/shader/recompiler/frontend/translate/Translator.h"
#include <bit>
#include <utility>
namespace Libs::Graphics::ShaderRecompiler::Frontend {
@@ -27,7 +28,8 @@ void Translator::EmitCompareConstant(const Decoder::Instruction& inst, bool valu
}
void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
IR::Type type, bool scalar, bool cmpx) {
IR::Type type, bool scalar, bool cmpx, bool swap_operands) {
// The equality compares share their IR opcode with the unsigned ones.
const bool signed_64 = opcode == IR::ValueOpcode::SLessThan64 ||
opcode == IR::ValueOpcode::SLessThanEqual64 ||
inst.opcode == Decoder::Opcode::V_CMP_EQ_I64 ||
@@ -42,8 +44,12 @@ void Translator::EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueO
}
return ReadOperand(operand, type);
};
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {read(inst.src0), read(inst.src1)})), scalar,
cmpx);
auto lhs = read(inst.src0);
auto rhs = read(inst.src1);
if (swap_operands) {
std::swap(lhs, rhs);
}
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs})), scalar, cmpx);
}
void Translator::EmitInteger16Compare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
@@ -65,11 +71,14 @@ void Translator::EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpc
EmitCompareResult(inst, IR::U1(ir.Emit(opcode, {lhs, rhs}, flags)), false, cmpx);
}
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx) {
const auto lhs = IR::F32(ReadOperand(inst.src0, IR::Type::F32));
const auto rhs = IR::F32(ReadOperand(inst.src1, IR::Type::F32));
const auto unordered = ir.LogicalOr(IR::U1(ir.Emit(IR::ValueOpcode::FPIsNan32, {lhs})),
IR::U1(ir.Emit(IR::ValueOpcode::FPIsNan32, {rhs})));
void Translator::EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
bool is64) {
const auto type = is64 ? IR::Type::F64 : IR::Type::F32;
const auto is_nan_op = is64 ? IR::ValueOpcode::FPIsNan64 : IR::ValueOpcode::FPIsNan32;
const auto lhs = ReadOperand(inst.src0, type);
const auto rhs = ReadOperand(inst.src1, type);
const auto unordered = ir.LogicalOr(IR::U1(ir.Emit(is_nan_op, {lhs})),
IR::U1(ir.Emit(is_nan_op, {rhs})));
EmitCompareResult(inst, ordered ? ir.LogicalNot(unordered) : unordered, false, cmpx);
}
@@ -221,8 +221,12 @@ void Translator::V_XOR3_B32(const Decoder::Instruction& inst) {
WriteOperand(DestinationOperand(inst), result);
}
void Translator::S_FF1_I32_B64(const Decoder::Instruction& inst) {
const auto source = ExtractU64(ReadU64(inst.src0));
void Translator::S_FF_I32_B64(const Decoder::Instruction& inst, bool find_zero) {
auto source = ReadU32Pair(inst.src0);
if (find_zero) {
source[0] = ir.BitwiseNot(source[0]);
source[1] = ir.BitwiseNot(source[1]);
}
const auto low_lsb = IR::U32(ir.Emit(IR::ValueOpcode::FindILsb32, {source[0]}));
const auto high_lsb = IR::U32(ir.Emit(IR::ValueOpcode::FindILsb32, {source[1]}));
const auto high_position = ir.IAdd(high_lsb, IR::U32(IR::Value(32u)));
@@ -178,12 +178,29 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
IR::U32(ir.Emit(IR::ValueOpcode::BitReverse32, {value[0]}))});
return;
}
case O::S_BCNT0_I32_B32:
case O::S_FF0_I32_B32: {
const auto value = ir.BitwiseNot(ReadU32(inst.src0));
const bool count = inst.opcode == O::S_BCNT0_I32_B32;
const auto result = IR::U32(ir.Emit(count ? IR::ValueOpcode::BitCount32
: IR::ValueOpcode::FindILsb32, {value}));
WriteOperand(inst.dst, result);
if (count) {
ir.SetScc(ir.INotEqual(result, IR::U32(IR::Value(0u))));
}
return;
}
case O::S_BCNT1_I32_B32:
return SimpleInteger(inst, IR::ValueOpcode::BitCount32, IR::Type::U32, false, false,
true);
case O::S_BCNT0_I32_B64:
case O::S_BCNT1_I32_B64: {
// Vulkan bit counts operate on 32-bit words; avoid packing only to split again.
const auto value = ReadU32Pair(inst.src0);
auto value = ReadU32Pair(inst.src0);
if (inst.opcode == O::S_BCNT0_I32_B64) {
value[0] = ir.BitwiseNot(value[0]);
value[1] = ir.BitwiseNot(value[1]);
}
const auto low = IR::U32(ir.Emit(IR::ValueOpcode::BitCount32, {value[0]}));
const auto high = IR::U32(ir.Emit(IR::ValueOpcode::BitCount32, {value[1]}));
const auto result = ir.IAdd(low, high);
@@ -221,7 +238,8 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
return ComposedIntegerBinary(inst, IR::ValueOpcode::BitwiseOr32, false, true, true);
case O::S_XNOR_B32:
return ComposedIntegerBinary(inst, IR::ValueOpcode::BitwiseXor32, false, true, true);
case O::S_FF1_I32_B64: return S_FF1_I32_B64(inst);
case O::S_FF0_I32_B64: return S_FF_I32_B64(inst, true);
case O::S_FF1_I32_B64: return S_FF_I32_B64(inst, false);
case O::S_FLBIT_I32_B32: return V_FFBH_32(inst, false);
case O::S_FLBIT_I32_B64: return S_FLBIT_I32_B64(inst);
@@ -261,7 +279,9 @@ void Translator::EmitScalar(const Decoder::Instruction& inst) {
case O::S_CBRANCH_EXECZ:
case O::S_CBRANCH_EXECNZ:
case O::S_CBRANCH_CDBGSYS:
case O::S_CBRANCH_CDBGUSER:
case O::S_CBRANCH_CDBGSYS_OR_USER:
case O::S_CBRANCH_CDBGSYS_AND_USER:
case O::S_ENDPGM: return;
default: return FailMissingTranslation(inst);
}
@@ -875,30 +875,70 @@ void IncludeInstructionVectorRegisters(const Decoder::Instruction& inst, uint32_
case Decoder::Opcode::V_FLOOR_F64:
case Decoder::Opcode::V_FRACT_F64: include_vector(inst.dst, 2u); [[fallthrough]];
case Decoder::Opcode::V_CVT_F32_F64: include_vector(inst.src0, 2u); break;
case Decoder::Opcode::V_CMP_F_F64:
case Decoder::Opcode::V_CMP_LT_F64:
case Decoder::Opcode::V_CMP_EQ_F64:
case Decoder::Opcode::V_CMP_LE_F64:
case Decoder::Opcode::V_CMP_GT_F64:
case Decoder::Opcode::V_CMP_LG_F64:
case Decoder::Opcode::V_CMP_GE_F64:
case Decoder::Opcode::V_CMP_O_F64:
case Decoder::Opcode::V_CMP_U_F64:
case Decoder::Opcode::V_CMP_NGE_F64:
case Decoder::Opcode::V_CMP_NLG_F64:
case Decoder::Opcode::V_CMP_NGT_F64:
case Decoder::Opcode::V_CMP_NLE_F64:
case Decoder::Opcode::V_CMP_NEQ_F64:
case Decoder::Opcode::V_CMP_NLT_F64:
case Decoder::Opcode::V_CMP_TRU_F64:
case Decoder::Opcode::V_CMPX_F_F64:
case Decoder::Opcode::V_CMPX_LT_F64:
case Decoder::Opcode::V_CMPX_EQ_F64:
case Decoder::Opcode::V_CMPX_LE_F64:
case Decoder::Opcode::V_CMPX_GT_F64:
case Decoder::Opcode::V_CMPX_LG_F64:
case Decoder::Opcode::V_CMPX_GE_F64:
case Decoder::Opcode::V_CMP_EQ_I64:
case Decoder::Opcode::V_CMPX_O_F64:
case Decoder::Opcode::V_CMPX_U_F64:
case Decoder::Opcode::V_CMPX_NGE_F64:
case Decoder::Opcode::V_CMPX_NLG_F64:
case Decoder::Opcode::V_CMPX_NGT_F64:
case Decoder::Opcode::V_CMPX_NLE_F64:
case Decoder::Opcode::V_CMPX_NEQ_F64:
case Decoder::Opcode::V_CMPX_NLT_F64:
case Decoder::Opcode::V_CMPX_TRU_F64:
case Decoder::Opcode::V_CMP_F_I64:
case Decoder::Opcode::V_CMP_LT_I64:
case Decoder::Opcode::V_CMP_EQ_I64:
case Decoder::Opcode::V_CMP_LE_I64:
case Decoder::Opcode::V_CMP_GT_I64:
case Decoder::Opcode::V_CMP_NE_I64:
case Decoder::Opcode::V_CMP_GE_I64:
case Decoder::Opcode::V_CMP_T_I64:
case Decoder::Opcode::V_CMP_F_U64:
case Decoder::Opcode::V_CMP_LT_U64:
case Decoder::Opcode::V_CMP_EQ_U64:
case Decoder::Opcode::V_CMP_LE_U64:
case Decoder::Opcode::V_CMP_GT_U64:
case Decoder::Opcode::V_CMP_NE_U64:
case Decoder::Opcode::V_CMP_GE_U64:
case Decoder::Opcode::V_CMP_T_U64:
case Decoder::Opcode::V_CMPX_F_I64:
case Decoder::Opcode::V_CMPX_LT_I64:
case Decoder::Opcode::V_CMPX_EQ_I64:
case Decoder::Opcode::V_CMPX_LE_I64:
case Decoder::Opcode::V_CMPX_GT_I64:
case Decoder::Opcode::V_CMPX_NE_I64:
case Decoder::Opcode::V_CMPX_GE_I64:
case Decoder::Opcode::V_CMPX_T_I64:
case Decoder::Opcode::V_CMPX_F_U64:
case Decoder::Opcode::V_CMPX_LT_U64:
case Decoder::Opcode::V_CMPX_EQ_U64:
case Decoder::Opcode::V_CMPX_LE_U64:
case Decoder::Opcode::V_CMPX_GT_U64:
case Decoder::Opcode::V_CMPX_NE_U64:
case Decoder::Opcode::V_CMPX_GE_U64:
case Decoder::Opcode::V_CMPX_T_U64:
include_vector(inst.src0, 2u);
include_vector(inst.src1, 2u);
break;
@@ -1123,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 =
@@ -1341,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),
@@ -122,12 +122,13 @@ private:
void EmitCompareResult(const Decoder::Instruction& inst, IR::U1 value, bool scalar, bool cmpx);
void EmitCompareConstant(const Decoder::Instruction& inst, bool value, bool scalar, bool cmpx);
void EmitIntegerCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, IR::Type type,
bool scalar, bool cmpx);
bool scalar, bool cmpx, bool swap_operands = false);
void EmitInteger16Compare(const Decoder::Instruction& inst, IR::ValueOpcode opcode,
bool signed_value, bool cmpx);
void EmitFloatCompare(const Decoder::Instruction& inst, IR::ValueOpcode opcode, bool half,
bool cmpx);
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx);
void EmitFloatOrderedCompare(const Decoder::Instruction& inst, bool ordered, bool cmpx,
bool is64 = false);
void EmitFloatClassCompare(const Decoder::Instruction& inst, bool cmpx, bool half = false);
void V_CVT_F32_UBYTE(const Decoder::Instruction& inst, uint32_t byte_index);
void V_CVT_F32_U32(const Decoder::Instruction& inst);
@@ -186,7 +187,7 @@ private:
void V_AND_OR_B32(const Decoder::Instruction& inst);
void V_OR3_B32(const Decoder::Instruction& inst);
void V_XOR3_B32(const Decoder::Instruction& inst);
void S_FF1_I32_B64(const Decoder::Instruction& inst);
void S_FF_I32_B64(const Decoder::Instruction& inst, bool find_zero);
void V_FFBH_32(const Decoder::Instruction& inst, bool sign);
void S_FLBIT_I32_B64(const Decoder::Instruction& inst);
void Integer24(const Decoder::Instruction& inst, bool sign, bool addend);
@@ -145,6 +145,23 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMPX_LE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true);
return;
// The IR has no signed 64-bit greater-than: a > b is b < a, and a >= b is b <= a.
case O::V_CMP_GT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, false,
true);
return;
case O::V_CMPX_GT_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThan64, IR::Type::U64, false, true,
true);
return;
case O::V_CMP_GE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, false,
true);
return;
case O::V_CMPX_GE_I64:
EmitIntegerCompare(inst, IR::ValueOpcode::SLessThanEqual64, IR::Type::U64, false, true,
true);
return;
case O::V_CMP_LE_U64:
EmitIntegerCompare(inst, IR::ValueOpcode::ULessThanEqual64, IR::Type::U64, false, false);
return;
@@ -242,6 +259,74 @@ void Translator::EmitVector(const Decoder::Instruction& inst) {
case O::V_CMPX_GE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, true);
return;
case O::V_CMP_F_F64: EmitCompareConstant(inst, false, false, false); return;
case O::V_CMP_TRU_F64: EmitCompareConstant(inst, true, false, false); return;
case O::V_CMPX_F_F64: EmitCompareConstant(inst, false, false, true); return;
case O::V_CMPX_TRU_F64: EmitCompareConstant(inst, true, false, true); return;
case O::V_CMP_LT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, false);
return;
case O::V_CMPX_LT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdLessThan64, false, true);
return;
case O::V_CMPX_EQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual64, false, true);
return;
case O::V_CMP_GT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, false);
return;
case O::V_CMPX_GT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThan64, false, true);
return;
case O::V_CMP_LG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, false);
return;
case O::V_CMPX_LG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdNotEqual64, false, true);
return;
case O::V_CMP_GE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdGreaterThanEqual64, false, false);
return;
case O::V_CMP_NLG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, false);
return;
case O::V_CMPX_NLG_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordEqual64, false, true);
return;
case O::V_CMP_NEQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, false);
return;
case O::V_CMPX_NEQ_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordNotEqual64, false, true);
return;
case O::V_CMP_NLE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, false);
return;
case O::V_CMPX_NLE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThan64, false, true);
return;
case O::V_CMP_NLT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, false);
return;
case O::V_CMPX_NLT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordGreaterThanEqual64, false, true);
return;
case O::V_CMP_NGE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, false);
return;
case O::V_CMPX_NGE_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThan64, false, true);
return;
case O::V_CMP_NGT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, false);
return;
case O::V_CMPX_NGT_F64:
EmitFloatCompare(inst, IR::ValueOpcode::FPUnordLessThanEqual64, false, true);
return;
case O::V_CMP_O_F64: EmitFloatOrderedCompare(inst, true, false, true); return;
case O::V_CMP_U_F64: EmitFloatOrderedCompare(inst, false, false, true); return;
case O::V_CMPX_O_F64: EmitFloatOrderedCompare(inst, true, true, true); return;
case O::V_CMPX_U_F64: EmitFloatOrderedCompare(inst, false, true, true); return;
case O::V_CMPX_EQ_F32:
EmitFloatCompare(inst, IR::ValueOpcode::FPOrdEqual32, false, true);
return;
@@ -162,6 +162,15 @@ VALUE_OPCODE(FPOrdEqual32, U1, F32, F32)
VALUE_OPCODE(FPOrdEqual64, U1, F64, F64)
VALUE_OPCODE(FPOrdLessThanEqual64, U1, F64, F64)
VALUE_OPCODE(FPOrdGreaterThanEqual64, U1, F64, F64)
VALUE_OPCODE(FPOrdNotEqual64, U1, F64, F64)
VALUE_OPCODE(FPOrdLessThan64, U1, F64, F64)
VALUE_OPCODE(FPOrdGreaterThan64, U1, F64, F64)
VALUE_OPCODE(FPUnordEqual64, U1, F64, F64)
VALUE_OPCODE(FPUnordNotEqual64, U1, F64, F64)
VALUE_OPCODE(FPUnordLessThan64, U1, F64, F64)
VALUE_OPCODE(FPUnordGreaterThan64, U1, F64, F64)
VALUE_OPCODE(FPUnordLessThanEqual64, U1, F64, F64)
VALUE_OPCODE(FPUnordGreaterThanEqual64, U1, F64, F64)
VALUE_OPCODE(FPUnordEqual32, U1, F32, F32)
VALUE_OPCODE(FPOrdNotEqual32, U1, F32, F32)
VALUE_OPCODE(FPUnordNotEqual32, U1, F32, F32)
@@ -174,6 +183,7 @@ VALUE_OPCODE(FPUnordLessThanEqual32, U1, F32, F32)
VALUE_OPCODE(FPOrdGreaterThanEqual32, U1, F32, F32)
VALUE_OPCODE(FPUnordGreaterThanEqual32, U1, F32, F32)
VALUE_OPCODE(FPIsNan32, U1, F32)
VALUE_OPCODE(FPIsNan64, U1, F64)
VALUE_OPCODE(FPCmpClass32, U1, F32, U32)
VALUE_OPCODE(FPCmpClass16, U1, U32, U32)
@@ -721,31 +721,7 @@ static std::vector<ResourceBlock> ResourceControlFlow(const Program& program, co
block.sources.erase(std::unique(block.sources.begin(), block.sources.end()),
block.sources.end());
}
// Only branches that can change future resource access belong in the host plan.
std::vector<uint8_t> resource_reachable(blocks.size());
std::vector<uint32_t> pending;
for (uint32_t i = 0; i < blocks.size(); ++i) {
if (!blocks[i].sources.empty() || !blocks[i].srt_reads.empty()) {
resource_reachable[i] = 1;
pending.push_back(i);
}
}
while (!pending.empty()) {
const auto index = pending.back();
pending.pop_back();
for (const auto* predecessor: program.blocks[index]->ImmPredecessors()) {
const auto found = indices.find(predecessor);
if (found == indices.end()) return {};
if (resource_reachable[found->second]) continue;
resource_reachable[found->second] = 1;
pending.push_back(found->second);
}
}
for (auto& block: blocks) {
block.condition = std::ranges::any_of(block.successors, [&](uint32_t successor) {
return resource_reachable[successor] != 0;
}) ? predicate(block.condition) : Value {};
}
for (auto& block: blocks) block.condition = predicate(block.condition);
if (std::ranges::none_of(
blocks, [](const ResourceBlock& block) { return !block.condition.IsEmpty(); })) {
return {};
@@ -807,6 +807,15 @@ private:
}
}
}
for (auto* block: m_program.blocks) {
for (auto& inst: *block) {
uint32_t index = 0;
if (inst.GetOpcode() == ValueOpcode::LoadAddressU32 &&
ScalarReadMemory(inst, index) != nullptr && inst.Arg(1).Resolve().IsImmediate() &&
ValidateRuntimeValue(m_program, Value(&inst)))
CollectScalarRead(Value(&inst), inst.Flags<MemoryFlags>().pc);
}
}
for (auto* read: m_scalar_reads) {
const auto flags = read->Flags<MemoryFlags>();
auto& memory = m_program.memory_info[flags.index];
+13 -1
View File
@@ -1,5 +1,6 @@
find_package(Qt6 COMPONENTS Concurrent Network Widgets REQUIRED)
add_subdirectory("${KYTY_SOURCE_DIR}/updater" "${CMAKE_CURRENT_BINARY_DIR}/updater")
set(CMAKE_AUTOUIC_SEARCH_PATHS forms)
set(CMAKE_INCLUDE_CURRENT_DIR ON)
@@ -42,7 +43,13 @@ elseif(LINUX)
set_target_properties(launcher PROPERTIES LINK_FLAGS "${KYTY_LD_OPTIONS}")
endif()
add_dependencies(launcher kyty_emulator)
add_custom_target(stage_updater
COMMAND ${CMAKE_COMMAND} -E make_directory $<TARGET_FILE_DIR:launcher>
COMMAND ${CMAKE_COMMAND} -E copy_if_different $<TARGET_FILE:kyty_updater>
$<TARGET_FILE_DIR:launcher>/$<TARGET_FILE_NAME:kyty_updater>
DEPENDS kyty_updater
VERBATIM)
add_dependencies(launcher kyty_emulator stage_updater)
if(KYTY_CLANG_CL)
add_custom_command(TARGET launcher POST_BUILD
@@ -96,6 +103,7 @@ install(TARGETS launcher
if(APPLE)
install(TARGETS kyty_emulator DESTINATION "KytyPS5.app/Contents/MacOS")
install(TARGETS kyty_updater DESTINATION "KytyPS5.app/Contents/MacOS")
install(FILES "${KYTY_TROPHY_UNLOCK_SOUND}" DESTINATION "KytyPS5.app/Contents/Resources/assets/sounds")
# Deploy and ad-hoc sign the Qt frameworks and plugins in the bundle.
find_program(KYTY_MACDEPLOYQT NAMES macdeployqt PATHS "${Qt6_DIR}/../../../bin" NO_DEFAULT_PATH)
@@ -112,6 +120,10 @@ if(APPLE)
")
endif()
if(NOT APPLE)
install(TARGETS kyty_updater RUNTIME DESTINATION .)
endif()
if(WIN32)
install(CODE "
execute_process(
+413 -384
View File
@@ -16,394 +16,423 @@
<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>
</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="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">
+41 -17
View File
@@ -1,12 +1,21 @@
#ifndef UPDATE_CHECKER_H
#define UPDATE_CHECKER_H
#include "updateRelease.h"
#include <QCryptographicHash>
#include <QFile>
#include <QNetworkAccessManager>
#include <QObject>
#include <QString>
#include <QUrl>
#include <QPointer>
#include <QProcess>
class QByteArray;
#include <functional>
#include <memory>
class QNetworkReply;
class QProgressDialog;
class QTemporaryDir;
class QWidget;
class UpdateChecker final: public QObject {
@@ -14,28 +23,43 @@ class UpdateChecker final: public QObject {
public:
explicit UpdateChecker(QWidget* parent);
~UpdateChecker() override;
[[nodiscard]] static bool IsSupported();
void Check(bool manual);
static void ShowPreviousResult(QWidget* parent);
void Check(bool manual);
void SetGameRunning(bool running) { m_game_running = running; }
[[nodiscard]] bool IsInstalling() const { return m_installing; }
signals:
void CheckingChanged(bool checking);
void InstallingChanged(bool installing);
private:
struct UpdateInfo;
void FetchRelease(const QUrl& url, std::function<void(UpdateRelease::Info)> done);
void CheckCurrent(const UpdateRelease::Info& current, bool manual);
void OfferUpdate();
void Download();
void ReadDownload(QNetworkReply* reply);
void PreparePackage();
void RunPreparation(const QString& program, const QStringList& arguments, bool validate);
void Restart();
void Finish(QString error = {}, bool report = true);
static UpdateInfo ParseUpdateInfo(const QByteArray& data);
void FetchUpdateInfo(const char* url, bool fallback, bool manual);
void ShowUpdateResult(const UpdateInfo& info, bool manual);
QWidget* m_parent = nullptr;
QNetworkAccessManager m_network;
bool m_checking_updates = false;
// Answer from the primary update feed, kept while the GitHub fallback feed is
// being consulted, so it can still be shown if the fallback request fails.
QString m_primary_tag;
QUrl m_primary_page_url;
QWidget* m_parent;
QNetworkAccessManager m_network;
QPointer<QNetworkReply> m_reply;
QPointer<QProgressDialog> m_progress;
QProcess m_prepare;
std::unique_ptr<QTemporaryDir> m_workspace;
QFile m_download;
QCryptographicHash m_hash {QCryptographicHash::Sha256};
UpdateRelease::Info m_release;
QString m_download_error;
bool m_busy = false;
bool m_installing = false;
bool m_game_running = false;
bool m_canceled = false;
};
#endif // UPDATE_CHECKER_H
+38
View File
@@ -0,0 +1,38 @@
#ifndef UPDATE_RELEASE_H
#define UPDATE_RELEASE_H
#include <QByteArray>
#include <QDateTime>
#include <QString>
#include <QUrl>
namespace UpdateRelease {
inline const QString LATEST_URL =
QStringLiteral("https://api.github.com/repos/KytyPS5/KytyPS5/releases/latest");
inline const QString TAG_URL =
QStringLiteral("https://api.github.com/repos/KytyPS5/KytyPS5/releases/tags/%1");
inline const QString PAGE_URL =
QStringLiteral("https://github.com/KytyPS5/KytyPS5/releases/tag/%1");
inline const QString DOWNLOAD_URL =
QStringLiteral("https://github.com/KytyPS5/KytyPS5/releases/download/%1/%2");
struct Info {
QString tag;
QUrl page_url;
QUrl download_url;
QByteArray sha256;
qint64 size = 0;
QDateTime published_at;
QString error;
};
enum class VersionRelation { Current, Newer, Older, Unknown };
[[nodiscard]] QString PlatformSuffix();
[[nodiscard]] Info Parse(const QByteArray& data, const QString& platform_suffix);
[[nodiscard]] VersionRelation Compare(const Info& latest, const Info& current);
} // namespace UpdateRelease
#endif // UPDATE_RELEASE_H
@@ -463,6 +463,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;
+8 -1
View File
@@ -136,6 +136,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
[this](const QString&) { m_update_checker->Check(true); });
connect(m_update_checker, &UpdateChecker::CheckingChanged, m_ui->check_updates_link,
&QLabel::setDisabled);
connect(m_update_checker, &UpdateChecker::InstallingChanged, this, &MainDialogPrivate::Update);
connect(m_ui->check_updates_on_startup, &QCheckBox::toggled, this, [this](bool checked) {
g_check_updates_on_startup = checked;
m_ui->widget->WriteSettings();
@@ -146,6 +147,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
});
connect(&m_process, &QProcess::stateChanged, this, [this](QProcess::ProcessState state) {
m_update_checker->SetGameRunning(state != QProcess::NotRunning);
if (state == QProcess::NotRunning) {
if (m_running_item != nullptr) {
m_running_item->SetRunning(false);
@@ -162,6 +164,7 @@ void MainDialogPrivate::Setup(MainDialog* main_dialog) {
}
void MainDialogPrivate::FindInterpreter() {
UpdateChecker::ShowPreviousResult(m_main_dialog);
QDir search_dir(QApplication::applicationDirPath());
m_interpreter = search_dir.absoluteFilePath(EMULATOR_EXE);
@@ -509,6 +512,9 @@ void MainDialogPrivate::ReadSettings(QSettings& s) {
}
void MainDialogPrivate::Run() {
if (m_update_checker->IsInstalling()) {
return;
}
m_running_item = m_ui->widget->GetSelectedItem();
if (m_running_item == nullptr) {
return;
@@ -526,7 +532,8 @@ void MainDialogPrivate::Run() {
void MainDialogPrivate::Update() {
const auto* item = m_ui->widget->GetSelectedItem();
bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr);
bool run_enabled = (m_process.state() == QProcess::NotRunning && item != nullptr &&
!m_update_checker->IsInstalling());
if (run_enabled) {
const auto& info = item->GetInfo();
+374 -99
View File
@@ -2,143 +2,418 @@
#include "kytyGitVersion.h"
#include <QByteArray>
#include <QAbstractButton>
#include <QApplication>
#include <QDebug>
#include <QDesktopServices>
#include <QJsonDocument>
#include <QJsonObject>
#include <QDir>
#include <QFileInfo>
#include <QMessageBox>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QString>
#include <QUrl>
#include <QWidget>
#include <QProcessEnvironment>
#include <QProgressDialog>
#include <QTemporaryDir>
namespace {
constexpr char DEFAULT_FEED_URL[] = "https://kytyps5.github.io/data/updates.json";
constexpr char FALLBACK_FEED_URL[] =
"https://api.github.com/repos/KytyPS5/KytyPS5/releases/latest";
constexpr qint64 MAX_METADATA_SIZE = 2 * 1024 * 1024;
QString LauncherRelativePath() {
#if defined(_WIN32)
return QStringLiteral("launcher.exe");
#elif defined(__APPLE__)
return QStringLiteral("KytyPS5.app/Contents/MacOS/KytyPS5");
#else
return QStringLiteral("launcher");
#endif
}
QString InstallDirectory() {
QDir dir(QCoreApplication::applicationDirPath());
#if defined(__APPLE__)
// Official macOS releases are a bundle; never update a flat development build.
if (!dir.cdUp() || !dir.cdUp() || dir.dirName() != QStringLiteral("KytyPS5.app") ||
!dir.cdUp()) {
return {};
}
#endif
return dir.absolutePath();
}
QString HelperName() {
#if defined(_WIN32)
return QStringLiteral("kyty_updater.exe");
#else
return QStringLiteral("kyty_updater");
#endif
}
QNetworkRequest Request(const QUrl& url) {
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
request.setRawHeader("User-Agent", "KytyPS5-Updater");
request.setTransferTimeout(30000);
return request;
}
QString TarProgram() {
#if defined(_WIN32)
// Use the OS tool, not a tar.exe from the download or current directory.
return QDir(qEnvironmentVariable("SystemRoot", QStringLiteral("C:/Windows")))
.filePath(QStringLiteral("System32/tar.exe"));
#else
return QStringLiteral("/usr/bin/tar");
#endif
}
} // namespace
struct UpdateChecker::UpdateInfo {
QString tag;
QUrl page_url;
QString error;
};
UpdateChecker::UpdateChecker(QWidget* parent): QObject(parent), m_parent(parent), m_network(this) {}
UpdateChecker::~UpdateChecker() {
if (m_reply) {
m_reply->disconnect(this);
m_reply->abort();
}
// Extraction must stop before QTemporaryDir removes its output.
m_prepare.disconnect(this);
if (m_prepare.state() != QProcess::NotRunning) {
m_prepare.kill();
m_prepare.waitForFinished();
}
}
bool UpdateChecker::IsSupported() {
#if defined(KYTY_OFFICIAL_BUILD) && defined(NDEBUG)
return !QString::fromLatin1(KYTY_GIT_HASH).endsWith(QStringLiteral("-dirty"));
return !QString::fromLatin1(KYTY_GIT_HASH).endsWith(QStringLiteral("-dirty")) &&
!UpdateRelease::PlatformSuffix().isEmpty();
#else
return false;
#endif
}
UpdateChecker::UpdateInfo UpdateChecker::ParseUpdateInfo(const QByteArray& data) {
const auto document = QJsonDocument::fromJson(data);
if (!document.isObject()) {
return {{}, {}, tr("Invalid update feed")};
}
const auto root = document.object();
UpdateInfo info {root.value(QStringLiteral("tag")).toString(),
QUrl(root.value(QStringLiteral("html_url")).toString()), {}};
if (info.tag.isEmpty()) {
info.tag = root.value(QStringLiteral("tag_name")).toString();
}
if (info.tag.isEmpty() || !info.page_url.isValid() ||
info.page_url.scheme() != QStringLiteral("https")) {
info.error = tr("Incomplete update feed");
}
return info;
}
void UpdateChecker::Check(bool manual) {
if (!IsSupported() || m_checking_updates) {
void UpdateChecker::ShowPreviousResult(QWidget* parent) {
const auto directory = InstallDirectory();
QFile result(QDir(directory).filePath(QStringLiteral(".kyty-update-result")));
if (directory.isEmpty() || !result.open(QIODevice::ReadOnly)) {
return;
}
m_checking_updates = true;
emit CheckingChanged(true);
FetchUpdateInfo(DEFAULT_FEED_URL, false, manual);
const auto status = result.readLine().trimmed();
auto message = QString::fromUtf8(result.read(64 * 1024)).trimmed();
result.close();
result.remove();
if (status != "success" && status != "failure") {
return;
}
const bool success = status == "success";
if (message.isEmpty()) {
message = success ? tr("The update was installed successfully.")
: tr("The update could not be installed.");
}
QMessageBox(success ? QMessageBox::Information : QMessageBox::Warning, tr("KytyPS5 Update"),
message, QMessageBox::Ok, parent)
.exec();
}
void UpdateChecker::FetchUpdateInfo(const char* url, bool fallback, bool manual) {
QNetworkRequest request(QUrl(QString::fromLatin1(url)));
request.setAttribute(QNetworkRequest::RedirectPolicyAttribute,
QNetworkRequest::NoLessSafeRedirectPolicy);
request.setRawHeader("User-Agent", "Kyty-Launcher");
void UpdateChecker::FetchRelease(const QUrl& url, std::function<void(UpdateRelease::Info)> done) {
auto request = Request(url);
request.setRawHeader("Accept", "application/vnd.github+json");
request.setRawHeader("X-GitHub-Api-Version", "2022-11-28");
request.setTransferTimeout(15000);
auto* reply = m_network.get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply, fallback, manual]() {
UpdateInfo info;
if (reply->error() == QNetworkReply::NoError) {
info = ParseUpdateInfo(reply->readAll());
} else {
m_reply = reply;
reply->setReadBufferSize(MAX_METADATA_SIZE + 1);
connect(reply, &QNetworkReply::readyRead, this, [reply]() {
if (reply->bytesAvailable() > MAX_METADATA_SIZE) {
reply->abort();
}
});
connect(reply, &QNetworkReply::finished, this, [this, reply, done = std::move(done)]() {
UpdateRelease::Info info;
if (reply->bytesAvailable() > MAX_METADATA_SIZE) {
info.error = tr("The update response is too large.");
} else if (reply->error() != QNetworkReply::NoError) {
info.error = reply->errorString();
} else {
info = UpdateRelease::Parse(reply->readAll(), UpdateRelease::PlatformSuffix());
}
m_reply = nullptr;
reply->deleteLater();
if (!fallback) {
const bool up_to_date =
info.error.isEmpty() && info.tag == QString::fromLatin1(KYTY_RELEASE_TAG);
if (!up_to_date) {
if (info.error.isEmpty()) {
// The primary feed is reachable but disagrees with the build tag
// (e.g. a stale mirror); remember its answer in case the fallback
// request also fails.
m_primary_tag = info.tag;
m_primary_page_url = info.page_url;
}
FetchUpdateInfo(FALLBACK_FEED_URL, true, manual);
return;
}
}
if (fallback && !info.error.isEmpty() && !m_primary_tag.isEmpty()) {
// The fallback feed failed, but the primary feed already returned a
// usable answer; prefer it over reporting a network error.
info = {m_primary_tag, m_primary_page_url, {}};
}
m_primary_tag.clear();
m_primary_page_url.clear();
m_checking_updates = false;
emit CheckingChanged(false);
ShowUpdateResult(info, manual);
done(info);
});
}
void UpdateChecker::ShowUpdateResult(const UpdateInfo& info, bool manual) {
if (!info.error.isEmpty()) {
qWarning() << "Could not check for updates:" << info.error;
if (manual) {
QMessageBox::warning(m_parent, tr("Update Check"),
tr("Could not check for updates:\n%1").arg(info.error));
}
void UpdateChecker::Check(bool manual) {
if (!IsSupported() || m_busy) {
return;
}
m_busy = true;
emit CheckingChanged(true);
FetchRelease(QUrl(UpdateRelease::LATEST_URL), [this, manual](UpdateRelease::Info latest) {
m_release = std::move(latest);
if (!m_release.published_at.isValid()) {
Finish(m_release.error, manual);
} else if (m_release.tag == QString::fromLatin1(KYTY_RELEASE_TAG)) {
CheckCurrent(m_release, manual);
} else {
// Date/hash tags cannot order releases published on the same day.
const auto tag = QUrl::toPercentEncoding(QString::fromLatin1(KYTY_RELEASE_TAG));
FetchRelease(QUrl(UpdateRelease::TAG_URL.arg(QString::fromLatin1(tag))),
[this, manual](const UpdateRelease::Info& current) {
CheckCurrent(current, manual);
});
}
});
}
const bool current = info.tag == QString::fromLatin1(KYTY_RELEASE_TAG);
if (current) {
if (manual) {
QMessageBox::information(m_parent, tr("Update Check"),
tr("You are using the latest version (%1).").arg(info.tag));
void UpdateChecker::CheckCurrent(const UpdateRelease::Info& current, bool manual) {
if (current.tag != QString::fromLatin1(KYTY_RELEASE_TAG) || !current.published_at.isValid()) {
Finish(tr("Could not verify the currently installed release. Please try again later."),
manual);
return;
}
const auto relation = UpdateRelease::Compare(m_release, current);
if (relation == UpdateRelease::VersionRelation::Newer) {
if (m_release.error.isEmpty()) {
OfferUpdate();
} else {
Finish(m_release.error, manual);
}
return;
}
const auto message =
tr("An update is available.\n\nCurrent: %1\nLatest: %2\n\n"
"Open the release page?")
.arg(QString::fromLatin1(KYTY_RELEASE_TAG), info.tag);
if (QMessageBox::question(m_parent, tr("KytyPS5 Update"), message,
QMessageBox::Open | QMessageBox::Cancel,
QMessageBox::Open) == QMessageBox::Open) {
QDesktopServices::openUrl(info.page_url);
if (relation == UpdateRelease::VersionRelation::Unknown) {
Finish(tr("Could not determine whether the available release is newer."), manual);
return;
}
Finish();
if (manual) {
QMessageBox::information(m_parent, tr("Update Check"),
tr("No newer release is available."));
}
}
void UpdateChecker::OfferUpdate() {
QMessageBox question(QMessageBox::Information, tr("KytyPS5 Update"),
tr("Version %1 is available.\n\nCurrent: %2\n\n"
"Download and install it now? The launcher will restart. Your settings "
"and games are kept.")
.arg(m_release.tag, QString::fromLatin1(KYTY_RELEASE_TAG)),
QMessageBox::Yes | QMessageBox::Open | QMessageBox::Cancel, m_parent);
question.button(QMessageBox::Yes)->setText(tr("Install Update"));
question.button(QMessageBox::Open)->setText(tr("Release Notes"));
question.setDefaultButton(QMessageBox::Yes);
const auto answer = question.exec();
if (answer == QMessageBox::Yes) {
Download();
return;
}
if (answer == QMessageBox::Open) {
QDesktopServices::openUrl(m_release.page_url);
}
Finish();
}
void UpdateChecker::Download() {
if (m_game_running) {
Finish(tr("Close the running game before installing an update."));
return;
}
const auto directory = InstallDirectory();
const auto helper = QDir(QCoreApplication::applicationDirPath()).filePath(HelperName());
if (directory.isEmpty() || !QFileInfo::exists(helper) || !QFileInfo::exists(TarProgram())) {
Finish(tr("This installation cannot update itself. Download the release from GitHub and "
"extract it manually."));
return;
}
m_workspace = std::make_unique<QTemporaryDir>(
QDir(directory).filePath(QStringLiteral(".kyty-update-XXXXXX")));
if (!m_workspace->isValid()) {
Finish(tr("The installation folder is not writable. Move KytyPS5 to a writable folder and "
"try again."));
return;
}
if (!QFile::copy(helper, m_workspace->filePath(HelperName())) ||
!QDir(m_workspace->path()).mkdir(QStringLiteral("payload"))) {
Finish(tr("Could not prepare the update folder."));
return;
}
m_download.setFileName(
m_workspace->filePath(QStringLiteral("package") + UpdateRelease::PlatformSuffix()));
if (!m_download.open(QIODevice::WriteOnly)) {
Finish(m_download.errorString());
return;
}
m_hash.reset();
m_download_error.clear();
m_canceled = false;
m_installing = true;
emit InstallingChanged(true);
m_progress = new QProgressDialog(tr("Downloading %1…").arg(m_release.tag), tr("Cancel"), 0, 100,
m_parent);
m_progress->setWindowTitle(tr("KytyPS5 Update"));
m_progress->setWindowModality(Qt::ApplicationModal);
m_progress->setAutoClose(false);
m_progress->setAutoReset(false);
m_progress->setMinimumDuration(0);
connect(m_progress, &QProgressDialog::canceled, this, [this]() {
m_canceled = true;
if (m_reply) {
m_reply->abort();
} else if (m_prepare.state() != QProcess::NotRunning) {
m_prepare.kill();
}
});
auto* reply = m_network.get(Request(m_release.download_url));
m_reply = reply;
reply->setReadBufferSize(1024 * 1024);
connect(reply, &QNetworkReply::readyRead, this, [this, reply]() { ReadDownload(reply); });
connect(reply, &QNetworkReply::downloadProgress, this, [this](qint64 received, qint64) {
if (m_progress) {
m_progress->setValue(
static_cast<int>(qMin<qint64>(99, received * 100 / m_release.size)));
}
});
connect(reply, &QNetworkReply::finished, this, [this, reply]() {
ReadDownload(reply);
const auto error = reply->error();
const auto error_text = reply->errorString();
m_reply = nullptr;
reply->deleteLater();
const auto size = m_download.size();
const bool flushed = m_download.flush();
m_download.close();
if (m_canceled) {
Finish();
} else if (!m_download_error.isEmpty()) {
Finish(m_download_error);
} else if (error != QNetworkReply::NoError) {
Finish(error_text);
} else if (!flushed || size != m_release.size || m_hash.result() != m_release.sha256) {
Finish(tr("The update failed its size or SHA-256 verification. Please try again."));
} else {
PreparePackage();
}
});
}
void UpdateChecker::ReadDownload(QNetworkReply* reply) {
const auto data = reply->readAll();
if (data.isEmpty() || m_canceled || !m_download_error.isEmpty()) {
return;
}
if (m_download.size() + data.size() > m_release.size) {
m_download_error = tr("The downloaded file is larger than the release asset.");
} else if (m_download.write(data) != data.size()) {
m_download_error = m_download.errorString();
} else {
m_hash.addData(data);
}
if (!m_download_error.isEmpty() && !reply->isFinished()) {
reply->abort();
}
}
void UpdateChecker::PreparePackage() {
m_progress->setLabelText(tr("Preparing the update…"));
m_progress->setRange(0, 0);
RunPreparation(TarProgram(),
{QStringLiteral("-xf"), m_download.fileName(), QStringLiteral("-C"),
m_workspace->filePath(QStringLiteral("payload")),
QStringLiteral("--no-same-owner"), QStringLiteral("--no-same-permissions")},
false);
}
void UpdateChecker::RunPreparation(const QString& program, const QStringList& arguments,
bool validate) {
m_prepare.disconnect(this);
m_prepare.setProgram(program);
m_prepare.setArguments(arguments);
m_prepare.setWorkingDirectory(m_workspace->path());
auto environment = QProcessEnvironment::systemEnvironment();
environment.remove(QStringLiteral("TAR_OPTIONS"));
m_prepare.setProcessEnvironment(environment);
connect(&m_prepare, &QProcess::errorOccurred, this, [this](QProcess::ProcessError error) {
if (error == QProcess::FailedToStart) {
Finish(m_prepare.errorString());
}
});
connect(&m_prepare, qOverload<int, QProcess::ExitStatus>(&QProcess::finished), this,
[this, validate](int exit_code, QProcess::ExitStatus status) {
if (m_canceled) {
Finish();
} else if (exit_code != 0 || status != QProcess::NormalExit) {
Finish(
tr("Could not prepare the update:\n%1")
.arg(QString::fromUtf8(m_prepare.readAllStandardError()).left(4096)));
} else if (!validate) {
RunPreparation(m_workspace->filePath(HelperName()),
{QStringLiteral("--validate"),
m_workspace->filePath(QStringLiteral("payload")),
InstallDirectory(), LauncherRelativePath()},
true);
} else {
Restart();
}
});
m_prepare.start();
}
void UpdateChecker::Restart() {
if (m_game_running || m_canceled) {
Finish(tr("The update was canceled. Close any running game before trying again."));
return;
}
m_progress->setLabelText(tr("Restarting to install the update…"));
m_progress->setCancelButton(nullptr);
QProcess helper;
helper.setProgram(m_workspace->filePath(HelperName()));
helper.setArguments({QStringLiteral("--install"),
m_workspace->filePath(QStringLiteral("payload")), InstallDirectory(),
LauncherRelativePath(),
QString::number(QCoreApplication::applicationPid()), QDir::currentPath()});
helper.setWorkingDirectory(m_workspace->path());
helper.setStandardOutputFile(m_workspace->filePath(QStringLiteral("helper.log")));
helper.setStandardErrorFile(m_workspace->filePath(QStringLiteral("helper.log")),
QIODevice::Append);
if (!helper.startDetached()) {
Finish(tr("Could not start the update installer."));
return;
}
m_workspace->setAutoRemove(false);
QApplication::quit();
}
void UpdateChecker::Finish(QString error, bool report) {
const auto release_page = m_release.page_url;
m_release = {};
m_download.close();
m_workspace.reset();
if (m_progress) {
m_progress->disconnect(this);
m_progress->hide();
m_progress->deleteLater();
m_progress = nullptr;
}
m_busy = false;
m_installing = false;
emit CheckingChanged(false);
emit InstallingChanged(false);
if (error.isEmpty()) {
return;
}
qWarning() << "KytyPS5 updater:" << error;
if (!report) {
return;
}
QMessageBox message(QMessageBox::Warning, tr("KytyPS5 Update"), error, QMessageBox::Close,
m_parent);
if (!release_page.isEmpty()) {
message.addButton(QMessageBox::Open);
message.button(QMessageBox::Open)->setText(tr("Open Release Page"));
}
if (message.exec() == QMessageBox::Open) {
QDesktopServices::openUrl(release_page);
}
}
+89
View File
@@ -0,0 +1,89 @@
#include "updateRelease.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QObject>
#include <QRegularExpression>
namespace UpdateRelease {
QString PlatformSuffix() {
#if defined(_WIN32) && (defined(_M_X64) || defined(__x86_64__))
return QStringLiteral("-Windows-x64.zip");
#elif defined(__linux__) && defined(__x86_64__)
return QStringLiteral("-Linux-x86_64.tar.gz");
#elif defined(__APPLE__) && defined(__x86_64__)
return QStringLiteral("-macOS-x86_64.zip");
#else
return {};
#endif
}
Info Parse(const QByteArray& data, const QString& platform_suffix) {
Info info;
const auto document = QJsonDocument::fromJson(data);
if (!document.isObject()) {
info.error = QObject::tr("Invalid GitHub release response.");
return info;
}
const auto root = document.object();
info.tag = root.value(QStringLiteral("tag_name")).toString();
const auto published_at =
QDateTime::fromString(root.value(QStringLiteral("published_at")).toString(), Qt::ISODate);
if (info.tag.isEmpty() || !published_at.isValid() ||
root.value(QStringLiteral("draft")).toBool() ||
root.value(QStringLiteral("prerelease")).toBool()) {
info.error = QObject::tr("The release is not a published stable release.");
return info;
}
info.published_at = published_at;
const auto encoded_tag = QString::fromLatin1(QUrl::toPercentEncoding(info.tag));
info.page_url = QUrl(PAGE_URL.arg(encoded_tag));
if (platform_suffix.isEmpty()) {
info.error = QObject::tr("Built-in updates are unavailable for this platform.");
return info;
}
const auto asset_name = info.tag + platform_suffix;
for (const auto& value: root.value(QStringLiteral("assets")).toArray()) {
const auto asset = value.toObject();
if (asset.value(QStringLiteral("name")).toString() != asset_name) {
continue;
}
const auto digest = asset.value(QStringLiteral("digest")).toString();
const auto size = asset.value(QStringLiteral("size")).toInteger();
static const QRegularExpression digest_pattern(
QStringLiteral("\\Asha256:[A-Fa-f0-9]{64}\\z"));
if (!digest_pattern.match(digest).hasMatch() || size <= 0) {
info.error = QObject::tr("The update package size or SHA-256 checksum is invalid.");
return info;
}
const auto encoded_asset = QString::fromLatin1(QUrl::toPercentEncoding(asset_name));
info.download_url = QUrl(DOWNLOAD_URL.arg(encoded_tag, encoded_asset));
info.sha256 = QByteArray::fromHex(digest.mid(7).toLatin1());
info.size = size;
return info;
}
info.error = QObject::tr("This release has no update package for your platform.");
return info;
}
VersionRelation Compare(const Info& latest, const Info& current) {
if (latest.tag.isEmpty() || current.tag.isEmpty()) {
return VersionRelation::Unknown;
}
if (latest.tag == current.tag) {
return VersionRelation::Current;
}
if (!latest.published_at.isValid() || !current.published_at.isValid() ||
latest.published_at == current.published_at) {
return VersionRelation::Unknown;
}
return latest.published_at > current.published_at ? VersionRelation::Newer
: VersionRelation::Older;
}
} // namespace UpdateRelease
+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);
+2 -1
View File
@@ -4,6 +4,7 @@
#include "common/emulatorConfig.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "graphics/presentation/systemOverlay.h"
#include "libs/dialog.h"
#include "libs/errno.h"
#include "libs/libs.h"
@@ -94,7 +95,7 @@ struct SystemServiceHdrToneMapLuminance {
static int KYTY_SYSV_ABI SystemServiceHideSplashScreen() {
PRINT_NAME();
Graphics::HideSplashScreen();
return OK;
}
+6
View File
@@ -63,6 +63,7 @@ static void PrintUsage() {
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
::printf(
" --hide-cursor Hide the cursor after 2 s idle. Default: off.\n");
::printf(" --no-splash Disable the startup splash screen.\n");
::printf(" --vr Enable the virtual VR headset.\n");
::printf(" --amd-cpu Apply AMD CPU instruction patches.\n");
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
@@ -219,6 +220,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
continue;
}
if (arg == "--no-splash") {
options.config.splash_enabled = false;
continue;
}
if (arg == "--vr") {
options.config.vr_enabled = true;
continue;
+15
View File
@@ -0,0 +1,15 @@
# The installer is copied outside the application before it runs. Keep it free of
# Qt and application DLL dependencies so it can replace those files on Windows.
if(MSVC)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()
add_executable(kyty_updater main.cpp UpdateInstaller.cpp)
if(MSVC)
target_compile_options(kyty_updater PRIVATE /EHsc)
else()
target_compile_options(kyty_updater PRIVATE -fexceptions)
endif()
if(MINGW)
target_link_options(kyty_updater PRIVATE -static -municode)
endif()
+249
View File
@@ -0,0 +1,249 @@
#include "UpdateInstaller.h"
#include <algorithm>
#include <stdexcept>
#include <vector>
namespace Kyty::Updater {
namespace {
namespace fs = std::filesystem;
struct Entry {
fs::path relative;
fs::file_type type;
};
struct Plan {
fs::path source;
fs::path target;
std::vector<Entry> entries;
};
bool IsWithin(const fs::path& path, const fs::path& root) {
auto part = path.begin();
for (const auto& component: root) {
if (part == path.end() || *part++ != component) {
return false;
}
}
return true;
}
bool IsRelative(const fs::path& path) {
if (path.empty() || path.is_absolute() || path.has_root_name()) {
return false;
}
for (const auto& component: path) {
if (component == ".." || component == "." || component.empty()) {
return false;
}
}
return true;
}
fs::file_type Type(const fs::path& path) {
std::error_code error;
const auto status = fs::symlink_status(path, error);
if (error && error != std::errc::no_such_file_or_directory) {
throw fs::filesystem_error("Cannot inspect update path", path, error);
}
return status.type();
}
void CheckLink(const fs::path& path, const fs::path& root) {
const auto link = fs::read_symlink(path);
if (link.is_absolute() || !IsWithin(fs::canonical(path), root)) {
throw std::runtime_error("Update contains a link outside its installation: " +
path.string());
}
}
void CheckDestination(const fs::path& target, const Entry& entry) {
const auto destination = target / entry.relative;
const auto type = Type(destination);
if (type == fs::file_type::not_found) {
return;
}
if ((entry.type == fs::file_type::directory) != (type == fs::file_type::directory) ||
(type != fs::file_type::regular && type != fs::file_type::directory &&
type != fs::file_type::symlink)) {
throw std::runtime_error("Update would replace a different file type: " +
destination.string());
}
if (type == fs::file_type::symlink) {
CheckLink(destination, target);
}
}
Plan MakePlan(const fs::path& source, const fs::path& target, const fs::path& launcher) {
if (!IsRelative(launcher) || Type(source) != fs::file_type::directory ||
Type(target) != fs::file_type::directory) {
throw std::runtime_error(
"Update source, installation directory, or launcher path is invalid.");
}
Plan plan {fs::canonical(source), fs::canonical(target), {}};
if (IsWithin(plan.target, plan.source)) {
throw std::runtime_error("Update source must not contain the installation directory.");
}
// macOS releases also have flat runtime files; install only the signed bundle.
fs::path scope;
if (launcher.begin()->extension() == ".app") {
scope = *launcher.begin();
}
const auto source_root = scope.empty() ? plan.source : plan.source / scope;
if (Type(source_root) != fs::file_type::directory) {
throw std::runtime_error("The release does not contain the application bundle.");
}
const auto binary_directory = launcher.parent_path();
#ifdef _WIN32
const fs::path suffix = ".exe";
#else
const fs::path suffix;
#endif
for (const auto& required: {launcher, binary_directory / ("kyty_emulator" + suffix.string()),
binary_directory / ("kyty_updater" + suffix.string())}) {
if (Type(plan.source / required) != fs::file_type::regular) {
throw std::runtime_error("Release is missing an executable: " + required.string());
}
#ifndef _WIN32
const auto executable =
fs::perms::owner_exec | fs::perms::group_exec | fs::perms::others_exec;
if ((fs::status(plan.source / required).permissions() & executable) == fs::perms::none) {
throw std::runtime_error("Release executable lacks execute permissions: " +
required.string());
}
#endif
}
if (!scope.empty()) {
plan.entries.push_back({scope, fs::file_type::directory});
}
for (const auto& item: fs::recursive_directory_iterator(source_root)) {
const auto relative = item.path().lexically_relative(plan.source);
const auto type = item.symlink_status().type();
if (relative.begin()->string().starts_with(".kyty-update")) {
throw std::runtime_error("Release uses a reserved updater filename.");
}
if (type == fs::file_type::symlink) {
CheckLink(item.path(), source_root);
} else if (type != fs::file_type::directory && type != fs::file_type::regular) {
throw std::runtime_error("Release contains an unsupported file: " + relative.string());
}
plan.entries.push_back({relative, type});
}
// Checking directories before their children prevents destination symlink traversal.
std::sort(plan.entries.begin(), plan.entries.end(),
[](const Entry& left, const Entry& right) { return left.relative < right.relative; });
for (const auto& entry: plan.entries) {
CheckDestination(plan.target, entry);
}
return plan;
}
struct InstallLock {
fs::path path;
void Acquire(const fs::path& target) {
auto candidate = target / ".kyty-update-lock";
if (!fs::create_directory(candidate)) {
throw std::runtime_error(
"Another update is running, or a previous update was interrupted. "
"Close all updater processes, inspect retained backups, then remove " +
candidate.string());
}
path = std::move(candidate);
}
~InstallLock() {
if (!path.empty()) {
std::error_code ignored;
// An incomplete rollback leaves backups here and keeps the lock for recovery.
fs::remove(path, ignored);
}
}
};
struct Change {
fs::path relative;
bool old_moved = false;
bool installed = false;
};
} // namespace
Result Validate(const fs::path& source, const fs::path& target, const fs::path& launcher_relative) {
try {
MakePlan(source, target, launcher_relative);
// Check actual write access, including ACLs, and interrupted/concurrent updates.
InstallLock lock;
lock.Acquire(target);
return {true, {}};
} catch (const std::exception& error) {
return {false, error.what()};
}
}
Result Install(const fs::path& source, const fs::path& target, const fs::path& launcher_relative) {
InstallLock lock;
fs::path backup;
std::vector<Change> changes;
std::vector<fs::path> created_directories;
try {
const auto plan = MakePlan(source, target, launcher_relative);
lock.Acquire(plan.target);
backup = lock.path / "backup";
changes.reserve(plan.entries.size());
created_directories.reserve(plan.entries.size());
// The launcher extracts beside the installation, so files can move directly
// from that staging directory without another full copy of the release.
for (const auto& entry: plan.entries) {
CheckDestination(plan.target, entry);
const auto destination = plan.target / entry.relative;
if (entry.type == fs::file_type::directory) {
if (fs::create_directory(destination)) {
created_directories.push_back(destination);
}
continue;
}
changes.push_back({entry.relative});
auto& change = changes.back();
if (Type(destination) != fs::file_type::not_found) {
fs::create_directories((backup / entry.relative).parent_path());
fs::rename(destination, backup / entry.relative);
change.old_moved = true;
}
fs::rename(plan.source / entry.relative, destination);
change.installed = true;
}
std::error_code cleanup_error;
fs::remove_all(backup, cleanup_error);
return {true, cleanup_error ? "Update installed; backup cleanup failed: " + backup.string()
: std::string {}};
} catch (const std::exception& error) {
std::string message = error.what();
bool restored = true;
for (auto change = changes.rbegin(); change != changes.rend(); ++change) {
try {
const auto destination = target / change->relative;
if (change->installed) {
fs::rename(destination, source / change->relative);
}
if (change->old_moved) {
fs::rename(backup / change->relative, destination);
}
} catch (const std::exception& rollback_error) {
restored = false;
message += "\nRollback failed: " + std::string(rollback_error.what());
}
}
for (auto directory = created_directories.rbegin(); directory != created_directories.rend();
++directory) {
std::error_code ignored;
fs::remove(*directory, ignored);
}
if (!backup.empty()) {
if (restored) {
std::error_code ignored;
fs::remove_all(backup, ignored);
} else {
message += "\nBackup retained at: " + backup.string();
}
}
return {false, message};
}
}
} // namespace Kyty::Updater
+18
View File
@@ -0,0 +1,18 @@
#pragma once
#include <filesystem>
#include <string>
namespace Kyty::Updater {
struct Result {
bool success;
std::string message;
};
// Only release files are replaced. Files absent from the release are preserved.
// Successful installation consumes staged files; stage on the target filesystem.
Result Validate(const std::filesystem::path& source, const std::filesystem::path& target,
const std::filesystem::path& launcher_relative);
Result Install(const std::filesystem::path& source, const std::filesystem::path& target,
const std::filesystem::path& launcher_relative);
} // namespace Kyty::Updater
+167
View File
@@ -0,0 +1,167 @@
#include "UpdateInstaller.h"
#include <chrono>
#include <fstream>
#include <iostream>
#include <limits>
#include <stdexcept>
#include <thread>
#include <vector>
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#else
#include <cerrno>
#include <csignal>
#include <spawn.h>
#include <unistd.h>
extern char** environ;
#endif
namespace {
namespace fs = std::filesystem;
bool WaitForParent(unsigned long pid) {
#ifdef _WIN32
HANDLE process = OpenProcess(SYNCHRONIZE, FALSE, pid);
if (!process) {
return GetLastError() == ERROR_INVALID_PARAMETER;
}
const auto result = WaitForSingleObject(process, 120000);
CloseHandle(process);
return result == WAIT_OBJECT_0;
#else
if (pid > static_cast<unsigned long>(std::numeric_limits<pid_t>::max())) {
return false;
}
const auto deadline = std::chrono::steady_clock::now() + std::chrono::minutes(2);
while (std::chrono::steady_clock::now() < deadline) {
if (kill(static_cast<pid_t>(pid), 0) != 0) {
return errno == ESRCH;
}
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
return false;
#endif
}
bool Launch(const fs::path& launcher, const fs::path& directory) {
#ifdef _WIN32
std::wstring command = L"\"" + launcher.wstring() + L"\"";
STARTUPINFOW startup {};
startup.cb = sizeof(startup);
PROCESS_INFORMATION process {};
if (!CreateProcessW(launcher.c_str(), command.data(), nullptr, nullptr, FALSE, 0, nullptr,
directory.c_str(), &startup, &process)) {
return false;
}
CloseHandle(process.hThread);
CloseHandle(process.hProcess);
return true;
#else
if (chdir(directory.c_str()) != 0) {
return false;
}
setsid();
pid_t child;
char* arguments[] = {const_cast<char*>(launcher.c_str()), nullptr};
return posix_spawn(&child, launcher.c_str(), nullptr, nullptr, arguments, environ) == 0;
#endif
}
void WriteResult(const fs::path& workspace, const fs::path& target,
const Kyty::Updater::Result& result) try {
const auto temporary = workspace / "update-result.txt";
std::ofstream stream(temporary, std::ios::trunc);
stream << (result.success ? "success\n" : "failure\n") << result.message;
stream.close();
if (!stream) {
throw std::runtime_error("Cannot write the update result.");
}
const auto destination = target / ".kyty-update-result";
// Never open the destination: rename safely replaces even an existing symlink.
std::error_code ignored;
fs::remove(destination, ignored);
fs::rename(temporary, destination);
} catch (const std::exception& error) {
// Reporting must never prevent the original or updated launcher restarting.
std::cerr << "Cannot record update result: " << error.what() << '\n';
}
void CleanupWorkspace(const fs::path& source, const fs::path& target) try {
const auto workspace = source.parent_path();
if (source.filename() != "payload" ||
!workspace.filename().string().starts_with(".kyty-update-") ||
fs::canonical(workspace.parent_path()) != fs::canonical(target)) {
return;
}
// Windows cannot unlink the running helper. Remove the archive and payload
// separately so that only this small executable can remain after success.
for (const auto& entry: fs::directory_iterator(workspace)) {
std::error_code ignored;
fs::remove_all(entry.path(), ignored);
}
std::error_code ignored;
fs::remove(workspace, ignored);
} catch (const std::exception& error) {
std::cerr << "Update installed; temporary files could not be removed: " << error.what() << '\n';
}
int Run(const std::vector<fs::path>& arguments) {
const bool validate = arguments.size() == 5 && arguments[1] == "--validate";
const bool install = arguments.size() == 7 && arguments[1] == "--install";
if (!validate && !install) {
std::cerr
<< "Usage: kyty_updater --validate SOURCE TARGET LAUNCHER_REL\n"
" kyty_updater --install SOURCE TARGET LAUNCHER_REL PARENT_PID WORKDIR\n";
return 2;
}
const auto source = fs::absolute(arguments[2]);
const auto target = fs::absolute(arguments[3]);
const auto launcher = arguments[4];
if (validate) {
const auto result = Kyty::Updater::Validate(source, target, launcher);
if (!result.success) {
std::cerr << result.message << '\n';
}
return result.success ? 0 : 1;
}
const auto pid_string = arguments[5].string();
std::size_t consumed = 0;
const auto pid = std::stoul(pid_string, &consumed);
if (pid <= 1 || consumed != pid_string.size() || pid_string.front() == '-') {
throw std::runtime_error("Invalid launcher process identifier.");
}
if (!WaitForParent(pid)) {
WriteResult(source.parent_path(), target,
{false, "Update cancelled: the launcher did not exit within two minutes."});
return 1;
}
auto result = Kyty::Updater::Install(source, target, launcher);
WriteResult(source.parent_path(), target, result);
if (!Launch(target / launcher, fs::absolute(arguments[6]))) {
result.message += "\nThe launcher could not restart. Open it manually.";
WriteResult(source.parent_path(), target, result);
std::cerr << result.message << '\n';
return 1;
}
if (result.success) {
CleanupWorkspace(source, target);
}
return result.success ? 0 : 1;
}
} // namespace
#ifdef _WIN32
int wmain(int argc, wchar_t* argv[]) {
#else
int main(int argc, char* argv[]) {
#endif
try {
return Run(std::vector<fs::path>(argv, argv + argc));
} catch (const std::exception& error) {
std::cerr << error.what() << '\n';
return 1;
}
}
+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");
+3 -18
View File
@@ -243,7 +243,7 @@ void TestSrtAliasesRetainReadPolicy() {
program.descriptor_sources.push_back({.dwords = {Value(&first), Value(0u)},
.dword_count = 2});
DescriptorSource indirect;
indirect.indirect_descriptor.emplace().sources = {0, 1};
indirect.indirect_descriptor.emplace(DescriptorSource::IndirectDescriptor{}).sources = {0, 1};
program.descriptor_sources.push_back(indirect);
auto plan = ExtractResourcePlan(program);
for (const auto &inst : plan.value_storage) {
@@ -435,7 +435,7 @@ void TestUnbasedFlatCacheHitMaterializes() {
"unbased FLAT plan produced unexpected descriptors");
}
void TestWrittenDescriptorPredicateReads(bool memory_condition, bool loop) {
void TestWrittenDescriptorPredicateReads(bool memory_condition) {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
program.stage = Libs::Graphics::ShaderType::Compute;
@@ -467,26 +467,12 @@ void TestWrittenDescriptorPredicateReads(bool memory_condition, bool loop) {
Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::ConditionalBranch;
program.blocks[0]->terminator.true_block = program.blocks[1];
program.blocks[0]->terminator.false_block = program.blocks[2];
program.blocks[1]->id = 1;
program.blocks[1]->terminator.kind =
Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::Return;
program.blocks[2]->id = 2;
program.blocks[2]->terminator.kind =
Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::Return;
if (loop) {
// The predicate reaches the store through a continue block, then loops back.
auto &continued = AddValueBlock(program);
program.blocks[0]->terminator.true_block = &continued;
continued.terminator.kind = Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::Branch;
continued.terminator.true_block = &store_block;
continued.AddBranch(&store_block);
store_block.terminator.kind = Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind::Branch;
store_block.terminator.true_block = &block;
store_block.AddBranch(&block);
}
block.AddBranch(program.blocks[0]->terminator.true_block);
block.AddBranch(program.blocks[2]);
program.memory_info.push_back({.kind = ResourceKind::Buffer, .resource = 0});
auto &output = store_block.AppendNewInst(ValueOpcode::GetBufferResource,
{source.dwords[0], source.dwords[1], source.dwords[2], source.dwords[3]});
@@ -691,8 +677,7 @@ int main() {
TestExactReciprocalDescriptorArithmetic();
TestUnbasedFlatCacheHitMaterializes();
for (const bool memory_condition : {false, true})
for (const bool loop : {false, true})
TestWrittenDescriptorPredicateReads(memory_condition, loop);
TestWrittenDescriptorPredicateReads(memory_condition);
TestFailedMaterializationRejectsStage();
TestFiniteImageRefreshReusesScalarReads();
TestMixedSamplerVariantsShareRuntimeDescriptor();
File diff suppressed because it is too large. Load diff
+95 -86
View File
@@ -1945,14 +1945,18 @@ void TestSopkCompareImmediateExtension() {
void TestDisabledDebugBranches() {
using namespace ShaderRecompiler;
// Both debug branches fall through when conditional shader debugging is disabled.
// Debug branches fall through when conditional shader debugging is disabled.
// Retain the post-ENDPGM helper displacement from PPSA08709 and exercise the
// OR_USER variant encountered in PPSA03671 with the same body.
for (const auto opcode : {Decoder::Opcode::S_CBRANCH_CDBGSYS,
Decoder::Opcode::S_CBRANCH_CDBGSYS_OR_USER}) {
const std::pair<uint32_t, Decoder::Opcode> branches[] = {
{0x17, Decoder::Opcode::S_CBRANCH_CDBGSYS},
{0x18, Decoder::Opcode::S_CBRANCH_CDBGUSER},
{0x19, Decoder::Opcode::S_CBRANCH_CDBGSYS_OR_USER},
{0x1a, Decoder::Opcode::S_CBRANCH_CDBGSYS_AND_USER}};
for (const auto [encoding, opcode] : branches) {
std::array<uint32_t, 274> shader;
shader.fill(EncodeSopp(0x00));
shader[0] = EncodeSopp(opcode == Decoder::Opcode::S_CBRANCH_CDBGSYS ? 0x17 : 0x19, 0x10f);
shader[0] = EncodeSopp(encoding, 0x10f);
shader[1] = EncodeVop1(0x01, 1, 129);
shader[2] = EncodeMubuf0(0x1c, 0, false);
shader[3] = EncodeMubuf1(1, 0, 0);
@@ -1962,6 +1966,7 @@ void TestDisabledDebugBranches() {
Decoder::Program decoded;
Decoder::DecodeProgram(shader, decoded);
Check(decoded.instructions.front().opcode == opcode &&
decoded.instructions.front().opcode_id == encoding &&
decoded.instructions.front().branch_target == 0x440u &&
decoded.instructions.back().pc == 0x43cu &&
decoded.instructions.back().opcode == Decoder::Opcode::S_ENDPGM,
@@ -4823,34 +4828,92 @@ void TestNewShaderDecoderArchitecture() {
"VOP2 DPP V_PK_FMAC_F16 lost its implicit packed modifiers");
}
void TestNewShaderRecompilerRejectsDppOn64BitCompares() {
const uint32_t opcodes[] = {
0xa2u, 0xa5u, 0xb5u, 0xe2u, 0xe4u, 0xe5u,
0xf5u}; // eq_i64, ne_i64, cmpx_ne_i64, eq_u64, gt_u64, ne_u64, cmpx_ne_u64
for (const auto opcode : opcodes) {
const uint32_t shader[] = {
EncodeVopc(opcode, 250u, 0u), // DPP escape in SRC0
EncodeVop2Dpp(0u),
0xbf810000u,
};
ShaderRecompiler::Decoder::Program program;
ShaderRecompiler::Decoder::DecodeProgram(shader, program);
Check(program.instructions.size() == 2u,
"64-bit VOPC DPP decode did not consume its modifier word");
const auto &compare = program.instructions.front();
Check(compare.opcode == ShaderRecompiler::Decoder::Opcode::UNSUPPORTED,
"64-bit VOPC illegally accepted a DPP modifier");
Check((compare.unsupported_reason.find("VOPC DPP modifier is not supported for opcode") != std::string::npos),
"64-bit VOPC DPP rejection reason was not explicit");
void TestNewShaderRecompilerInteger64CompareEncodings() {
namespace D = ShaderRecompiler::Decoder;
using O = D::Opcode;
const O families[][8] = {
{O::V_CMP_F_I64, O::V_CMP_LT_I64, O::V_CMP_EQ_I64, O::V_CMP_LE_I64,
O::V_CMP_GT_I64, O::V_CMP_NE_I64, O::V_CMP_GE_I64, O::V_CMP_T_I64},
{O::V_CMPX_F_I64, O::V_CMPX_LT_I64, O::V_CMPX_EQ_I64, O::V_CMPX_LE_I64,
O::V_CMPX_GT_I64, O::V_CMPX_NE_I64, O::V_CMPX_GE_I64, O::V_CMPX_T_I64},
{O::V_CMP_F_U64, O::V_CMP_LT_U64, O::V_CMP_EQ_U64, O::V_CMP_LE_U64,
O::V_CMP_GT_U64, O::V_CMP_NE_U64, O::V_CMP_GE_U64, O::V_CMP_T_U64},
{O::V_CMPX_F_U64, O::V_CMPX_LT_U64, O::V_CMPX_EQ_U64, O::V_CMPX_LE_U64,
O::V_CMPX_GT_U64, O::V_CMPX_NE_U64, O::V_CMPX_GE_U64, O::V_CMPX_T_U64}};
const uint32_t bases[] = {0xa0u, 0xb0u, 0xe0u, 0xf0u};
for (uint32_t family = 0; family < std::size(families); ++family) {
const bool update_exec = (family % 2) != 0;
for (uint32_t predicate = 0; predicate < 8; ++predicate) {
const auto encoding = bases[family] + predicate;
for (const bool vop3 : {false, true}) {
const std::array<uint32_t, 2> words = vop3
? std::array{EncodeVop3Word0(encoding, 20),
EncodeVop3Word1(257, 259, 0)}
: std::array{EncodeVopc(encoding, 257, 3), 0xbf810000u};
D::Instruction compare;
D::DecodeInstruction(words, 0, compare);
const auto destination = update_exec ? D::OperandKind::ExecLo
: vop3 ? D::OperandKind::Sgpr : D::OperandKind::VccLo;
Check(compare.family == (vop3 ? D::Family::VOP3 : D::Family::VOPC) &&
compare.opcode == families[family][predicate] &&
compare.opcode_id == encoding &&
compare.word_count == (vop3 ? 2u : 1u) &&
compare.dst.kind == destination &&
(!vop3 || update_exec || compare.dst.reg == 20) &&
compare.src_count == 2 &&
compare.src0.kind == D::OperandKind::Vgpr && compare.src0.reg == 1 &&
compare.src1.kind == D::OperandKind::Vgpr && compare.src1.reg == 3,
"64-bit integer compare decoded incorrectly");
for (const bool high_src0 : {false, true}) {
// Only the compare's pair width makes v13 reachable by MOVRELD,
// including predicates F/T that never read their operands.
const uint32_t src0 = high_src0 ? 268u : 266u;
const uint32_t src1 = high_src0 ? 10u : 12u;
std::vector<uint32_t> shader{EncodeVop1(0x42, 12, 256)};
if (vop3) {
shader.push_back(EncodeVop3Word0(encoding, 20));
shader.push_back(EncodeVop3Word1(src0, src1 + 256, 0));
} else {
shader.push_back(EncodeVopc(encoding, src0, src1));
}
shader.push_back(EncodeSopp(0x01));
D::Program decoded;
D::DecodeProgram(shader, decoded);
ShaderComputeInputInfo compute{};
ShaderRecompiler::Frontend::TranslateOptions options{.stage = ShaderType::Compute};
options.input_info.compute = &compute;
const auto program = ShaderRecompiler::Frontend::TranslateProgram(
decoded, ShaderRecompiler::CFG::BuildGraph(decoded), options);
bool includes_high_word = false;
for (const auto *block : program.blocks) {
for (const auto &inst : *block) {
includes_high_word |=
inst.GetOpcode() == ShaderRecompiler::IR::ValueOpcode::SetVectorRegister &&
ShaderRecompiler::IR::RegIndex(inst.Arg(0).VectorRegister()) == 13;
}
}
Check(includes_high_word, "64-bit compare omitted a VGPR pair's high word");
}
}
for (const bool dpp : {false, true}) {
const uint32_t shader[] = {
EncodeVopc(encoding, dpp ? 250u : 249u, 3),
dpp ? EncodeVop2Dpp(1) : EncodeVopcSdwa(1),
0xbf810000u};
D::Program program;
D::DecodeProgram(shader, program);
Check(program.instructions.size() == 2u &&
program.instructions.back().opcode == O::S_ENDPGM,
"64-bit VOPC decode did not consume its modifier word");
const auto &compare = program.instructions.front();
const auto reason = dpp ? "VOPC DPP modifier is not supported for opcode"
: "VOPC SDWA modifier is not supported for opcode";
Check(compare.opcode == O::UNSUPPORTED && compare.word_count == 2u &&
compare.unsupported_reason.find(reason) != std::string::npos,
"64-bit VOPC did not explicitly reject an illegal modifier");
}
}
}
const uint32_t sdwa[] = {EncodeVopc(0xa5u, 249u, 0u), 0x06060000u};
ShaderRecompiler::Decoder::Instruction compare;
ShaderRecompiler::Decoder::DecodeInstruction(sdwa, 0u, compare);
Check(compare.opcode == ShaderRecompiler::Decoder::Opcode::UNSUPPORTED &&
compare.word_count == 2u &&
compare.unsupported_reason.find("VOPC SDWA modifier is not supported") != std::string::npos,
"V_CMP_NE_I64 accepted an illegal SDWA encoding");
}
void TestNewShaderRecompilerCapturedVopcSdwaCmpxClass() {
@@ -14031,59 +14094,6 @@ void TestUniformSelectedDescriptorLoadAddress() {
}
}
void TestNativeScalarAtomicPayloadStaysOnGpu() {
using namespace ShaderRecompiler::IR;
// Exact final block from SAROS b62b494cb2567011: read a mutable flag,
// atomically update that address, then test the loaded flag before S_TRAP.
const uint32_t shader[] = {
0x7e0002ffu, 0x00100000u, 0x7e020280u, 0xf4040100u,
0xfa000018u, 0xbf8cc07fu, 0x8801ff05u, 0x01000000u,
0xbe800304u, 0xbe820381u, 0xbe8303ffu, 0x00016204u,
0xf4001a82u, 0xfa000018u, 0xe1680018u, 0x80000000u,
0xbf8cc07fu, 0xbf0d846au, 0xbf840001u, 0xbf920001u,
0xbf810000u,
};
const std::array<uint32_t, 2> user_data{0x1000u, 0u};
auto options = MakeCompileOptions(ShaderType::Compute);
options.user_data = user_data;
auto translated = ShaderRecompiler::TranslateProgram(shader, options);
const auto &program = translated.program;
const Inst *payload = nullptr;
for (const auto *block : program.blocks) {
for (const auto &inst : *block) {
if (inst.GetOpcode() == ValueOpcode::LoadAddressU32 &&
program.memory_info[inst.Flags<MemoryFlags>().index].kind == ResourceKind::ScalarAddress)
payload = &inst;
}
}
Check(payload != nullptr && payload->Parent() != nullptr &&
!program.memory_info[payload->Flags<MemoryFlags>().index].planning_only &&
program.info.uses_dma && program.info.buffers.size() == 1 &&
program.info.buffers[0].atomic,
"native scalar payload was replaced by a host snapshot before its atomic write");
auto plan = ExtractResourcePlan(program);
Check(plan.srt_reads.size() == 2 && plan.control_flow.empty() &&
!plan.capture_specialization_reads,
"terminal native shader assertion entered resource planning");
struct Reads { uint32_t descriptors = 0; uint32_t payload = 0; } reads;
const auto read = +[](void *data, uint64_t address, std::span<uint32_t> words) {
auto &reads = *static_cast<Reads *>(data);
if (address == 0x2018u) { ++reads.payload; return false; }
if (words.size() != 1 || (address != 0x1018u && address != 0x101cu)) return false;
++reads.descriptors;
words[0] = address == 0x1018u ? 0x2000u : 0u;
return true;
};
const SrtRuntime runtime{.user_data = user_data, .read_memory = read,
.userdata = &reads, .read_specialization_memory = read};
ResourceSnapshot snapshot;
ResourceSpecialization specialization;
Check(MaterializeResources(plan, runtime, snapshot, specialization) &&
reads.descriptors == 2 && reads.payload == 0 &&
snapshot.specialization_reads.empty() && snapshot.buffers[0].dwords[0] == 0x2000u,
"native resource materialization read the mutable flag or lost its atomic descriptor");
}
void TestBoundedScalarMaterialImageKeys() {
using namespace ShaderRecompiler::IR;
// SAROS 9fba2edffc549531: min(header count,64), scalar rows of 160 bytes,
@@ -15604,7 +15614,7 @@ int main() {
TestVopcCmpxClassF16Decoder();
TestNewShaderRecompilerCapturedVopcSdwaCmpxLtU16();
TestNewShaderRecompilerIrLookupMissFailsExplicitly();
TestNewShaderRecompilerRejectsDppOn64BitCompares();
TestNewShaderRecompilerInteger64CompareEncodings();
TestFloatComparisonInputModes();
TestPsInputCountRegisterDecode();
TestPixelAncillaryLayerInput();
@@ -15709,7 +15719,6 @@ int main() {
TestGpuProducedWritableDescriptor();
TestUniformSelectedWritableDescriptor();
TestUniformSelectedDescriptorLoadAddress();
TestNativeScalarAtomicPayloadStaysOnGpu();
TestBoundedScalarMaterialImageKeys();
TestImmutableDescriptorPredicate();
TestTypedDescriptorRealCarryAndScalarLoads();