Compare commits

...

9 Commits

Author SHA1 Message Date
nmzik 596ecdcca0 Fix shader CFG structurization for nested early exits 2026-08-19 00:14:51 +02:00
nmzik 5d83ce82af renderer: track and materialize deferred DCC clears
Co-authored-by: shadowbeat070 <70843316+shadowbeat070@users.noreply.github.com>
2026-08-18 19:10:36 +02:00
shadowbeat070 4fb72e9db7 graphics: add --gpu-assisted-validation and report fatal VkResults (#268)
* graphics: report the VkResult when a submit or fence wait fails

* graphics: add a --gpu-assisted-validation flag

* graphics: imply Vulkan validation for GPU-assisted validation

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-08-18 14:44:48 +02:00
Stefanos Costa c4ca8f4bf8 shader: fix the Linux and macOS build (#285)
* fixed linux build
- added correct uint long translation
-  replaced throw with non-constexpr function so that it fails in case of malformed table

* shader: remove redundant opcode table comment

* shader: use EXIT for invalid opcode tables

---------

Co-authored-by: nmzik <Nmzik@mail.ru>
2026-08-18 14:41:48 +02:00
Hashim Khan a4b970d97d launcher: explain unsupported mods-style patch JSON (#248)
When a _Patches file has no patches array (often a shadPS4/ETAHen mods
list), show a clear status instead of "Loaded 0 patch(es)". Fixes #174.
2026-08-18 14:22:40 +02:00
nmzik 8cfc9cd7a8 renderer: execute attachmentless PS side-effect draws 2026-08-18 14:03:36 +02:00
nmzik e39162fc52 shader: remove validation from program cache key 2026-08-18 14:03:36 +02:00
Hashim Khan 6bf69297c6 launcher: keep window open when game folder prompt is dismissed (#245)
Calling QApplication::quit() after the user closes the first-run game
folder dialog can segfault during nested modal shutdown. Leave the
launcher open so settings can still be configured. Fixes #121.
2026-08-18 04:18:56 +02:00
SoporteChilemat 95d282854d Build audio_out2_port_tests with kyty_tests (#242) 2026-08-18 04:08:16 +02:00
21 changed files with 758 additions and 164 deletions
+1
View File
@@ -576,6 +576,7 @@ if(BUILD_TESTING)
sync_on_address_tests
shader_recompiler_compute_tests
virtual_memory_allocation_tests
audio_out2_port_tests
)
endif()
+4
View File
@@ -93,6 +93,10 @@ bool SpirvDebugPrintfEnabled() {
return g_config->spirv_debug_printf_enabled;
}
bool GpuAssistedValidationEnabled() {
return g_config->gpu_assisted_validation_enabled && g_config->vulkan_validation_enabled;
}
bool RenderDocEnabled() {
return g_config->renderdoc_enabled;
}
+3
View File
@@ -49,6 +49,7 @@ struct ConfigOptions {
std::filesystem::path printf_output_file = "_kyty.txt";
ProfilerDirection profiler_direction = ProfilerDirection::None;
bool spirv_debug_printf_enabled = false;
bool gpu_assisted_validation_enabled = false;
bool renderdoc_enabled = false;
bool readback_linear_images = false;
bool playgo_hack_enabled = false;
@@ -84,6 +85,8 @@ ProfilerDirection GetProfilerDirection();
bool SpirvDebugPrintfEnabled();
bool GpuAssistedValidationEnabled();
bool RenderDocEnabled();
bool ReadbackLinearImagesEnabled();
bool PlayGoHackEnabled();
+100 -13
View File
@@ -252,7 +252,7 @@ void TextureCache::DeleteImage(ImageId id) {
EXIT("TextureCache: deleting a GPU-modified image without resolving its contents\n");
}
m_download_images.erase(id);
if (owner->info.metadata.kind == ImageMetadataKind::Htile) {
if (owner->info.HasMetadata()) {
m_surface_metas.erase(owner->info.metadata.range.address);
}
UnregisterImage(id);
@@ -1327,6 +1327,19 @@ vk::ImageView TextureCache::FindRenderTarget(ImageId id, const ImageDesc& desc)
image.MarkGpuModified();
image.usage.render_target = true;
RefreshImage(id, desc);
// DCC uses a separate metadata allocation. Register it when the color target is bound,
// matching the point where CMask/FMask will be registered. Preserve a PendingDcc entry
// because the metadata fill may have run before this bind.
if (desc.info.metadata.kind == ImageMetadataKind::Dcc) {
image.info.metadata = desc.info.metadata;
auto [metadata, inserted] = m_surface_metas.try_emplace(
desc.info.metadata.range.address, MetaDataInfo {.type = MetaDataInfo::Type::Dcc});
if (!inserted && metadata->second.type == MetaDataInfo::Type::PendingDcc) {
metadata->second.type = MetaDataInfo::Type::Dcc;
} else if (!inserted && metadata->second.type != MetaDataInfo::Type::Dcc) {
EXIT("TextureCache: color target reuses non-DCC metadata\n");
}
}
CommitGpuWrite(image);
TrackImageDownloadLocked(id, image);
const auto view = image.FindView(desc.view_info);
@@ -1349,8 +1362,20 @@ vk::ImageView TextureCache::FindDepthTarget(ImageId id, const ImageDesc& desc) {
RefreshImage(id, desc);
if (desc.info.HasMetadata()) {
image.info.metadata = desc.info.metadata;
m_surface_metas.try_emplace(desc.info.metadata.range.address,
MetaDataInfo {.clear_mask = image.info.htile_clear_mask});
auto [metadata, inserted] =
m_surface_metas.try_emplace(desc.info.metadata.range.address,
MetaDataInfo {.type = MetaDataInfo::Type::HTile,
.clear_mask = image.info.htile_clear_mask});
if (!inserted && metadata->second.type == MetaDataInfo::Type::PendingDcc) {
// PendingDcc fills use DCC-specific encoding. Do not reinterpret a speculative
// DCC/buffer fill as HTile state if the address is later classified as depth metadata.
metadata->second.type = MetaDataInfo::Type::HTile;
metadata->second.clear_mask = image.info.htile_clear_mask;
metadata->second.fill_value = 0xffffffffu;
metadata->second.fill_size = 0;
} else if (!inserted && metadata->second.type != MetaDataInfo::Type::HTile) {
EXIT("TextureCache: depth target reuses non-HTile metadata\n");
}
}
CommitGpuWrite(image);
if (desc.info.HasStencil()) {
@@ -1826,33 +1851,90 @@ void TextureCache::ClearGpuModified(ImageId id) {
}
bool TextureCache::IsMeta(uint64_t address) {
CacheLock lock(*this, m_lock);
return m_surface_metas.contains(address);
}
bool TextureCache::IsMetaCleared(uint64_t address, uint32_t slice) {
CacheLock lock(*this, m_lock);
const auto found = m_surface_metas.find(address);
if (found == m_surface_metas.end()) {
return found != m_surface_metas.end() && found->second.type != MetaDataInfo::Type::PendingDcc;
}
bool TextureCache::IsMetaCleared(uint64_t address, uint32_t slice, uint32_t* fill_value) {
CacheLock lock(*this, m_lock);
const auto found = m_surface_metas.find(address);
if (found == m_surface_metas.end() || found->second.type == MetaDataInfo::Type::PendingDcc ||
slice >= 32) {
return false;
}
if (fill_value != nullptr) {
*fill_value = found->second.fill_value;
}
return (found->second.clear_mask & (1u << slice)) != 0;
}
bool TextureCache::ClearMeta(uint64_t address) {
CacheLock lock(*this, m_lock);
const auto found = m_surface_metas.find(address);
if (found == m_surface_metas.end()) {
if (found == m_surface_metas.end() || found->second.type == MetaDataInfo::Type::PendingDcc ||
found->second.type == MetaDataInfo::Type::Dcc) {
// Preserve the broad metadata-clear operation for CMask/FMask/HTile. DCC requires a
// validated fill value, so an arbitrary compute write must not clear it.
return false;
}
found->second.clear_mask = UINT32_MAX;
return true;
}
bool TextureCache::TryConsumeDccFill(uint64_t address, uint64_t size, uint32_t fill_value) {
if (!GuestRange {address, size}.Valid()) {
EXIT("TextureCache: invalid DCC fill range\n");
}
// DCC fills use a repeated byte code. Require all four bytes of the detected dword to agree,
// and mark only recognized deferred-clear encodings as logically clear.
const auto dcc_clear_mask = [fill_value] {
const auto code = static_cast<uint8_t>(fill_value);
if (fill_value != static_cast<uint32_t>(code) * 0x01010101u) {
return 0u;
}
switch (code) {
case 0x00:
case 0x20:
case 0x40:
case 0x80:
case 0xc0: return UINT32_MAX;
default: return 0u;
}
}();
CacheLock lock(*this, m_lock);
auto found = m_surface_metas.find(address);
if (found == m_surface_metas.end()) {
// This dispatch may precede color-target discovery. Retain it in the shared
// metadata map, but PendingDcc remains invisible to IsMeta until registration. Returning
// false lets the guest dispatch execute and initialize memory while the type is uncertain.
m_surface_metas.emplace(address, MetaDataInfo {.type = MetaDataInfo::Type::PendingDcc,
.clear_mask = dcc_clear_mask,
.fill_value = fill_value,
.fill_size = size});
return false;
}
if (found->second.type == MetaDataInfo::Type::PendingDcc) {
found->second.clear_mask = dcc_clear_mask;
found->second.fill_value = fill_value;
found->second.fill_size = size;
return false;
}
if (found->second.type == MetaDataInfo::Type::Dcc) {
found->second.clear_mask = dcc_clear_mask;
found->second.fill_value = fill_value;
found->second.fill_size = size;
return true;
}
// The strict DCC path must not reinterpret a CMask/FMask/HTile allocation as DCC.
return false;
}
bool TextureCache::TouchMeta(uint64_t address, uint32_t slice, bool is_clear) {
CacheLock lock(*this, m_lock);
const auto found = m_surface_metas.find(address);
if (found == m_surface_metas.end()) {
if (found == m_surface_metas.end() || found->second.type == MetaDataInfo::Type::PendingDcc ||
slice >= 32) {
return false;
}
if (is_clear) {
@@ -1867,8 +1949,13 @@ void TextureCache::UnmapMemory(uint64_t address, uint64_t size) {
if (!GuestRange {address, size}.Valid()) {
EXIT("TextureCache: invalid unmap range\n");
}
CacheLock lock(*this, m_lock);
auto images = FindImagesInRegion(address, size, false);
CacheLock lock(*this, m_lock);
const auto end = address + size;
for (auto metadata = m_surface_metas.lower_bound(address);
metadata != m_surface_metas.end() && metadata->first < end;) {
metadata = m_surface_metas.erase(metadata);
}
auto images = FindImagesInRegion(address, size, false);
if (!images.empty()) {
m_scheduler.Finish();
}
+15 -1
View File
@@ -69,8 +69,12 @@ public:
[[nodiscard]] RegionInfo QueryRegion(uint64_t address, uint64_t size);
[[nodiscard]] bool IsMeta(uint64_t address);
[[nodiscard]] bool IsMetaCleared(uint64_t address, uint32_t slice);
[[nodiscard]] bool IsMetaCleared(uint64_t address, uint32_t slice,
uint32_t* fill_value = nullptr);
[[nodiscard]] bool ClearMeta(uint64_t address);
// Returns true when registered DCC absorbed the fill and the caller may skip the dispatch.
// False may still record PendingDcc state, but the guest dispatch must execute.
[[nodiscard]] bool TryConsumeDccFill(uint64_t address, uint64_t size, uint32_t fill_value);
[[nodiscard]] bool TouchMeta(uint64_t address, uint32_t slice, bool is_clear);
void UnmapMemory(uint64_t address, uint64_t size);
@@ -88,7 +92,17 @@ private:
};
struct MetaDataInfo {
// A guest metadata-fill dispatch may initialize DCC before its render target is bound.
// PendingDcc retains that exact fill until FindRenderTarget classifies the address,
// without exposing an unconfirmed buffer address to the normal metadata heuristics.
// Keep all surface metadata in this one shadPS4-style entry so CMask/FMask can be
// registered beside HTile and DCC without introducing parallel tracking paths.
enum class Type : uint8_t { PendingDcc, CMask, FMask, HTile, Dcc };
Type type = Type::PendingDcc;
uint32_t clear_mask = 0;
uint32_t fill_value = 0xffffffffu;
uint64_t fill_size = 0;
};
struct OverlapResult {
@@ -21,6 +21,28 @@ namespace Libs::Graphics {
static std::atomic<uint32_t> g_render_color_log_count = 0;
static void ResolveDccClearInfo(RenderColorInfo& info, vk::Format format, bool has_dcc,
uint32_t packed_clear) {
// Register-backed DCC clears use the target's packed clear value. Decode one-word guest
// formats here; unsupported encodings remain tracked without unsafe materialization.
info.metadata_clear_supported =
has_dcc && DecodePackedColorClear(format, packed_clear, info.color_clear_value);
switch (format) {
case vk::Format::eR8G8B8A8Unorm:
case vk::Format::eR8G8B8A8Srgb:
case vk::Format::eB8G8R8A8Unorm:
case vk::Format::eB8G8R8A8Srgb:
case vk::Format::eA2B10G10R10UnormPack32:
case vk::Format::eA2R10G10B10UnormPack32:
info.metadata_fixed_clear_supported = has_dcc;
break;
default: info.metadata_fixed_clear_supported = false; break;
}
if (!info.metadata_clear_supported) {
info.color_clear_value = {};
}
}
// NOLINTNEXTLINE(readability-function-cognitive-complexity)
void RenderExecutor::ResolveRenderColorTarget(uint64_t submit_id, RenderCommandBuffer& buffer,
RenderColorInfo& r,
@@ -55,20 +77,22 @@ void RenderExecutor::ResolveRenderColorTarget(uint64_t submit_id, RenderCommandB
}
// No color output
r.type = RenderColorType::NoColorOutput;
r.desc = {};
r.base_addr = 0;
r.image_id = {};
r.image_view = nullptr;
r.format = vk::Format::eUndefined;
r.extent = {};
r.base_mip_level = 0;
r.base_array_layer = 0;
r.buffer_size = 0;
r.samples = 1;
r.export_mapping = {};
r.color_clear_enable = false;
r.color_clear_value = {};
r.type = RenderColorType::NoColorOutput;
r.desc = {};
r.base_addr = 0;
r.image_id = {};
r.image_view = nullptr;
r.format = vk::Format::eUndefined;
r.extent = {};
r.base_mip_level = 0;
r.base_array_layer = 0;
r.buffer_size = 0;
r.samples = 1;
r.export_mapping = {};
r.color_clear_enable = false;
r.metadata_clear_supported = false;
r.metadata_fixed_clear_supported = false;
r.color_clear_value = {};
return;
}
const auto samples = render_sample_count(rt.attrib.num_fragments);
@@ -107,9 +131,9 @@ void RenderExecutor::ResolveRenderColorTarget(uint64_t submit_id, RenderCommandB
}
}
// CB_COLOR_CONTROL describes the color-buffer operation / ROP3 logic op.
// ROP3 Copy is the normal color write path, not a render-target clear.
// SRGB clear words are still encoded as normalized component values.
// Color-control state selects the color-buffer operation and logical blend operation.
// The normal copy operation is a regular color write, not an attachment clear.
// Nonlinear clear values are still stored as normalized components.
// Fast color clears are metadata driven and must be handled explicitly when
// that metadata path is implemented; render-pass load must preserve contents.
r.color_clear_enable = false;
@@ -309,6 +333,13 @@ void RenderExecutor::ResolveRenderColorTarget(uint64_t submit_id, RenderCommandB
desc.info.bytes_per_block = bytes_per_element;
desc.info.samples = samples;
desc.info.tile_mode = rt.attrib3.tile_mode;
const bool has_dcc = rt.info.dcc_compression_enable && rt.dcc_addr.addr != 0;
if (has_dcc) {
// DCC uses a separate metadata allocation. Carry its address through ImageInfo so
// TextureCache can associate metadata fills observed before target registration.
desc.info.metadata.kind = ImageMetadataKind::Dcc;
desc.info.metadata.range.address = rt.dcc_addr.addr;
}
for (uint32_t level = 0; level < levels; level++) {
if (volume) {
const auto& mip = volume_layout.mips[level];
@@ -355,7 +386,7 @@ void RenderExecutor::ResolveRenderColorTarget(uint64_t submit_id, RenderCommandB
r.samples = samples;
r.export_mapping = target_format.export_mapping;
r.color_clear_enable = false;
r.color_clear_value = {};
ResolveDccClearInfo(r, target_format.format, has_dcc, rt.clear_word0.word0);
BindRenderTarget(r.image_id);
}
@@ -31,7 +31,9 @@ struct RenderColorInfo {
uint32_t target_slot = 0;
uint32_t samples = 1;
Prospero::ColorComponentMapping export_mapping;
bool color_clear_enable = false;
bool color_clear_enable = false;
bool metadata_clear_supported = false;
bool metadata_fixed_clear_supported = false;
vk::ClearColorValue color_clear_value {};
};
+28 -12
View File
@@ -17,10 +17,29 @@
#include <algorithm>
#include <bit>
#include <cstdio>
#include <cstring>
#include <memory>
namespace Libs::Graphics {
namespace {
void ReportVulkanFatal(const char* what, vk::Result result, uint32_t slot, uint64_t submit_seq,
uint32_t debug_op, uint64_t debug_submit, uint32_t arg0, uint32_t arg1,
uint32_t arg2, uint32_t arg3, uint64_t arg4) {
LOGF("%s failed: %s (%d), slot=%u submit_seq=%" PRIu64 " debug_op=%u debug_submit=%" PRIu64
" args=%u,%u,%u,%u,0x%016" PRIx64 "\n",
what, VulkanToString(result).c_str(), static_cast<int>(result), slot, submit_seq, debug_op,
debug_submit, arg0, arg1, arg2, arg3, arg4);
std::printf("%s failed: %s (%d), slot=%u submit_seq=%" PRIu64 " debug_op=%u debug_submit=%" PRIu64
" args=%u,%u,%u,%u,0x%016" PRIx64 "\n",
what, VulkanToString(result).c_str(), static_cast<int>(result), slot, submit_seq,
debug_op, debug_submit, arg0, arg1, arg2, arg3, arg4);
std::fflush(stdout);
}
} // namespace
FenceResourceRetainer::~FenceResourceRetainer() {
if (!m_resources.empty()) {
EXIT("fence resource retainer destroyed before release\n");
@@ -169,8 +188,9 @@ void CommandBuffer::Execute(const SubmitInfo& submit) {
auto result = graphics.device.resetFences(1, &fence);
if (result != vk::Result::eSuccess) {
LOGF("vkResetFences failed before submit: %s (%d)\n", VulkanToString(result).c_str(),
static_cast<int>(result));
ReportVulkanFatal("vkResetFences (before submit)", result, m_slot->id, m_submit_seq,
m_debug_op, m_debug_submit_id, m_debug_arg0, m_debug_arg1, m_debug_arg2,
m_debug_arg3, m_debug_arg4);
}
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
@@ -192,11 +212,9 @@ void CommandBuffer::Execute(const SubmitInfo& submit) {
m_fence_waited = false;
if (result != vk::Result::eSuccess) {
LOGF("vkQueueSubmit failed: %s (%d), slot=%u submit_seq=%" PRIu64
" debug_op=%u debug_submit=%" PRIu64 " args=%u,%u,%u,%u,0x%016" PRIx64 "\n",
VulkanToString(result).c_str(), static_cast<int>(result), m_slot->id, m_submit_seq,
m_debug_op, m_debug_submit_id, m_debug_arg0, m_debug_arg1, m_debug_arg2, m_debug_arg3,
m_debug_arg4);
ReportVulkanFatal("vkQueueSubmit", result, m_slot->id, m_submit_seq, m_debug_op,
m_debug_submit_id, m_debug_arg0, m_debug_arg1, m_debug_arg2, m_debug_arg3,
m_debug_arg4);
}
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
}
@@ -213,11 +231,9 @@ void CommandBuffer::WaitForFenceOnly() {
auto device = m_graphics.device;
auto result = device.waitForFences(1, &m_slot->fence, VK_TRUE, UINT64_MAX);
if (result != vk::Result::eSuccess) {
LOGF("vkWaitForFences failed: %s (%d), slot=%u submit_seq=%" PRIu64
" debug_op=%u debug_submit=%" PRIu64 " args=%u,%u,%u,%u,0x%016" PRIx64 "\n",
VulkanToString(result).c_str(), static_cast<int>(result), m_slot->id, m_submit_seq,
m_debug_op, m_debug_submit_id, m_debug_arg0, m_debug_arg1, m_debug_arg2, m_debug_arg3,
m_debug_arg4);
ReportVulkanFatal("vkWaitForFences", result, m_slot->id, m_submit_seq, m_debug_op,
m_debug_submit_id, m_debug_arg0, m_debug_arg1, m_debug_arg2, m_debug_arg3,
m_debug_arg4);
}
EXIT_NOT_IMPLEMENTED(result != vk::Result::eSuccess);
m_fence_waited = true;
@@ -121,6 +121,12 @@ PipelineCache::GraphicsPipeline& PipelineCache::CreateGraphicsPipeline(
depth.samples);
}
}
if (color_count == 0 && !with_depth) {
attachment_samples = render_sample_count(ctx.GetAaConfig().msaa_num_samples);
EXIT_IF(!static_cast<bool>(
m_graphics.GetPhysicalDeviceProperties().limits.framebufferNoAttachmentsSampleCounts &
vulkan_sample_count(attachment_samples)));
}
EXIT_IF(attachment_samples == 0 ||
vulkan_sample_count(attachment_samples) == vk::SampleCountFlagBits {});
@@ -139,6 +139,21 @@ static bool TryConsumeComputeImageClear(const ShaderComputeInputInfo& input, Com
}
auto& cache = command.GetContext().GetTextureCache();
if (!cache.ClearImageFromBuffer(command, descriptor.Base48(), size, packed_clear)) {
// Recognized metadata-fill shaders access DCC as an ordinary storage buffer and may run
// before the render target is bound. TryConsumeDccFill either consumes registered state
// or retains a PendingDcc fill while allowing the dispatch to run.
const bool registered_metadata =
cache.TryConsumeDccFill(descriptor.Base48(), size, packed_clear);
static std::atomic<uint32_t> logged_metadata_clears {0};
if (logged_metadata_clears.fetch_add(1, std::memory_order_relaxed) < 32) {
LOGF("GraphicsRenderDispatchDirect: %s metadata clear shader=0x%016" PRIx64
" addr=0x%016" PRIx64 " size=0x%016" PRIx64 " value=0x%08" PRIx32 "\n",
registered_metadata ? "tracked" : "deferred", input.stage.program->shader_hash,
descriptor.Base48(), size, packed_clear);
}
if (registered_metadata) {
return true;
}
return false;
}
static std::atomic<uint32_t> logged_clears {0};
+97 -18
View File
@@ -340,6 +340,9 @@ static PipelineDynamicParameters BuildGraphicsDynamicParams(const RenderCommandB
framebuffer_extent = colors[0].extent;
} else if (depth.image_id) {
framebuffer_extent = {depth.width, depth.height};
} else {
const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits;
framebuffer_extent = {limits.maxFramebufferWidth, limits.maxFramebufferHeight};
}
const auto final_scissor = calc_final_scissor(vp, ctx.GetScanModeControl(), framebuffer_extent);
@@ -374,8 +377,8 @@ static PipelineDynamicParameters BuildGraphicsDynamicParams(const RenderCommandB
(use_front ? poly_offset.front_scale : poly_offset.back_scale) / 16.0f;
}
// CB_COLOR_CONTROL.operation controls special CB operations, not the normal color component
// write mask. Use CB_TARGET_MASK here so scanout passes with mode=Disable do not go black.
// Color-control operation selects special color-buffer paths, not the normal component write
// mask. Attachment availability therefore follows the target write mask.
for (uint32_t i = 0; i < RENDER_COLOR_ATTACHMENTS_MAX; i++) {
ret.color_write_enable[i] =
(i < color_count &&
@@ -549,6 +552,71 @@ struct DrawCallInfo {
uint32_t first_instance = 0;
};
static bool ResolveDccAttachmentClear(TextureCache& cache, const RenderColorInfo& target,
const ImageViewInfo& view,
vk::ClearColorValue& clear_value) {
if (target.desc.info.metadata.kind != ImageMetadataKind::Dcc) {
return false;
}
// A DCC fast clear may update metadata only and leave the color allocation stale. Vulkan has
// no guest DCC state, so materialize the deferred value when the surface is next bound.
const auto metadata_address = target.desc.info.metadata.range.address;
uint32_t metadata_value = 0xffffffffu;
if (!cache.IsMetaCleared(metadata_address, view.base_layer, &metadata_value)) {
return false;
}
// Translate recognized constant and register-backed DCC states into a host clear value.
clear_value = {};
switch (static_cast<uint8_t>(metadata_value)) {
case 0x00: break;
case 0x20:
if (!target.metadata_clear_supported) {
return false;
}
clear_value = target.color_clear_value;
break;
case 0x40:
if (!target.metadata_fixed_clear_supported) {
return false;
}
clear_value.float32[3] = 1.0f;
break;
case 0x80:
if (!target.metadata_fixed_clear_supported) {
return false;
}
clear_value.float32[0] = 1.0f;
clear_value.float32[1] = 1.0f;
clear_value.float32[2] = 1.0f;
break;
case 0xc0:
if (!target.metadata_fixed_clear_supported) {
return false;
}
clear_value.float32[0] = 1.0f;
clear_value.float32[1] = 1.0f;
clear_value.float32[2] = 1.0f;
clear_value.float32[3] = 1.0f;
break;
default: return false;
}
for (uint32_t layer = 1; layer < view.layer_count; layer++) {
if (!cache.IsMetaCleared(metadata_address, view.base_layer + layer)) {
return false;
}
}
// Consume only after every layer in this Vulkan view can be materialized together.
for (uint32_t layer = 0; layer < view.layer_count; layer++) {
if (!cache.TouchMeta(metadata_address, view.base_layer + layer, false)) {
EXIT("failed to consume DCC clear state\n");
}
}
return true;
}
RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderColorInfo* colors,
uint32_t color_count, RenderDepthInfo& depth) {
EXIT_IF(colors == nullptr || color_count > RENDER_COLOR_ATTACHMENTS_MAX);
@@ -589,14 +657,20 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
ImageSubresourceRange {view.base_level, view.level_count, view.base_layer,
view.layer_count},
buffer.Handle());
state.width = std::min(state.width, target.extent.width);
state.height = std::min(state.height, target.extent.height);
state.num_layers = std::min(state.num_layers, view.layer_count);
auto& attachment = state.color_attachments[i];
attachment.image_view = target.image_view;
attachment.image_layout = layout;
attachment.clear_value = target.color_clear_value.uint32;
attachment.is_clear = target.color_clear_enable;
state.width = std::min(state.width, target.extent.width);
state.height = std::min(state.height, target.extent.height);
state.num_layers = std::min(state.num_layers, view.layer_count);
auto& attachment = state.color_attachments[i];
attachment.image_view = target.image_view;
attachment.image_layout = layout;
attachment.clear_value = target.color_clear_value.uint32;
vk::ClearColorValue metadata_clear_value {};
const bool metadata_clear =
ResolveDccAttachmentClear(cache, target, view, metadata_clear_value);
if (metadata_clear) {
attachment.clear_value = metadata_clear_value.uint32;
}
attachment.is_clear = target.color_clear_enable || metadata_clear;
}
if (depth.image_id) {
const auto owner = cache.ResolveOwner(depth.image_id);
@@ -648,8 +722,12 @@ RenderState RenderExecutor::AcquireRenderTargets(CommandBuffer& buffer, RenderCo
attachment.has_stencil = static_cast<bool>(aspects & vk::ImageAspectFlagBits::eStencil);
attachment.stencil_clear = depth.stencil_clear_enable;
}
if (attachment_samples == 0 ||
vulkan_sample_count(attachment_samples) == vk::SampleCountFlagBits {}) {
if (color_count == 0 && !depth.image_id) {
const auto& limits = buffer.GetGraphics().GetPhysicalDeviceProperties().limits;
state.width = limits.maxFramebufferWidth;
state.height = limits.maxFramebufferHeight;
} else if (attachment_samples == 0 ||
vulkan_sample_count(attachment_samples) == vk::SampleCountFlagBits {}) {
EXIT("render state has no valid attachments\n");
}
if (state.num_layers == std::numeric_limits<uint32_t>::max()) {
@@ -680,10 +758,11 @@ enum class CbColorMode : uint8_t {
static bool ConsumeMetadataColorOperation(const RenderCommandBuffer& buffer) {
const auto& ctx = buffer.GetRegisters();
const auto mode = ctx.GetColorControl().mode;
// These AGC CB modes run color-buffer metadata/decompression operations. The shader is a
// dummy vehicle for the CB, and its exported color must not be applied as a normal draw.
// Kyty currently stores host images as expanded Vulkan images and does not track CMASK/DCC
// metadata state, so the matching host operation is a no-op.
// These special modes run color-buffer metadata or decompression operations. The shader is a
// vehicle for that operation, and its exported color must not be applied as a normal draw.
// Kyty stores expanded Vulkan images rather than compressed guest surfaces, so no equivalent
// hardware pass is emitted. Tracked DCC clear state is materialized on attachment bind;
// future CMask/FMask support can consume its state through the same TextureCache path.
return mode == static_cast<uint8_t>(CbColorMode::EliminateFastClear) ||
mode == static_cast<uint8_t>(CbColorMode::FmaskDecompress) ||
mode == static_cast<uint8_t>(CbColorMode::DccDecompress);
@@ -941,14 +1020,14 @@ bool RenderExecutor::PrepareDrawRenderState(uint64_t submit_id, RenderCommandBuf
}
ResolveRenderDepthTarget(submit_id, buffer, state.depth_info);
state.ps_active = DrawHasActivePixelShader(buffer);
const bool with_depth = (state.depth_info.format != vk::Format::eUndefined &&
static_cast<bool>(state.depth_info.image_id));
if (state.color_count == 0 && !with_depth) {
if (state.color_count == 0 && !with_depth && !state.ps_active) {
LogFramebufferSkip(draw.name, state.color_info[0], state.depth_info, buffer,
draw.index_count, draw.flags);
return false;
}
state.ps_active = DrawHasActivePixelShader(buffer);
return true;
}
@@ -554,6 +554,14 @@ static vk::Device VulkanCreateDevice(vk::PhysicalDevice physical_device, const V
EXIT_NOT_IMPLEMENTED(supported_features2.features.depthBiasClamp != VK_TRUE);
features12.timelineSemaphore = VK_TRUE;
if (Config::GpuAssistedValidationEnabled()) {
if (supported_features12.bufferDeviceAddress != VK_TRUE) {
EXIT("--gpu-assisted-validation requires bufferDeviceAddress, unsupported by this "
"device\n");
}
features12.bufferDeviceAddress = VK_TRUE;
}
vk::PhysicalDeviceFeatures device_features {};
device_features.fragmentStoresAndAtomics = VK_TRUE;
device_features.samplerAnisotropy = VK_TRUE;
@@ -571,6 +579,9 @@ static vk::Device VulkanCreateDevice(vk::PhysicalDevice physical_device, const V
graphics.sample_rate_shading_enabled = true;
device_features.vertexPipelineStoresAndAtomics =
supported_features2.features.vertexPipelineStoresAndAtomics;
if (Config::GpuAssistedValidationEnabled()) {
device_features.shaderInt64 = supported_features2.features.shaderInt64;
}
if (graphics.subgroup_size_control_enabled &&
supported_features13.subgroupSizeControl == VK_TRUE) {
@@ -833,23 +844,26 @@ void WindowContext::CreateVulkan() {
app_info.engineVersion = 1;
app_info.apiVersion = VULKAN_TARGET_API_VERSION; // NOLINT
vk::ValidationFeatureEnableEXT enabled_features[] = {
if (Config::SpirvDebugPrintfEnabled() && Config::GpuAssistedValidationEnabled()) {
EXIT("--spirv-debug-printf and --gpu-assisted-validation are mutually exclusive\n");
}
vk::ValidationFeatureEnableEXT enabled_features[3] = {};
uint32_t enabled_features_count = 0;
#ifdef KYTY_ENABLE_BEST_PRACTICES
vk::ValidationFeatureEnableEXT::eBestPractices,
enabled_features[enabled_features_count++] = vk::ValidationFeatureEnableEXT::eBestPractices;
#endif
#ifdef KYTY_ENABLE_DEBUG_PRINTF
vk::ValidationFeatureEnableEXT::eDebugPrintf,
#endif
};
uint32_t enabled_features_count =
sizeof(enabled_features) / sizeof(vk::ValidationFeatureEnableEXT);
#ifdef KYTY_ENABLE_DEBUG_PRINTF
if (!Config::SpirvDebugPrintfEnabled()) {
enabled_features_count--;
if (Config::SpirvDebugPrintfEnabled()) {
enabled_features[enabled_features_count++] = vk::ValidationFeatureEnableEXT::eDebugPrintf;
}
#endif
if (Config::GpuAssistedValidationEnabled()) {
enabled_features[enabled_features_count++] = vk::ValidationFeatureEnableEXT::eGpuAssisted;
LOGF("Vulkan GPU-assisted validation is enabled; expect a large slowdown\n");
std::printf("Vulkan GPU-assisted validation is enabled; expect a large slowdown\n");
std::fflush(stdout);
}
vk::ValidationFeaturesEXT validation_features {};
validation_features.sType = vk::StructureType::eValidationFeaturesEXT;
@@ -1,7 +1,6 @@
#include "graphics/shader/recompiler/frontend/cfg/ShaderCFG.h"
#include <algorithm>
#include <array>
#include <fmt/format.h>
#include <iterator>
#include <map>
@@ -1079,11 +1078,96 @@ bool IsLoopControlGateway(const Graph& graph, const NaturalLoop& loop, uint32_t
is_control_target(block->terminator.false_block);
}
bool HasLinearPathTo(const Graph& graph, uint32_t start, uint32_t target) {
std::vector<bool> visited(graph.blocks.size(), false);
for (auto block_id = start; block_id != target;) {
const auto* block = graph.FindBlock(block_id);
if (block == nullptr || block_id >= visited.size() || visited[block_id] ||
block->successors.size() != 1) {
return false;
}
visited[block_id] = true;
block_id = block->successors.front();
}
return true;
}
bool HasLinearPathToTerminal(const Graph& graph, uint32_t start) {
std::vector<bool> visited(graph.blocks.size(), false);
for (auto block_id = start;;) {
const auto* block = graph.FindBlock(block_id);
if (block == nullptr || block_id >= visited.size() || visited[block_id]) {
return false;
}
if (block->successors.empty()) {
return true;
}
if (block->successors.size() != 1) {
return false;
}
visited[block_id] = true;
block_id = block->successors.front();
}
}
bool IsEnclosingLinearExit(const Graph& graph, uint32_t header, uint32_t block_id) {
// AGC commonly lowers nested early returns through a terminal epilogue shared with an
// enclosing conditional. Such a path is an exit boundary, not part of the inner selection.
const auto* block = graph.FindBlock(block_id);
if (block == nullptr || graph.Dominates(header, block_id) ||
!HasLinearPathToTerminal(graph, block_id) || block->predecessors.empty()) {
return false;
}
return std::ranges::all_of(block->predecessors, [&](uint32_t predecessor) {
return graph.Dominates(header, predecessor) || graph.Dominates(predecessor, header);
});
}
bool CanReachBefore(const Graph& graph, uint32_t start, uint32_t target, uint32_t stop) {
std::vector<uint32_t> pending = {start};
std::vector<bool> visited(graph.blocks.size(), false);
while (!pending.empty()) {
const auto block_id = pending.back();
pending.pop_back();
if (block_id == target) {
return true;
}
if (block_id == stop || block_id >= visited.size() || visited[block_id]) {
continue;
}
visited[block_id] = true;
const auto* block = graph.FindBlock(block_id);
if (block != nullptr) {
pending.insert(pending.end(), block->successors.begin(), block->successors.end());
}
}
return false;
}
uint32_t FindSelectionMerge(const Graph& graph, const BasicBlock& block) {
const auto global_merge = graph.FindNearestCommonPostDominator(block.terminator.true_block,
block.terminator.false_block);
const auto* loop = FindInnermostContainingLoop(graph, block.id);
if (loop == nullptr) {
const auto* global_block = graph.FindBlock(global_merge);
const auto true_target = block.terminator.true_block;
const auto false_target = block.terminator.false_block;
if (global_block != nullptr && global_block->successors.empty()) {
// A terminal common post-dominator can hide the local continuation of a nested
// early-exit selection. Prefer that continuation as the structured merge.
const bool true_exits = HasLinearPathTo(graph, true_target, global_merge);
const bool false_exits = HasLinearPathTo(graph, false_target, global_merge);
if (true_exits != false_exits) {
return true_exits ? false_target : true_target;
}
const bool false_reaches_true =
CanReachBefore(graph, false_target, true_target, global_merge);
const bool true_reaches_false =
CanReachBefore(graph, true_target, false_target, global_merge);
if (false_reaches_true != true_reaches_false) {
return false_reaches_true ? true_target : false_target;
}
}
return global_merge;
}
@@ -1269,17 +1353,24 @@ std::vector<uint32_t> SelectionRegion(const Graph& graph, const BasicBlock& head
std::vector<uint32_t> region;
std::vector<uint32_t> pending = {header.terminator.true_block, header.terminator.false_block};
const auto* loop = FindInnermostContainingLoop(graph, header.id);
const auto global_merge = graph.FindNearestCommonPostDominator(header.terminator.true_block,
header.terminator.false_block);
while (!pending.empty()) {
const auto block_id = pending.back();
pending.pop_back();
if (block_id == merge || Contains(region, block_id) ||
(loop != nullptr && (block_id == loop->merge || block_id == loop->continue_block))) {
(loop != nullptr && (block_id == loop->merge || block_id == loop->continue_block)) ||
IsEnclosingLinearExit(graph, header.id, block_id) ||
(block_id == global_merge && HasLinearPathToTerminal(graph, block_id))) {
continue;
}
const auto* block = graph.FindBlock(block_id);
if (block == nullptr) {
continue;
}
if (block->successors.empty()) {
continue;
}
AddUnique(region, block_id);
pending.insert(pending.end(), block->successors.begin(), block->successors.end());
}
@@ -1449,8 +1540,8 @@ uint32_t Graph::FindNearestCommonPostDominator(uint32_t block_a, uint32_t block_
namespace {
struct GotoRouteBlocks {
uint32_t before = UINT32_MAX;
std::array<uint32_t, 5> blocks {};
uint32_t before = UINT32_MAX;
std::vector<uint32_t> blocks;
};
void PlaceGotoRouteBlocks(Graph& graph, uint32_t original_block_count,
@@ -1514,6 +1605,48 @@ uint32_t AppendGotoSetBlock(Graph& graph, uint32_t route_variable, bool value, u
return block_id;
}
void AppendEnclosingRouteBlocks(Graph& graph, uint32_t original_block_count,
uint32_t route_variable, uint32_t shared, uint32_t other,
uint32_t route_select, uint32_t route_root,
std::vector<uint32_t>& route_blocks) {
for (uint32_t depth = 0; depth < original_block_count; depth++) {
uint32_t predecessor = UINT32_MAX;
uint32_t leaf = UINT32_MAX;
for (uint32_t candidate = 0; candidate < original_block_count; candidate++) {
const auto* candidate_block = graph.FindBlock(candidate);
if (candidate_block == nullptr ||
candidate_block->terminator.kind != TerminatorKind::ConditionalBranch) {
continue;
}
const auto candidate_true = candidate_block->terminator.true_block;
const auto candidate_false = candidate_block->terminator.false_block;
if (candidate_true == route_root &&
(candidate_false == shared || candidate_false == other)) {
predecessor = candidate;
leaf = candidate_false;
break;
}
if (candidate_false == route_root &&
(candidate_true == shared || candidate_true == other)) {
predecessor = candidate;
leaf = candidate_true;
break;
}
}
if (predecessor == UINT32_MAX) {
break;
}
const auto routed_leaf =
AppendGotoSetBlock(graph, route_variable, leaf == other, route_select);
auto* predecessor_block = graph.FindBlock(predecessor);
ReplaceValue(predecessor_block->successors, leaf, routed_leaf);
ReplaceTerminatorTarget(predecessor_block->terminator, leaf, routed_leaf);
route_blocks.push_back(routed_leaf);
route_root = predecessor;
}
}
// Turn H0 -> shared/H1, H1 -> shared/other into nested selections whose empty
// forwarding blocks set one typed SSA route value. Guest semantic blocks remain unique.
bool RouteOneSharedArm(Graph& graph, uint32_t original_block_count, uint32_t outer_id,
@@ -1538,66 +1671,120 @@ bool RouteOneSharedArm(Graph& graph, uint32_t original_block_count, uint32_t out
const bool true_is_shared = inner->terminator.true_block == shared;
const bool false_is_shared = inner->terminator.false_block == shared;
if (true_is_shared == false_is_shared) {
continue;
}
const auto other =
true_is_shared ? inner->terminator.false_block : inner->terminator.true_block;
if (other == outer_id || other == inner_id || other == shared) {
continue;
}
const auto first_arm = std::min(shared, other);
if (first_arm >= original_block_count || outer_id >= inner_id || inner_id >= first_arm) {
continue;
}
if (graph.FindNearestCommonPostDominator(shared, other) == UINT32_MAX) {
continue;
if (true_is_shared != false_is_shared) {
const auto other =
true_is_shared ? inner->terminator.false_block : inner->terminator.true_block;
if (other == outer_id || other == inner_id || other == shared) {
continue;
}
const auto first_arm = std::min(shared, other);
if (first_arm >= original_block_count || outer_id >= inner_id ||
inner_id >= first_arm ||
graph.FindNearestCommonPostDominator(shared, other) == UINT32_MAX) {
continue;
}
const auto route_select = AppendGotoSelectBlock(graph, route_variable, other, shared);
const auto inner_merge = AppendSyntheticBranchBlock(graph, route_select);
const auto outer_shared =
AppendGotoSetBlock(graph, route_variable, false, route_select);
const auto inner_shared = AppendGotoSetBlock(graph, route_variable, false, inner_merge);
const auto inner_other = AppendGotoSetBlock(graph, route_variable, true, inner_merge);
outer = graph.FindBlock(outer_id);
auto* mutable_inner = graph.FindBlock(inner_id);
outer->terminator.true_block = inner_is_true ? inner_id : outer_shared;
outer->terminator.false_block = inner_is_true ? outer_shared : inner_id;
outer->successors = {outer->terminator.true_block, outer->terminator.false_block};
mutable_inner->terminator.true_block = true_is_shared ? inner_shared : inner_other;
mutable_inner->terminator.false_block = true_is_shared ? inner_other : inner_shared;
mutable_inner->successors = {mutable_inner->terminator.true_block,
mutable_inner->terminator.false_block};
std::vector<uint32_t> route_blocks = {inner_shared, inner_other, inner_merge,
outer_shared};
AppendEnclosingRouteBlocks(graph, original_block_count, route_variable, shared, other,
route_select, outer_id, route_blocks);
route_blocks.push_back(route_select);
route = {first_arm, std::move(route_blocks)};
return true;
}
const auto route_select = AppendGotoSelectBlock(graph, route_variable, other, shared);
const auto inner_merge = AppendSyntheticBranchBlock(graph, route_select);
const auto outer_shared = AppendGotoSetBlock(graph, route_variable, false, route_select);
const auto inner_shared = AppendGotoSetBlock(graph, route_variable, false, inner_merge);
const auto inner_other = AppendGotoSetBlock(graph, route_variable, true, inner_merge);
// H0 -> continuation/H1, H1 -> exit/body, body -> continuation. Route both
// destinations after joining H0 and H1 so neither selection has a nonlocal edge.
for (const bool exit_is_true: {true, false}) {
const auto other =
exit_is_true ? inner->terminator.true_block : inner->terminator.false_block;
const auto body =
exit_is_true ? inner->terminator.false_block : inner->terminator.true_block;
if (other == outer_id || other == inner_id || other == shared || body == outer_id ||
body == inner_id || body == shared || !graph.Dominates(inner_id, body) ||
!graph.PostDominates(shared, body)) {
continue;
}
outer = graph.FindBlock(outer_id);
auto* mutable_inner = graph.FindBlock(inner_id);
outer->terminator.true_block = inner_is_true ? inner_id : outer_shared;
outer->terminator.false_block = inner_is_true ? outer_shared : inner_id;
outer->successors = {outer->terminator.true_block, outer->terminator.false_block};
mutable_inner->terminator.true_block = true_is_shared ? inner_shared : inner_other;
mutable_inner->terminator.false_block = true_is_shared ? inner_other : inner_shared;
mutable_inner->successors = {mutable_inner->terminator.true_block,
mutable_inner->terminator.false_block};
route = {first_arm, {inner_shared, inner_other, inner_merge, outer_shared, route_select}};
return true;
std::vector<uint32_t> inner_shared_predecessors;
const auto* shared_block = graph.FindBlock(shared);
for (const auto predecessor: shared_block->predecessors) {
if (predecessor != inner_id && graph.Dominates(inner_id, predecessor)) {
inner_shared_predecessors.push_back(predecessor);
}
}
const auto first_arm = std::min(shared, other);
if (inner_shared_predecessors.empty() || first_arm >= original_block_count ||
outer_id >= inner_id || inner_id >= first_arm ||
graph.FindNearestCommonPostDominator(shared, other) == UINT32_MAX) {
continue;
}
const auto route_select = AppendGotoSelectBlock(graph, route_variable, other, shared);
const auto inner_merge = AppendSyntheticBranchBlock(graph, route_select);
const auto outer_shared =
AppendGotoSetBlock(graph, route_variable, false, route_select);
const auto inner_shared = AppendGotoSetBlock(graph, route_variable, false, inner_merge);
const auto inner_other = AppendGotoSetBlock(graph, route_variable, true, inner_merge);
outer = graph.FindBlock(outer_id);
auto* mutable_inner = graph.FindBlock(inner_id);
outer->terminator.true_block = inner_is_true ? inner_id : outer_shared;
outer->terminator.false_block = inner_is_true ? outer_shared : inner_id;
outer->successors = {outer->terminator.true_block, outer->terminator.false_block};
if (exit_is_true) {
mutable_inner->terminator.true_block = inner_other;
} else {
mutable_inner->terminator.false_block = inner_other;
}
ReplaceValue(mutable_inner->successors, other, inner_other);
for (const auto predecessor: inner_shared_predecessors) {
auto* predecessor_block = graph.FindBlock(predecessor);
ReplaceValue(predecessor_block->successors, shared, inner_shared);
ReplaceTerminatorTarget(predecessor_block->terminator, shared, inner_shared);
}
std::vector<uint32_t> route_blocks = {inner_shared, inner_other, inner_merge,
outer_shared};
AppendEnclosingRouteBlocks(graph, original_block_count, route_variable, shared, other,
route_select, outer_id, route_blocks);
route_blocks.push_back(route_select);
route = {first_arm, std::move(route_blocks)};
return true;
}
}
return false;
}
bool RouteSharedSelectionArms(Graph& graph) {
bool RouteSharedSelectionArm(Graph& graph, uint32_t route_variable) {
if (graph.irreducible) {
return false;
}
const auto route_budget = static_cast<uint32_t>(graph.blocks.size());
bool found = false;
for (uint32_t route_variable = 0; route_variable < route_budget; route_variable++) {
const auto block_count = static_cast<uint32_t>(graph.blocks.size());
bool routed = false;
for (uint32_t block_id = 0; block_id < block_count; block_id++) {
GotoRouteBlocks route;
if (!RouteOneSharedArm(graph, block_count, block_id, route_variable, route)) {
continue;
}
const auto block_count = static_cast<uint32_t>(graph.blocks.size());
for (uint32_t block_id = 0; block_id < block_count; block_id++) {
GotoRouteBlocks route;
if (RouteOneSharedArm(graph, block_count, block_id, route_variable, route)) {
PlaceGotoRouteBlocks(graph, block_count, route);
RebuildPredecessors(graph);
RecomputeAnalyses(graph);
found = true;
routed = true;
break;
}
if (!routed) {
return found;
return true;
}
}
return false;
@@ -1922,17 +2109,25 @@ bool Structurize(Graph& graph, std::string* error) {
if (StructurizeImpl(graph, error)) {
return true;
}
if (!RouteSharedSelectionArms(original)) {
return false;
}
Graph failed_graph = std::move(graph);
std::string failed_error = error != nullptr ? *error : std::string {};
graph = std::move(original);
if (error != nullptr) {
error->clear();
}
if (StructurizeImpl(graph, error)) {
return true;
const auto route_budget = static_cast<uint32_t>(graph.blocks.size());
// Apply one route at a time and retry. Eagerly routing every matching diamond can
// rewrite unrelated selections that were already structurally valid.
for (uint32_t route_variable = 0; route_variable < route_budget; route_variable++) {
if (!RouteSharedSelectionArm(graph, route_variable)) {
break;
}
Graph routed = graph;
if (error != nullptr) {
error->clear();
}
if (StructurizeImpl(routed, error)) {
graph = std::move(routed);
return true;
}
}
graph = std::move(failed_graph);
if (error != nullptr) {
@@ -1,6 +1,7 @@
#ifndef EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_DECOMPILER_OPCODETABLE_H_
#define EMULATOR_INCLUDE_EMULATOR_GRAPHICS_SHADER_RECOMPILER_DECOMPILER_OPCODETABLE_H_
#include "common/assert.h"
#include "graphics/shader/recompiler/frontend/decode/ShaderDecoder.h"
#include <array>
@@ -27,7 +28,7 @@ consteval auto MakeOpcodeTable(const Entry (&entries)[EntryCount]) {
for (size_t i = 0; i < EntryCount; i++) {
const auto encoding = entries[i].encoding;
if (encoding >= EncodingCount || table.indices[encoding] != 0) {
throw "invalid opcode table";
EXIT("invalid opcode table\n");
}
table.entries[i] = entries[i];
table.indices[encoding] = static_cast<uint16_t>(i + 1);
@@ -321,7 +321,7 @@ IR::U64 Translator::ExpandWholeQuadMask(IR::U64 value) {
const auto merged2 = ir.Emit(IR::ValueOpcode::BitwiseOr64, {merged1, shifted2});
const auto merged3 = ir.Emit(IR::ValueOpcode::BitwiseOr64, {merged2, shifted3});
const auto low_bits =
ir.Emit(IR::ValueOpcode::BitwiseAnd64, {merged3, IR::Value(0x1111111111111111ull)});
ir.Emit(IR::ValueOpcode::BitwiseAnd64, {merged3, IR::Value(uint64_t {0x1111111111111111})});
return IR::U64(ir.Emit(IR::ValueOpcode::IMul64, {low_bits, IR::Value(uint64_t {0xfull})}));
}
+1 -4
View File
@@ -117,12 +117,11 @@ struct ShaderStageProgramKey {
uint64_t shader_hash = 0;
ShaderId program_id;
Config::ShaderOptimizationType optimization_type = Config::ShaderOptimizationType::None;
bool validation = false;
bool operator==(const ShaderStageProgramKey& other) const {
return stage == other.stage && lane_mask_mode == other.lane_mask_mode &&
shader_hash == other.shader_hash && program_id == other.program_id &&
optimization_type == other.optimization_type && validation == other.validation;
optimization_type == other.optimization_type;
}
};
@@ -143,7 +142,6 @@ struct ShaderStageProgramKeyHash {
mix(value);
}
mix(static_cast<uint32_t>(key.optimization_type));
mix(key.validation ? 1u : 0u);
return hash;
}
};
@@ -917,7 +915,6 @@ static ShaderStageProgramKey MakeShaderStageProgramKey(ShaderType stage, uint64_
key.shader_hash = shader_hash;
key.program_id = program_id;
key.optimization_type = Config::GetShaderOptimizationType();
key.validation = Config::ShaderValidationEnabled();
return key;
}
+4 -4
View File
@@ -180,10 +180,10 @@ void MainDialogPrivate::FindInterpreter() {
return;
}
if (!m_ui->widget->EnsureGameDirectory()) {
QApplication::quit();
return;
}
// Prompt for a game folder when none are configured, but keep the launcher
// open if the user dismisses the dialog (quitting here can segfault during
// nested modal shutdown / background compatibility load).
m_ui->widget->EnsureGameDirectory();
m_ui->label_settings_file->setText(tr("Settings file: ") + m_ui->widget->GetSettingsFile());
+25 -2
View File
@@ -59,8 +59,15 @@ void PatchesDialog::Load() {
return;
}
const auto patches =
QJsonDocument::fromJson(file.readAll()).object().value(QStringLiteral("patches")).toArray();
const auto document = QJsonDocument::fromJson(file.readAll());
if (document.isNull() || !document.isObject()) {
m_status->setText(tr("Invalid patch JSON in %1.").arg(file.fileName()));
m_apply->setEnabled(false);
return;
}
const auto root = document.object();
const auto patches = root.value(QStringLiteral("patches")).toArray();
for (const auto& value: patches) {
const auto patch = value.toObject();
auto* item = new QListWidgetItem(patch.value(QStringLiteral("name")).toString(), m_patches);
@@ -70,6 +77,22 @@ void PatchesDialog::Load() {
}
m_apply->setEnabled(!patches.isEmpty());
if (patches.isEmpty()) {
// shadPS4/ETAHen cheat files use "mods"; Kyty only reads a "patches" array.
if (root.contains(QStringLiteral("mods"))) {
m_status->setText(
tr("This file uses a \"mods\" list. Kyty expects a top-level \"patches\" "
"array in %1.")
.arg(file.fileName()));
} else if (!root.contains(QStringLiteral("patches"))) {
m_status->setText(
tr("No \"patches\" array in %1.").arg(file.fileName()));
} else {
m_status->setText(tr("Loaded 0 patch(es) from %1.").arg(file.fileName()));
}
return;
}
m_status->setText(tr("Loaded %1 patch(es) from %2.").arg(patches.size()).arg(file.fileName()));
}
+11
View File
@@ -51,6 +51,8 @@ static void PrintUsage() {
::printf(" --vblank-frequency <num> Virtual vblank frequency. Default: 60.\n");
::printf(" --console-language <0-29> Console language. Default: 1 (English US).\n");
::printf(" --vulkan-validation <true|false> Enable Vulkan validation.\n");
::printf(" --gpu-assisted-validation <t|f> Bounds-check shader accesses on the GPU.\n"
" Implies --vulkan-validation; very slow.\n");
::printf(" --shader-validation <true|false> Enable shader validation.\n");
::printf(" --shader-optimization-type <value> None, Size, or Performance.\n");
::printf(" --shader-log-direction <value> Silent, Console, or File.\n");
@@ -213,6 +215,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--gpu-assisted-validation") {
if (!ParseBool(value, options.config.gpu_assisted_validation_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
return false;
}
} else if (arg == "--shader-validation") {
if (!ParseBool(value, options.config.shader_validation_enabled)) {
::printf("invalid boolean for %s: %s\n", arg.c_str(), value.c_str());
@@ -277,6 +284,10 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
}
}
if (options.config.gpu_assisted_validation_enabled) {
options.config.vulkan_validation_enabled = true;
}
return show_help || (!options.app0_dir.empty() && !options.elf.empty());
}
+43 -2
View File
@@ -4542,10 +4542,15 @@ public:
};
auto metadata_depth_a = MakeMetadataDepth(metadata_data_a, metadata_a);
auto metadata_depth_b = MakeMetadataDepth(metadata_data_b, metadata_b);
metadata_depth_a.info.htile_clear_mask = 0;
const auto metadata_depth_a_id =
texture_cache.FindImage(metadata_depth_a);
const auto metadata_depth_b_id =
texture_cache.FindImage(metadata_depth_b);
Require(name, "pending DCC/HTile separation",
!texture_cache.TryConsumeDccFill(base + metadata_a, 0x80, 0) &&
!texture_cache.IsMeta(base + metadata_a),
"an unclassified PS5 DCC fill was exposed as registered metadata");
const auto metadata_depth_a_view =
texture_cache.FindDepthTarget(metadata_depth_a_id, metadata_depth_a);
const auto metadata_depth_b_view =
@@ -4553,6 +4558,7 @@ public:
Require(name, "shared-page metadata state",
metadata_depth_a_view != nullptr &&
metadata_depth_b_view != nullptr &&
!texture_cache.IsMetaCleared(base + metadata_a, 0) &&
texture_cache.ClearMeta(base + metadata_a) &&
texture_cache.ClearMeta(base + metadata_b),
"shared-page metadata did not retain logical clear state");
@@ -4576,6 +4582,11 @@ public:
texture_cache.IsMeta(base + metadata_b) &&
texture_cache.IsMetaCleared(base + metadata_b, 0),
"image discovery retired aliased metadata or its live depth image");
Require(name, "registered DCC/HTile separation",
texture_cache.TouchMeta(base + metadata_a, 0, false) &&
!texture_cache.TryConsumeDccFill(base + metadata_a, 0x80, 0) &&
!texture_cache.IsMetaCleared(base + metadata_a, 0),
"a PS5 DCC fill changed registered HTile state");
Require(
name, "metadata first-touch state",
texture_cache.TouchMeta(base + metadata_b, 0, true) &&
@@ -6313,7 +6324,8 @@ public:
HW::Shader shaders{};
registers.SetColorBase(0, {.addr = base});
registers.SetColorInfo(
0, {.format = Prospero::ChannelLayout::k10_10_10_2,
0, {.dcc_compression_enable = true,
.format = Prospero::ChannelLayout::k10_10_10_2,
.channel_type = Prospero::ChannelType::kUNorm,
.channel_order = Prospero::ChannelOrder::kStandard});
registers.SetColorAttrib2(0, {.height = 31, .width = 31});
@@ -6323,6 +6335,10 @@ public:
.dimension = 2,
.cmask_pipe_aligned = true,
.dcc_pipe_aligned = true});
constexpr uint64_t dcc_address = base + allocation_size - 0x1000;
registers.SetColorDccAddr(0, {.addr = dcc_address});
registers.SetColorClearWord0(0, {.word0 = 0});
registers.SetColorClearWord1(0, {.word1 = 0});
registers.SetRenderTargetMask(0x0f);
scheduler.Begin(registers, user_config, shaders);
@@ -6330,6 +6346,10 @@ public:
auto &texture_cache = resources.GetTextureCache();
auto &executor = context.GetRenderExecutor();
resources.MapMemory(base, allocation_size);
Require(name, "deferred DCC clear",
!texture_cache.TryConsumeDccFill(dcc_address, 0x1000, 0) &&
!texture_cache.IsMeta(dcc_address),
"a pre-registration DCC clear was not deferred");
RenderColorInfo color{};
RenderExecutorTestAccess::ResolveRenderColorTarget(
@@ -6337,6 +6357,7 @@ public:
const auto attachment =
texture_cache.FindRenderTarget(color.image_id, color.desc);
const auto &image = texture_cache.GetImage(color.image_id);
uint32_t dcc_clear_value = 0xffffffffu;
Require(
name, "captured 3D target",
color.image_id && attachment != nullptr &&
@@ -6346,6 +6367,13 @@ public:
color.desc.info.pitch == 128 &&
color.desc.info.data.size == allocation_size &&
color.desc.info.mip_layout[0].size == 0x10000 &&
color.desc.info.metadata.kind == ImageMetadataKind::Dcc &&
color.desc.info.metadata.range.address == dcc_address &&
color.metadata_clear_supported &&
texture_cache.IsMetaCleared(dcc_address, 7,
&dcc_clear_value) &&
dcc_clear_value == 0 &&
!texture_cache.ClearMeta(dcc_address) &&
color.desc.view_info.type == vk::ImageViewType::e2D &&
color.desc.view_info.layer_count == 1 &&
image.backing.image_type == vk::ImageType::e3D &&
@@ -6372,8 +6400,21 @@ public:
sliced_color.image_view != nullptr &&
sliced_color.desc.view_info.base_layer == 7 &&
sliced_rendering.num_color_attachments == 1 &&
sliced_rendering.num_layers == 1,
sliced_rendering.num_layers == 1 &&
sliced_rendering.color_attachments[0].is_clear &&
sliced_rendering.color_attachments[0].clear_value ==
vk::ClearColorValue {}.uint32 &&
!texture_cache.IsMetaCleared(dcc_address, 7),
"a nonzero 3D attachment slice was treated as a Vulkan array layer");
dcc_clear_value = 0;
Require(name, "registered DCC code state",
texture_cache.TryConsumeDccFill(dcc_address, 0x1000, 0xffffffffu) &&
!texture_cache.IsMetaCleared(dcc_address, 7,
&dcc_clear_value) &&
dcc_clear_value == 0xffffffffu &&
texture_cache.TryConsumeDccFill(dcc_address, 0x1000, 0) &&
texture_cache.IsMetaCleared(dcc_address, 7),
"DCC fill codes were not classified as clear or uncompressed");
auto storage_desc = color.desc;
storage_desc.type = BindingType::Storage;
+70 -16
View File
@@ -7147,30 +7147,83 @@ void TestNewShaderRecompilerCfgOverlappingEarlyExitLadder() {
const auto original_coverage =
CfgInstructionCoverage(graph, decoded.instructions.size());
const bool structured = ShaderRecompiler::CFG::Structurize(graph, &error);
Check(!structured && Common::ContainsStr(error, "externally entered region"),
"overlapping early-exit ladder should require dispatcher without "
"cloning");
Check(structured, error.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"failed structurization duplicated semantic instructions");
for (size_t index = original_block_count; index < graph.blocks.size();
index++) {
Check(graph.blocks[index].inst_begin == graph.blocks[index].inst_end,
"structurization added a semantic block clone");
}
"early-exit ladder routing changed semantic instruction coverage");
Check(graph.blocks.size() > original_block_count,
"early-exit ladder routing did not add forwarding blocks");
const auto route_selects = std::ranges::count_if(graph.blocks, [](const auto &block) {
return block.terminator.condition ==
ShaderRecompiler::CFG::BranchCondition::GotoVariable;
});
const auto route_sets = std::ranges::count_if(graph.blocks, [](const auto &block) {
return block.terminator.goto_value >= 0;
});
Check(route_selects != 0u && route_sets >= 3u,
"early-exit ladder lacks explicit typed route state");
auto options = MakeCompileOptions(ShaderType::Pixel);
options.dump_ir = true;
ShaderRecompiler::CompileResult result;
Check(ShaderRecompiler::TryRecompile(shader, options, result, &error),
error.c_str());
Check(result.program.dispatcher_fallback &&
Common::ContainsStr(result.ir_dump, "mode=dispatcher"),
"overlapping early-exit ladder did not select dispatcher");
Check(Common::ContainsStr(result.ir_dump, "externally entered region"),
"overlapping early-exit ladder lost its fallback reason");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) != 0u,
"overlapping early-exit dispatcher lacks OpSwitch");
Check(!result.program.dispatcher_fallback &&
Common::ContainsStr(result.ir_dump, "mode=structured"),
"overlapping early-exit ladder did not stay structured");
Check(SpirvInstructionOpcodeCount(result.spirv, 251) == 0u,
"overlapping early-exit ladder unexpectedly used dispatcher OpSwitch");
CheckSpirvBinaryValidates(result.spirv);
}
void TestNewShaderRecompilerCfgNestedEarlyExitSharedTerminal() {
const uint32_t shader[] = {
EncodeSopc(0x06, 0, 0), // enclosing condition
EncodeSopp(0x04, 4), // enclosing -> continuation or inner header
EncodeSopc(0x06, 1, 1), // inner early-exit condition
EncodeSopp(0x04, 4), // inner -> shared terminal or body
EncodeSMovB32(2, 129), // inner body
EncodeSopp(0x02, 0), // body -> enclosing continuation
EncodeSMovB32(3, 129), // enclosing continuation
EncodeSopp(0x02, 0), // continuation -> shared terminal
EncodeSMovB32(4, 129), // shared terminal epilogue
0xbf810000u,
};
ShaderRecompiler::Decoder::Program decoded;
std::string error;
Check(ShaderRecompiler::Decoder::DecodeProgram(std::span{shader}, decoded,
&error),
error.c_str());
ShaderRecompiler::CFG::Graph graph;
Check(ShaderRecompiler::CFG::BuildGraph(decoded, graph, &error),
error.c_str());
const auto original_coverage =
CfgInstructionCoverage(graph, decoded.instructions.size());
const bool structured = ShaderRecompiler::CFG::Structurize(graph, &error);
Check(structured, error.c_str());
Check(CfgInstructionCoverage(graph, decoded.instructions.size()) ==
original_coverage,
"nested early-exit structurization changed semantic coverage");
const auto route_selects = std::ranges::count_if(graph.blocks, [](const auto &block) {
return block.terminator.condition ==
ShaderRecompiler::CFG::BranchCondition::GotoVariable;
});
const auto route_sets = std::ranges::count_if(graph.blocks, [](const auto &block) {
return block.terminator.goto_value >= 0;
});
Check(route_selects == 1u && route_sets == 3u,
"nested early exit lacks complete typed route state");
auto options = MakeCompileOptions(ShaderType::Compute);
options.dump_ir = true;
ShaderRecompiler::CompileResult result;
Check(ShaderRecompiler::TryRecompile(shader, options, result, &error),
error.c_str());
Check(!result.program.dispatcher_fallback &&
Common::ContainsStr(result.ir_dump, "mode=structured") &&
SpirvInstructionOpcodeCount(result.spirv, 251) == 0u,
"nested early exit to a shared terminal did not stay structured");
CheckSpirvBinaryValidates(result.spirv);
}
@@ -9374,6 +9427,7 @@ int main() {
TestNewShaderRecompilerCfgExecSccSharedArm();
TestNewShaderRecompilerCfgLoopSharedRegion();
TestNewShaderRecompilerCfgOverlappingEarlyExitLadder();
TestNewShaderRecompilerCfgNestedEarlyExitSharedTerminal();
TestNewShaderRecompilerCfgExternallyEnteredSelectionDispatcher();
TestNewShaderRecompilerCfgIrreducibleDispatcher();
TestComputeShaderInputWaveSize();