Compare commits

...

5 Commits

Author SHA1 Message Date
nmzik e51de54ee4 add hdr display support 2026-10-11 20:29:32 +02:00
nmzik 984cfcebd1 shader: emit native texel offsets using sample execution context
Co-authored-by: netwarex <mail@pety.me>
2026-10-11 18:53:01 +02:00
nmzik df0412afb5 shader: preserve shared predicates during resource plan extraction
Co-authored-by: netwarex <mail@pety.me>
2026-10-11 18:53:01 +02:00
nmzik f28c0fccb2 texture: preserve guest DCC metadata for comparison depth aliases
Co-authored-by: netwarex <mail@pety.me>
2026-10-11 18:53:01 +02:00
netwarex 4c87dbebb2 shader: select indirect texture descriptors for image gathers 2026-10-11 18:53:01 +02:00
28 changed files with 599 additions and 178 deletions
+9 -10
View File
@@ -303,28 +303,27 @@ list(APPEND kyty_emulator_src "${fault_buffer_shader_header}")
list(APPEND kyty_emulator_src ${gpu_tiler_shader_headers})
set(gpu_blit_generated_dir "${PROJECT_BINARY_DIR}/gpu_blit_shaders")
set(gpu_blit_shader_sources
"${gpu_tiler_shader_dir}/gpu_blit_fs_triangle.vert"
"${gpu_tiler_shader_dir}/gpu_blit_color_to_ms_depth.frag"
"${gpu_tiler_shader_dir}/gpu_video_out_overlay.frag"
)
foreach(shader_source IN LISTS gpu_blit_shader_sources)
get_filename_component(shader_name "${shader_source}" NAME_WE)
function(add_gpu_blit_shader shader_name shader_file)
set(shader_source "${gpu_tiler_shader_dir}/${shader_file}")
set(shader_spv "${gpu_blit_generated_dir}/${shader_name}.spv")
set(shader_header "${gpu_blit_generated_dir}/${shader_name}_spv.h")
string(TOUPPER "${shader_name}_SPV" shader_symbol)
add_custom_command(
OUTPUT "${shader_header}"
COMMAND ${CMAKE_COMMAND} -E make_directory "${gpu_blit_generated_dir}"
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os
COMMAND "${KYTY_GLSLANG_VALIDATOR}" -V --target-env vulkan1.3 -Os ${ARGN}
"-I${gpu_tiler_shader_dir}" -o "${shader_spv}" "${shader_source}"
COMMAND ${CMAKE_COMMAND} -DINPUT=${shader_spv} -DOUTPUT=${shader_header}
-DSYMBOL=${shader_symbol} -P "${KYTY_SOURCE_DIR}/embed_spirv.cmake"
DEPENDS "${shader_source}"
VERBATIM
)
list(APPEND gpu_blit_shader_headers "${shader_header}")
endforeach()
set(gpu_blit_shader_headers ${gpu_blit_shader_headers} "${shader_header}" PARENT_SCOPE)
endfunction()
add_gpu_blit_shader(gpu_blit_fs_triangle gpu_blit_fs_triangle.vert)
add_gpu_blit_shader(gpu_blit_color_to_ms_depth gpu_blit_color_to_ms_depth.frag)
add_gpu_blit_shader(gpu_video_out_overlay gpu_video_out_overlay.frag)
add_gpu_blit_shader(gpu_imgui_hdr gpu_video_out_overlay.frag -DIMGUI_HDR=1)
list(APPEND kyty_emulator_src ${gpu_blit_shader_headers})
list(APPEND kyty_emulator_src
+4
View File
@@ -72,6 +72,10 @@ bool FullscreenEnabled() {
return g_config->fullscreen_enabled;
}
bool HdrEnabled() {
return g_config->hdr_enabled;
}
bool HideCursorEnabled() {
return g_config->hide_cursor_enabled;
}
+2
View File
@@ -53,6 +53,7 @@ struct ConfigOptions {
PresentMode present_mode = PresentMode::Mailbox;
int32_t gpu_index = -1;
bool fullscreen_enabled = false;
bool hdr_enabled = false;
bool hide_cursor_enabled = false;
bool splash_enabled = true;
bool vr_enabled = false;
@@ -100,6 +101,7 @@ uint32_t GetControllerVibrationIntensity();
PresentMode GetPresentMode();
int32_t GetGpuIndex();
bool FullscreenEnabled();
bool HdrEnabled();
bool HideCursorEnabled();
bool VrEnabled();
bool AmdCpuEnabled();
+12 -3
View File
@@ -36,7 +36,9 @@ constexpr uint64_t NumFramesBeforeRemoval = 32;
[[nodiscard]] bool DecodeColorClear(const TextureCache::ImageDesc& desc, uint8_t code,
vk::ClearColorValue& clear) {
const auto& metadata = desc.info.metadata;
const auto format = desc.view_info.format;
const auto format = desc.info.IsDepth()
? TextureGetSurfaceFormatInfo(desc.info.guest_format).vk_format
: desc.view_info.format;
const bool cmask = metadata.kind == ImageMetadataKind::Cmask;
if (cmask ? code == 0 : code == 0x20) {
// Register clears belong to the color buffer; the texture pipe cannot decode them.
@@ -1059,8 +1061,15 @@ void TextureCache::MaterializeColorClear(ImageId id, const ImageDesc& desc,
}
{
std::scoped_lock lock {m_lock};
ClearImage(m_scheduler.Current(), id, view.format,
{vk::ImageAspectFlagBits::eColor, view.base_level, view.level_count,
const auto& image = m_slot_images[id];
const bool depth = image.info.IsDepth();
if (depth) {
const auto value = clear.color.float32[0];
clear.depthStencil = {value, 0u};
}
ClearImage(m_scheduler.Current(), id, depth ? image.backing.format : view.format,
{depth ? vk::ImageAspectFlagBits::eDepth : vk::ImageAspectFlagBits::eColor,
view.base_level, view.level_count,
image_first + slice, 1}, clear);
}
// Publish the conversion's expanded keys without treating them as guest writes
@@ -362,8 +362,17 @@ struct VideoOutFormatPolicy {
};
inline constexpr uint64_t VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ = 0x8100070422000000ull;
inline constexpr uint64_t VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ = 0x8100070400000000ull;
inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {{
[[nodiscard]] constexpr bool IsHdrVideoOutPixelFormat(uint64_t format) {
switch (format) {
case VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ:
case VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ: return true;
default: return false;
}
}
inline constexpr std::array<VideoOutFormatPolicy, 8> VIDEO_OUT_FORMAT_POLICIES {{
{0x8000000022000000ull,
{vk::Format::eR8G8B8A8Srgb, Prospero::BufferFormat::k8_8_8_8Srgb, 4, false}},
{0x8000000000000000ull,
@@ -374,6 +383,8 @@ inline constexpr std::array<VideoOutFormatPolicy, 7> VIDEO_OUT_FORMAT_POLICIES {
{vk::Format::eA2R10G10B10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
{VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ,
{vk::Format::eA2B10G10R10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
{VIDEO_OUT_PIXEL_FORMAT_B10_G10_R10_A2_BT2100_PQ,
{vk::Format::eA2R10G10B10UnormPack32, Prospero::BufferFormat::k10_10_10_2UNorm, 4, false}},
{0xc001000622000000ull,
{vk::Format::eR16G16B16A16Sfloat, Prospero::BufferFormat::k16_16_16_16Float, 8, false}},
{0xc001000600000000ull,
@@ -179,7 +179,9 @@ static void ValidateSampledDepthBinding(const ShaderRecompiler::IR::ImageResourc
const ShaderTextureResource& descriptor, const Image& image,
vk::Format view_format, uint64_t size) {
const bool resource_ok = IsSupportedSampledDepthResource(resource);
const bool encoding_ok = IsSupportedDepthTextureEncoding(descriptor, resource.r128);
// Comparison sampling changes the host format, not the guest metadata encoding.
const bool encoding_ok = descriptor.TileMode() != Prospero::TileMode::kDepth ||
IsSupportedDepthTextureEncoding(descriptor, resource.r128);
const bool view_ok =
IsSupportedSampledDepthView(image.info.pixel_format, view_format, descriptor.DstSelXYZW());
if (resource_ok && encoding_ok && view_ok) {
@@ -622,8 +624,7 @@ TextureBinding RenderExecutor::ResolveTexture(const ShaderRecompiler::IR::ImageR
if (storage) {
ValidateStorageTexture(resource, descriptor, desc.info.data.size);
}
if (!resource.r128 && descriptor.MetaCompress() && tile != Prospero::TileMode::kDepth &&
!desc.info.IsDepth()) {
if (!resource.r128 && descriptor.MetaCompress() && tile != Prospero::TileMode::kDepth) {
TileSizeAlign metadata_size {};
(void)TileGetDccSize(width, height, volume ? depth : image_layers,
desc.info.bytes_per_block, physical_levels, tile, metadata_size,
@@ -1,15 +1,53 @@
#version 450
layout(location = 0) in vec2 uv;
layout(location = 0) out vec4 color;
vec3 SdrToHdr10(vec3 rgb, bool source_linear) {
rgb = clamp(rgb, 0.0, 1.0);
vec3 linear = source_linear ? rgb :
mix(rgb / 12.92, pow((rgb + 0.055) / 1.055, vec3(2.4)),
greaterThan(rgb, vec3(0.04045)));
// Convert primaries and scale 250-nit reference white relative to 10,000 nits.
const mat3 to_rec2020 = (250.0 / 10000.0) *
mat3(0.627404, 0.069097, 0.016391,
0.329283, 0.919540, 0.088013,
0.043313, 0.011362, 0.895595);
vec3 powered = pow(to_rec2020 * linear, vec3(2610.0 / 16384.0));
return pow((3424.0 / 4096.0 + (2413.0 / 128.0) * powered) /
(1.0 + (2392.0 / 128.0) * powered), vec3(2523.0 / 32.0));
}
#ifdef IMGUI_HDR
layout(set = 0, binding = 0) uniform texture2D image;
layout(set = 1, binding = 0) uniform sampler image_sampler;
layout(location = 0) in struct { vec4 Color; vec2 UV; } In;
void main() {
vec4 sampled = In.Color * texture(sampler2D(image, image_sampler), In.UV);
color = vec4(SdrToHdr10(sampled.rgb, false), sampled.a);
}
#else
layout(location = 0) in vec2 uv;
layout(binding = 0) uniform sampler2D source;
layout(push_constant) uniform AlphaControl {
uint premultiplied;
uint flags; // bit 0: premultiplied, bit 1: HDR target, bit 2: PQ source, bit 3: linear source
};
void main() {
color = texture(source, uv);
if (premultiplied == 0u) {
color.rgb *= color.a;
const uint HDR_TARGET = 2u;
const uint PQ_SOURCE = 4u;
vec4 sampled = texture(source, uv);
bool premultiplied = (flags & 1u) != 0u;
bool convert_to_hdr = (flags & (HDR_TARGET | PQ_SOURCE)) == HDR_TARGET;
if (convert_to_hdr && premultiplied) {
sampled.rgb = sampled.a > 0.0 ? sampled.rgb / sampled.a : vec3(0.0);
}
if (convert_to_hdr) {
sampled.rgb = SdrToHdr10(sampled.rgb, (flags & 8u) != 0u);
}
if (!premultiplied || convert_to_hdr) {
sampled.rgb *= sampled.a;
}
color = sampled;
}
#endif
+8
View File
@@ -28,6 +28,14 @@ vk::Format VulkanFormat(Prospero::BufferFormat guest_format);
void RequireVulkanSuccess(vk::Result result, const char* operation);
vk::ShaderModule CompileSPV(std::span<const uint32_t> code, vk::Device device);
[[nodiscard]] constexpr bool IsHdrSwapchainFormat(vk::Format format) {
switch (format) {
case vk::Format::eA2B10G10R10UnormPack32:
case vk::Format::eA2R10G10B10UnormPack32: return true;
default: return false;
}
}
template <typename Handle, typename... Args>
void SetVulkanObjectNameF(vk::Device device, Handle handle, fmt::format_string<Args...> format,
Args&&... args) {
+3 -1
View File
@@ -26,11 +26,13 @@ public:
~Presenter();
KYTY_CLASS_NO_COPY(Presenter);
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info);
[[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info,
bool hdr = false);
[[nodiscard]] Frame& PrepareBlankFrame(uint32_t width, uint32_t height, bool opaque,
CommandBuffer* producer = nullptr);
[[nodiscard]] bool PresentLastFrame();
[[nodiscard]] bool IsGuestPaused() const noexcept;
[[nodiscard]] bool IsHdrSupported() const noexcept;
[[nodiscard]] bool NeedsSystemOverlayRefresh() const noexcept;
[[nodiscard]] RenderContext& Renderer() const noexcept;
void Present(Frame& frame);
@@ -7,6 +7,7 @@
#include "common/file.h"
#include "common/logging/log.h"
#include "common/stringUtils.h"
#include "gpu_blit_shaders/gpu_imgui_hdr_spv.h"
#include "graphics/host_gpu/graphicContext.h"
#include "graphics/presentation/ddsImage.h"
#include "imgui.h"
@@ -890,6 +891,11 @@ struct SystemOverlay::Impl {
info.PipelineInfoMain.PipelineRenderingCreateInfo.colorAttachmentCount = 1;
info.PipelineInfoMain.PipelineRenderingCreateInfo.pColorAttachmentFormats = &color_format;
info.CheckVkResultFn = CheckVulkanResult;
if (IsHdrSwapchainFormat(format)) {
info.CustomShaderFragCreateInfo.sType = VK_STRUCTURE_TYPE_SHADER_MODULE_CREATE_INFO;
info.CustomShaderFragCreateInfo.codeSize = sizeof(GPU_IMGUI_HDR_SPV);
info.CustomShaderFragCreateInfo.pCode = GPU_IMGUI_HDR_SPV;
}
EXIT_IF(!ImGui_ImplVulkan_Init(&info));
vulkan_initialized = true;
}
+33 -31
View File
@@ -272,6 +272,7 @@ public:
void Init(uint32_t width, uint32_t height);
FlipQueue& GetFlipQueue() { return m_flip_queue; }
Graphics::RenderContext& Renderer() const noexcept { return m_renderer; }
bool IsHdrSupported() const noexcept { return m_presenter.IsHdrSupported(); }
void VblankBegin();
void VblankEnd();
@@ -558,14 +559,6 @@ Graphics::ImageInfo BufferAttributeGroup::ImageInfo(const VideoOutBuffer& buffer
if (!Graphics::DecodeVideoOutPixelFormat(attribute.pixel_format, pixel_format)) {
EXIT("unsupported video-out pixel format: 0x%016" PRIx64 "\n", attribute.pixel_format);
}
if (attribute.pixel_format == Graphics::VIDEO_OUT_PIXEL_FORMAT_R10_G10_B10_A2_BT2100_PQ) {
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
if (!warned.test_and_set(std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(fmt::format(
"Warning: HDR-to-SDR conversion is not implemented; displaying PQ video-out "
"pixels unchanged (format=0x{:016x}).\n", attribute.pixel_format));
}
}
const auto tile_mode = Graphics::Prospero::TileMode::kRenderTarget;
Graphics::ImageInfo info {};
info.data = {buffer.data_address, 0};
@@ -1071,31 +1064,31 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
uint32_t height = 0;
bool current = false;
bool premultiplied_alpha = false;
bool hdr = false;
{
Common::LockGuard lock(cfg->mutex);
current = cfg->opened && !cfg->closing && cfg->generation == generation;
if (current) {
if (special) {
width = cfg->width;
height = cfg->height;
} else {
const auto& surface = cfg->buffers[index];
if (!surface.Occupied()) {
EXIT("cannot prepare flip from an unregistered surface, id=%" PRIu64
" index=%d\n",
request_id, index);
}
if (surface.group_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX ||
!cfg->groups[surface.group_index].occupied) {
EXIT("video-out surface references an unavailable attribute group, id=%" PRIu64
" index=%d group=%d\n",
request_id, index, surface.group_index);
}
source_info = cfg->groups[surface.group_index].ImageInfo(surface);
premultiplied_alpha =
(cfg->groups[surface.group_index].attribute.option &
VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED) != 0;
if (current && special) {
width = cfg->width;
height = cfg->height;
} else if (current) {
const auto& surface = cfg->buffers[index];
if (!surface.Occupied()) {
EXIT("cannot prepare flip from an unregistered surface, id=%" PRIu64
" index=%d\n",
request_id, index);
}
if (surface.group_index >= VIDEO_OUT_BUFFER_ATTRIBUTE_NUM_MAX ||
!cfg->groups[surface.group_index].occupied) {
EXIT("video-out surface references an unavailable attribute group, id=%" PRIu64
" index=%d group=%d\n",
request_id, index, surface.group_index);
}
const auto& group = cfg->groups[surface.group_index];
source_info = group.ImageInfo(surface);
hdr = Graphics::IsHdrVideoOutPixelFormat(group.attribute.pixel_format);
premultiplied_alpha = (group.attribute.option &
VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED) != 0;
}
}
if (!current) {
@@ -1108,12 +1101,18 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) {
}
return;
}
static std::atomic_flag warned = ATOMIC_FLAG_INIT;
if (hdr && !m_presenter.IsHdrSupported() && !warned.test_and_set(std::memory_order_relaxed)) {
Log::WriteToConsoleAndLog(
"Warning: HDR output is unavailable; HDR-to-SDR conversion is not implemented, "
"displaying PQ video-out pixels unchanged.\n");
}
Graphics::Presenter::Frame* frame = nullptr;
if (special) {
frame = &m_presenter.PrepareBlankFrame(width, height, index == VIDEO_OUT_BUFFER_INDEX_BLACK,
&buffer);
} else {
frame = &m_presenter.PrepareFrame(buffer, source_info);
frame = &m_presenter.PrepareFrame(buffer, source_info, hdr);
}
Common::LockGuard lock(m_mutex);
@@ -1790,11 +1789,14 @@ KYTY_SYSV_ABI int VideoOutGetOutputStatus(int handle, VideoOutOutputStatus* stat
int32_t attribute3 = 0;
Loader::SystemContentParamSfoGetInt("ATTRIBUTE3", &attribute3);
int32_t attribute = 0;
const bool hdr_allowed = !Loader::SystemContentParamSfoGetInt("ATTRIBUTE", &attribute) ||
(attribute & 0x20000000) != 0;
ctx->mutex.Lock();
// Primary output reports 4K unless param.json Video-out Info enables resolution detection.
status->resolution =
((attribute3 & 4) != 0 && ctx->width < 3840 && ctx->height < 2160 ? 1u : 2u);
status->dynamicRange = 1;
status->dynamicRange = hdr_allowed && DriverState().IsHdrSupported() ? 2u : 1u;
status->refreshRate =
(ctx->output_mode == VIDEO_OUT_OUTPUT_MODE_119_88HZ || Config::GetVblankFrequency() >= 119
? VIDEO_OUT_REFRESH_RATE_119_88HZ
+67 -16
View File
@@ -17,6 +17,7 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <limits>
#include <memory>
#include <vector>
@@ -31,6 +32,8 @@ struct Presenter::Frame {
vk::ImageView view = nullptr;
uint64_t present_tick = 0;
bool busy = false;
bool hdr = false;
bool blank = false;
void Configure(GraphicContext& graphics, vk::Extent2D extent, vk::Format format);
void Transit(vk::CommandBuffer command, vk::ImageLayout layout, vk::AccessFlags2 access);
@@ -274,14 +277,24 @@ public:
return static_cast<uint32_t>(m_images.size());
}
[[nodiscard]] vk::Format Format() const noexcept { return m_format; }
[[nodiscard]] bool IsHdrSupported() const noexcept { return m_hdr_supported.load(); }
bool SetHdr(bool hdr) {
const bool changed = m_hdr_active != (hdr && IsHdrSupported());
m_hdr_requested = hdr;
return changed;
}
private:
vk::SurfaceFormatKHR SelectFormat();
void Destroy();
void DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer);
WindowContext& m_window;
vk::SwapchainKHR m_handle = nullptr;
vk::Format m_format = vk::Format::eUndefined;
std::atomic_bool m_hdr_supported {false};
bool m_hdr_requested = false;
bool m_hdr_active = false;
vk::Extent2D m_extent {};
// Drawable pixel size observed when this swapchain was created.
vk::Extent2D m_window_extent {};
@@ -346,6 +359,40 @@ struct Presenter::Impl {
std::atomic<uint64_t> presented_overlay_revision {0};
};
vk::SurfaceFormatKHR Swapchain::SelectFormat() {
const auto& formats = m_window.surface_capabilities.formats;
auto& graphics = m_window.graphic_ctx;
const auto hdr_format = std::ranges::find_if(formats, [&](const auto& candidate) {
return candidate.colorSpace == vk::ColorSpaceKHR::eHdr10St2084EXT &&
IsHdrSwapchainFormat(candidate.format) &&
static_cast<bool>(
graphics.GetFormatProperties(candidate.format).optimalTilingFeatures &
vk::FormatFeatureFlagBits::eBlitDst);
});
m_hdr_supported = Config::HdrEnabled() && hdr_format != formats.end();
m_hdr_active = m_hdr_requested && IsHdrSupported();
if (m_hdr_active) {
return *hdr_format;
}
if (Config::HdrEnabled() && !IsHdrSupported()) {
LOGF("HDR10 surface unavailable; using SDR output\n");
}
if (formats.size() == 1 && formats.front().format == vk::Format::eUndefined) {
return {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear};
}
for (const auto& candidate: formats) {
if (candidate.colorSpace != vk::ColorSpaceKHR::eSrgbNonlinear) {
continue;
}
switch (candidate.format) {
case vk::Format::eB8G8R8A8Unorm:
case vk::Format::eR8G8B8A8Unorm: return candidate;
default: break;
}
}
EXIT("no supported UNORM swapchain format\n");
}
void Swapchain::Create() {
auto& graphics = m_window.graphic_ctx;
EXIT_IF(graphics.device == nullptr);
@@ -385,18 +432,7 @@ void Swapchain::Create() {
? vk::CompositeAlphaFlagBitsKHR::eOpaque
: vk::CompositeAlphaFlagBitsKHR::eInherit;
vk::SurfaceFormatKHR format {vk::Format::eR8G8B8A8Unorm, vk::ColorSpaceKHR::eSrgbNonlinear};
if (surface.formats.size() != 1 || surface.formats.front().format != vk::Format::eUndefined) {
const auto it = std::find_if(surface.formats.begin(), surface.formats.end(),
[](const vk::SurfaceFormatKHR& candidate) {
return candidate.format == vk::Format::eB8G8R8A8Unorm ||
candidate.format == vk::Format::eR8G8B8A8Unorm;
});
if (it == surface.formats.end()) {
EXIT("no supported UNORM swapchain format\n");
}
format = *it;
}
const auto format = SelectFormat();
m_format = format.format;
const auto swapchain_features = graphics.GetFormatProperties(m_format).optimalTilingFeatures;
if (!static_cast<bool>(swapchain_features & vk::FormatFeatureFlagBits::eBlitDst)) {
@@ -436,6 +472,7 @@ void Swapchain::Create() {
RequireVulkanSuccess(graphics.device.createSwapchainKHR(&create_info, nullptr, &m_handle),
"vkCreateSwapchainKHR");
EXIT_IF(m_handle == nullptr);
LOGF("Vulkan presentation: %s\n", m_hdr_active ? "HDR10 (PQ)" : "SDR");
m_images = EnumerateVulkan<vk::Image>(
"vkGetSwapchainImagesKHR", [&](uint32_t* count, vk::Image* images) {
@@ -692,9 +729,11 @@ void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& l
write.descriptorType = vk::DescriptorType::eCombinedImageSampler;
write.pImageInfo = &image;
command.pushDescriptorSetKHR(vk::PipelineBindPoint::eGraphics, m_overlay_layout, 0, 1, &write);
const uint32_t premultiplied = layer.premultiplied_alpha;
const uint32_t flags = (layer.premultiplied_alpha ? 1u : 0u) |
(m_hdr_active ? 2u : 0u) | (frame.hdr ? 4u : 0u) |
(frame.image.format == vk::Format::eR16G16B16A16Sfloat ? 8u : 0u);
command.pushConstants(m_overlay_layout, vk::ShaderStageFlagBits::eFragment, 0,
sizeof(premultiplied), &premultiplied);
sizeof(flags), &flags);
command.bindPipeline(vk::PipelineBindPoint::eGraphics, m_overlay_pipeline);
vk::RenderingAttachmentInfo attachment {};
attachment.imageView = m_image_views[m_image_index];
@@ -868,7 +907,7 @@ Presenter::Presenter(WindowContext& window): m_impl(std::make_unique<Impl>(windo
Presenter::~Presenter() = default;
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info) {
Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info, bool hdr) {
KYTY_PROFILER_FUNCTION();
EXIT_IF(buffer.IsInvalid());
auto frame_format = info.pixel_format;
@@ -878,6 +917,8 @@ Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo
default: break;
}
auto* frame = m_impl->frames.Acquire({info.extent.width, info.extent.height}, frame_format);
frame->hdr = hdr;
frame->blank = false;
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
auto& image = m_impl->ResolveSurface(info);
if (image.backing.format == vk::Format::eUndefined) {
@@ -894,6 +935,8 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height,
KYTY_PROFILER_FUNCTION();
auto format = m_impl->frames.GetFormat();
auto* frame = m_impl->frames.Acquire({width, height}, format);
frame->hdr = false;
frame->blank = true;
Common::LockGuard render_lock(m_impl->renderer.GetMutex());
frame->Configure(m_impl->window.graphic_ctx, {width, height}, format);
vk::ClearColorValue clear {};
@@ -922,6 +965,10 @@ bool Presenter::IsGuestPaused() const noexcept {
return m_impl->window.loop.paused.load(std::memory_order_acquire);
}
bool Presenter::IsHdrSupported() const noexcept {
return m_impl->swapchain.IsHdrSupported();
}
bool Presenter::NeedsSystemOverlayRefresh() const noexcept {
const auto visual = GetSystemOverlayVisualState();
return visual.active ||
@@ -967,8 +1014,12 @@ void Presenter::Impl::Present() {
KYTY_PROFILER_FUNCTION();
const auto overlay_visual = GetSystemOverlayVisualState();
// Keep black/blank flips in the current color space; only real main-bus frames switch it.
const auto* main_frame = layers[0].frame;
const bool hdr_changed = main_frame != nullptr && !main_frame->blank &&
swapchain.SetHdr(main_frame->hdr);
// Some window systems keep presenting an old swapchain after a resize.
if (swapchain.NeedsResize()) {
if (hdr_changed || swapchain.NeedsResize()) {
RecoverSwapchain(Swapchain::Status::Recreate);
}
for (uint32_t attempt = 0; attempt < 2; attempt++) {
@@ -685,6 +685,11 @@ static void VulkanGetExtensions(VulkanExtensions& r) {
});
r.enable_validation_layers = Config::VulkanValidationEnabled();
if (Config::HdrEnabled() &&
HasExtension(r.available_extensions, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME) &&
!HasExtension(r.required_extensions, VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME)) {
r.required_extensions.push_back(VK_EXT_SWAPCHAIN_COLOR_SPACE_EXTENSION_NAME);
}
if (HasExtension(r.available_extensions, VK_EXT_DEBUG_UTILS_EXTENSION_NAME)) {
r.required_extensions.push_back(VK_EXT_DEBUG_UTILS_EXTENSION_NAME);
@@ -323,25 +323,25 @@ uint32_t QueryDimensions(ValueEmitContext& ctx, const IR::MemoryInfo& mem,
return vector;
}
uint32_t PackedOffset(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR::Inst& address,
const ImageSampleLayout& layout, ImageDimension dimension) {
uint32_t PackedOffset(ValueEmitContext& ctx, IR::Value value, ImageDimension dimension,
uint32_t& mask) {
const auto components = ImageDimensionInfoFor(dimension).spatial_components;
const auto zero = ConstantI32(ctx.state, 0);
if (layout.offset == NoImageComponent || mem.image_address_components <= layout.offset) {
if (components == 1u) return zero;
const auto result = ctx.state.builder.AllocateId();
if (components == 3u) {
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 3),
result, zero, zero, zero);
} else {
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 2),
result, zero, zero);
uint32_t values[3] {};
if (value.IsImmediate()) {
// RDNA2 packs signed six-bit texel offsets into the low bits of each byte.
for (uint32_t index = 0; index < components; index++) {
const auto bits = (value.U32() >> (index * 8u)) & 0x3fu;
values[index] = ConstantI32(ctx.state, static_cast<int32_t>(bits ^ 0x20u) - 0x20);
}
return result;
mask |= spv::ImageOperandsConstOffsetMask;
return components == 1u
? values[0]
: ctx.state.builder.Constant(spv::OpConstantComposite,
TypeI32Vector(ctx.state, components),
std::span(values).first(components));
}
const auto packed = Unary(ctx.state, spv::OpBitcast, TypeI32(ctx.state),
AddressU32(ctx, mem, address, layout.offset));
uint32_t values[3] = {zero, zero, zero};
mask |= spv::ImageOperandsOffsetMask;
const auto packed = Unary(ctx.state, spv::OpBitcast, TypeI32(ctx.state), ctx.Def(value));
for (uint32_t index = 0; index < components; index++) {
values[index] = ctx.state.builder.AllocateId();
ctx.state.builder.AddFunction(spv::OpBitFieldSExtract, TypeI32(ctx.state), values[index],
@@ -350,13 +350,8 @@ uint32_t PackedOffset(ValueEmitContext& ctx, const IR::MemoryInfo& mem, const IR
}
if (components == 1u) return values[0];
const auto result = ctx.state.builder.AllocateId();
if (components == 3u) {
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 3),
result, values[0], values[1], values[2]);
} else {
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, 2),
result, values[0], values[1]);
}
ctx.state.builder.AddFunction(spv::OpCompositeConstruct, TypeI32Vector(ctx.state, components),
result, std::span(values).first(components));
return result;
}
@@ -794,21 +789,28 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto layout = Layout(mem);
const auto numeric_class = image.numeric_class;
const bool dref = HasFlag(mem, Decoder::ImageSampleFlagCompare);
IR::Value offset_value;
if (layout.offset != NoImageComponent) {
const auto value = address->Arg(layout.offset).Resolve();
offset_value = IR::ResolveActiveU32(value, inst.Arg(inst.NumArgs() - 1u).Resolve());
if (offset_value.IsEmpty()) offset_value = value;
}
if (dref && state.program.info.images[mem.resource].conversion_format !=
Prospero::BufferFormat::kInvalid) {
ctx.Fail(inst, "uses depth comparison with a converted image");
return;
}
if (op == IR::ValueOpcode::ImageGatherRaw) {
const auto coord = CoordF32(ctx, mem, *address, layout.coord,
dimension_info.coordinate_components, image.cube);
if (dimension == ImageDimension::Dim1D) {
if (dref || !HasFlag(mem, Decoder::ImageSampleFlagLevelZero) ||
if (image.indirect_root == mem.resource || dref ||
!HasFlag(mem, Decoder::ImageSampleFlagLevelZero) ||
HasFlag(mem, Decoder::ImageSampleFlagOffset) ||
HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
ctx.Fail(inst, "has an unsupported 1D gather variant");
return;
}
const auto coord = CoordF32(ctx, mem, *address, layout.coord,
dimension_info.coordinate_components, image.cube);
const auto sample = EmitOneDimensionalGatherLz(ctx, mem, coord, numeric_class);
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample),
numeric_class, false, mem, true));
@@ -829,32 +831,38 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
} else {
component_or_dref = ConstantU32(state, ImageGatherSource(state, mem));
}
uint32_t operand_mask = 0;
uint32_t offset = 0;
if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
operand_mask = spv::ImageOperandsConstOffsetsMask;
offset = HorizontalOffsets(state, dimension);
} else if (layout.offset != NoImageComponent) {
operand_mask = spv::ImageOperandsOffsetMask;
offset = PackedOffset(ctx, mem, *address, layout, dimension);
}
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler);
const auto EmitGather = [&](uint32_t mip) {
const auto sampled = MakeSampledImage(state, mem.resource, sampler_id, mip);
const auto sample = state.builder.AllocateId();
const std::array operands {operand_mask, offset};
state.builder.AddFunction(
dref ? spv::OpImageDrefGather : spv::OpImageGather, result_type,
sample, sampled, coord, component_or_dref,
std::span(operands).first(operand_mask != 0u ? 2u : 0u));
return sample;
const auto EmitGather = [&](uint32_t resource, uint32_t array_index) {
const auto& candidate = state.program.info.images[resource];
const auto dimension = candidate.dimension;
const auto coord = CoordF32(
ctx, mem, *address, layout.coord,
ImageDimensionInfoFor(dimension).coordinate_components, candidate.cube);
uint32_t operand_mask = 0;
uint32_t offset = 0;
if (HasFlag(mem, Decoder::ImageSampleFlagGatherHorizontal)) {
operand_mask = spv::ImageOperandsConstOffsetsMask;
offset = HorizontalOffsets(state, dimension);
} else if (layout.offset != NoImageComponent) {
offset = PackedOffset(ctx, offset_value, dimension, operand_mask);
}
const auto EmitMip = [&](uint32_t mip) {
const auto sampled = MakeSampledImage(state, resource, sampler_id, mip, array_index);
const auto sample = state.builder.AllocateId();
const std::array operands {operand_mask, offset};
state.builder.AddFunction(
dref ? spv::OpImageDrefGather : spv::OpImageGather, result_type,
sample, sampled, coord, component_or_dref,
std::span(operands).first(operand_mask != 0u ? 2u : 0u));
return sample;
};
return candidate.mip_mode == IR::ImageMipMode::Dynamic && candidate.mip_count > 1u
? EmitIndexSwitch(
state, GatherMip(ctx, inst, mem, *address, layout, candidate.mip_count),
candidate.mip_count, result_type, EmitMip)
: EmitMip(0u);
};
const auto sample = image.mip_mode == IR::ImageMipMode::Dynamic && image.mip_count > 1u
? EmitIndexSwitch(
state, GatherMip(ctx, inst, mem, *address, layout,
image.mip_count),
image.mip_count, result_type, EmitGather)
: EmitGather(0);
const auto sample = EmitImageAccess(ctx, inst, result_type, EmitGather);
ctx.Define(inst, ResultVector(ctx, UnpackImageGather(ctx, mem, sample),
result_numeric_class, false, mem, true));
return;
@@ -878,7 +886,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
dref_value = AddressF32(ctx, mem, *address, layout.dref);
}
}
std::array<uint32_t, 3> operands {};
std::array<uint32_t, 4> operands {};
size_t operand_count = 0;
if (HasFlag(mem, Decoder::ImageSampleFlagDerivative)) {
operands[0] = spv::ImageOperandsGradMask;
@@ -908,9 +916,20 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
state, ctx.Def(handle->Arg(0)), static_cast<uint32_t>(samplers.size()),
TypeU32(state), [&](uint32_t ordinal) { return ConstantU32(state, samplers[ordinal]); });
}
if (layout.offset != NoImageComponent &&
!offset_value.IsImmediate()) {
ctx.Fail(inst, "has a nonconstant sample texel offset");
}
const auto sampler_id = LoadSamplerDescriptor(state, mem.sampler, sampler_index);
const auto EmitSample = [&](uint32_t resource, uint32_t array_index) {
const auto& candidate = state.program.info.images[resource];
auto sample_operands = operands;
auto sample_operand_count = operand_count;
if (layout.offset != NoImageComponent) {
sample_operand_count = std::max(size_t{1}, sample_operand_count);
sample_operands[sample_operand_count++] = PackedOffset(
ctx, offset_value, candidate.dimension, sample_operands[0]);
}
const auto coord =
CoordF32(ctx, mem, *address, layout.coord,
ImageDimensionInfoFor(candidate.dimension).coordinate_components,
@@ -920,7 +939,7 @@ void EmitImage(ValueEmitContext& ctx, const IR::Inst& inst) {
const auto sample = state.builder.AllocateId();
state.builder.AddFunction(opcode, result_type, sample, sampled, coord,
std::span(&dref_value, dref ? 1u : 0u),
std::span<const uint32_t>(operands).first(operand_count));
std::span<const uint32_t>(sample_operands).first(sample_operand_count));
return sample;
};
auto result = EmitImageAccess(ctx, inst, result_type, EmitSample);
@@ -687,7 +687,8 @@ void Translator::IMAGE_SAMPLE(const Decoder::Instruction& inst) {
const auto resource = GetImageResource(memory);
const auto sampler = GetSamplerResource(memory);
const auto address = MakeImageAddress(inst, MemorySourceAt(inst, 0));
const auto result = ir.Emit(IR::ValueOpcode::ImageSampleRaw, {resource, sampler, address},
const auto result = ir.Emit(IR::ValueOpcode::ImageSampleRaw,
{resource, sampler, address, ir.GetExec()},
AddMemoryInfo(memory, inst.pc));
const bool dref = (memory.image_sample_flags & Decoder::ImageSampleFlagCompare) != 0u;
if (dref && memory.data_bits != 16u) {
@@ -714,7 +715,7 @@ void Translator::IMAGE_GATHER(const Decoder::Instruction& inst) {
has_lod ? resource.Instruction()->Arg(1) : IR::Value(0u),
has_lod ? resource.Instruction()->Arg(3) : IR::Value(0u),
has_lod ? sampler.Instruction()->Arg(1) : IR::Value(0u),
has_lod ? sampler.Instruction()->Arg(2) : IR::Value(0u)},
has_lod ? sampler.Instruction()->Arg(2) : IR::Value(0u), ir.GetExec()},
AddMemoryInfo(memory, inst.pc));
for (uint32_t index = 0; index < memory.data_dwords; index++) {
WriteOperand(OffsetOperand(inst.dst, index),
@@ -317,8 +317,8 @@ VALUE_OPCODE(BvhIntersect, U32x4, U32x4, ImageAddress, U1)
VALUE_OPCODE(ImageQueryLod, U32x4, ImageResource, SamplerResource, ImageAddress)
VALUE_OPCODE(ImageRead, U32x4, ImageResource, ImageAddress, U1)
VALUE_OPCODE(ImageWrite, Void, ImageResource, ImageAddress, U32x4, U1)
VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress)
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U32, U32, U32, U32)
VALUE_OPCODE(ImageSampleRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U1)
VALUE_OPCODE(ImageGatherRaw, U32x4, ImageResource, SamplerResource, ImageAddress, U32, U32, U32, U32, U1)
VALUE_OPCODE(ImageAtomicCompareSwap32, U32, ImageResource, ImageAddress, U32, U32, U1)
VALUE_OPCODE(ImageAtomicSwap32, U32, ImageResource, ImageAddress, U32, U1)
VALUE_OPCODE(ImageAtomicSwap64, U64, ImageResource, ImageAddress, U64, U1)
@@ -989,23 +989,29 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
// Keep only that Boolean structure, never the varying shader dependency graph.
Value unknown;
bool capture_control_reads = false;
std::unordered_map<const Inst*, Value> predicates;
std::function<Value(Value)> ClonePredicate = [&](Value value) -> Value {
value = value.Resolve();
if (value.IsEmpty()) return {};
const auto* inst = value.TryInstruction();
if (inst == nullptr)
return ValidateRuntimeValue(program, value, RuntimeValueType::Integer) ? value : Value {};
// Preserve shared EXEC predicates, including subgraphs with no uniform factor.
const auto [found, inserted] = predicates.try_emplace(inst);
if (!inserted) return found->second;
auto& result = found->second;
if (ValidateRuntimeValue(program, value, RuntimeValueType::Integer)) {
capture_control_reads |=
!ValidateRuntimeValue(program, value, RuntimeValueType::ImmutableInteger);
return Clone(value);
return result = Clone(value);
}
const auto* inst = value.TryInstruction();
if (inst == nullptr) return {};
const auto op = inst->GetOpcode();
if (op == ValueOpcode::ConditionRef || op == ValueOpcode::LogicalNot) {
const auto operand = ClonePredicate(inst->Arg(0));
if (operand.IsEmpty() || op == ValueOpcode::ConditionRef) return operand;
if (operand.IsEmpty() || op == ValueOpcode::ConditionRef) return result = operand;
auto& node = plan.value_storage.emplace_back(op);
node.SetArg(0, operand);
return Value(&node);
return result = Value(&node);
}
if (op != ValueOpcode::LogicalAnd && op != ValueOpcode::LogicalOr) return {};
auto left = ClonePredicate(inst->Arg(0));
@@ -1019,7 +1025,7 @@ ResourcePlan ExtractResourcePlan(const Program& program) {
auto& node = plan.value_storage.emplace_back(op);
node.SetArg(0, left);
node.SetArg(1, right);
return Value(&node);
return result = Value(&node);
};
plan.control_flow = ResourceControlFlow(program, ClonePredicate);
plan.uniform_fill = AnalyzeUniformFill(program);
@@ -1406,6 +1412,7 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization&
}
EXIT_IF(image.indirect_root == memory.resource &&
inst.GetOpcode() != ValueOpcode::ImageSampleRaw &&
inst.GetOpcode() != ValueOpcode::ImageGatherRaw &&
inst.GetOpcode() != ValueOpcode::ImageRead);
}
}
@@ -180,6 +180,12 @@
<property name="text"><string>Accurate texture synchronization</string></property>
</widget>
</item>
<item row="8" column="1">
<widget class="QCheckBox" name="checkBox_hdr">
<property name="toolTip"><string>Enable HDR10 output for supported games. Requires HDR enabled on the host display.</string></property>
<property name="text"><string>Enable HDR display support</string></property>
</widget>
</item>
</layout>
</widget>
</item>
+2
View File
@@ -117,6 +117,7 @@ public:
PresentMode present_mode = PresentMode::Mailbox;
int gpu_index = -1;
bool fullscreen_enabled = false;
bool hdr_enabled = false;
bool hide_cursor_enabled = false;
bool readback_linear_images = false;
bool sync_raw_image_buffers = false;
@@ -152,6 +153,7 @@ public:
present_mode = other.present_mode;
gpu_index = other.gpu_index;
fullscreen_enabled = other.fullscreen_enabled;
hdr_enabled = other.hdr_enabled;
hide_cursor_enabled = other.hide_cursor_enabled;
readback_linear_images = other.readback_linear_images;
sync_raw_image_buffers = other.sync_raw_image_buffers;
+2
View File
@@ -10,6 +10,7 @@ QVariantMap Configuration::GameSettings() const {
KYTY_CFG_SET(present_mode);
KYTY_CFG_SET(gpu_index);
KYTY_CFG_SET(fullscreen_enabled);
KYTY_CFG_SET(hdr_enabled);
KYTY_CFG_SET(hide_cursor_enabled);
KYTY_CFG_SET(readback_linear_images);
KYTY_CFG_SET(sync_raw_image_buffers);
@@ -55,6 +56,7 @@ void Configuration::ReadGameSettingsValues(const Settings& s) {
present_mode = PresentMode::Mailbox;
}
KYTY_CFG_GET(fullscreen_enabled);
KYTY_CFG_GET(hdr_enabled);
KYTY_CFG_GET(hide_cursor_enabled);
KYTY_CFG_GET(readback_linear_images);
KYTY_CFG_GET(sync_raw_image_buffers);
@@ -273,6 +273,7 @@ void ConfigurationEditDialog::Init(const Configuration& info) {
info.gpu_index >= 0 && info.gpu_index < m_ui->comboBox_gpu->count() - 1 ? info.gpu_index + 1
: 0);
m_ui->checkBox_fullscreen->setChecked(info.fullscreen_enabled);
m_ui->checkBox_hdr->setChecked(info.hdr_enabled);
m_ui->checkBox_hide_cursor->setChecked(info.hide_cursor_enabled);
m_ui->checkBox_readback->setChecked(info.readback_linear_images);
m_ui->checkBox_sync_raw_image_buffers->setChecked(info.sync_raw_image_buffers);
@@ -434,6 +435,7 @@ static void UpdateInfo(Configuration& info, Ui::ConfigurationEditDialog& ui, boo
TextToEnum<Configuration::PresentMode>(ui.comboBox_present_mode->currentText());
info.gpu_index = ui.comboBox_gpu->currentIndex() - 1;
info.fullscreen_enabled = ui.checkBox_fullscreen->isChecked();
info.hdr_enabled = ui.checkBox_hdr->isChecked();
info.hide_cursor_enabled = ui.checkBox_hide_cursor->isChecked();
info.readback_linear_images = ui.checkBox_readback->isChecked();
info.sync_raw_image_buffers = ui.checkBox_sync_raw_image_buffers->isChecked();
+3
View File
@@ -245,6 +245,9 @@ static QStringList CreateEmulatorArgs(const Configuration& info) {
if (info.fullscreen_enabled) {
args << "--fullscreen";
}
if (info.hdr_enabled) {
args << "--hdr";
}
if (info.hide_cursor_enabled) {
args << "--hide-cursor";
}
+2
View File
@@ -164,6 +164,8 @@ void Psf::OpenJson(const std::filesystem::path& file_name) {
add_string_param("contentId", "CONTENT_ID");
add_string_param("contentVersion", "APP_VER");
add_string_param("appVersion", "APP_VER");
AddParamInt("ATTRIBUTE", 0);
add_int_param("attribute", "ATTRIBUTE");
add_int_param("attribute3", "ATTRIBUTE3");
add_int_param("userDefinedParam1", "USER_DEFINED_PARAM_1");
add_int_param("userDefinedParam2", "USER_DEFINED_PARAM_2");
+6
View File
@@ -61,6 +61,7 @@ static void PrintUsage() {
::printf(
" --gpu <index> Vulkan physical device index. Default: auto.\n");
::printf(" --fullscreen Run in borderless desktop fullscreen.\n");
::printf(" --hdr Enable HDR10 output when supported.\n");
::printf(
" --hide-cursor Hide the cursor after 2 s idle. Default: off.\n");
::printf(" --no-splash Disable the startup splash screen.\n");
@@ -215,6 +216,11 @@ static bool ParseArgs(int argc, char* argv[], RunOptions& options, bool& show_he
continue;
}
if (arg == "--hdr") {
options.config.hdr_enabled = true;
continue;
}
if (arg == "--hide-cursor") {
options.config.hide_cursor_enabled = true;
continue;
+59
View File
@@ -640,6 +640,64 @@ void TestFailedMaterializationRejectsStage() {
"missing runtime user data did not reject the cached stage");
}
void TestSharedControlPredicates() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
using Libs::Graphics::ShaderRecompiler::CFG::TerminatorKind;
constexpr uint32_t depth = 8;
for (const auto operation : {ValueOpcode::LogicalAnd, ValueOpcode::LogicalOr}) {
Program program;
program.user_data_count = 1;
program.srt_plan_complete = program.resource_tracking_complete = true;
auto &block = AddValueBlock(program);
const auto emit = [&](ValueOpcode opcode, std::initializer_list<Value> args) {
return Value(&block.AppendNewInst(opcode, args));
};
const auto user = emit(ValueOpcode::GetUserData, {Value(ScalarReg(0))});
const auto uniform = emit(ValueOpcode::INotEqual32, {user, Value(0u)});
const auto lane = emit(ValueOpcode::GetBuiltin,
{Value(static_cast<uint32_t>(StageInputKind::LocalInvocationId)), Value(0u)});
auto varying = emit(ValueOpcode::INotEqual32, {lane, Value(0u)});
// Both failed and partially uniform subgraphs share their operands exponentially.
for (uint32_t i = 0; i < depth; ++i)
varying = emit(ValueOpcode::LogicalOr, {varying, varying});
auto condition = emit(operation, {varying, uniform});
for (uint32_t i = 0; i < depth; ++i)
condition = emit(ValueOpcode::LogicalOr, {condition, condition});
condition = emit(ValueOpcode::LogicalNot, {condition});
condition = emit(ValueOpcode::ConditionRef, {condition});
for (uint32_t i = 0; i < 3; ++i) AddValueBlock(program);
for (uint32_t i = 0; i < 2; ++i) {
program.blocks[i]->condition = condition;
program.blocks[i]->terminator.kind = TerminatorKind::ConditionalBranch;
program.blocks[i]->terminator.true_block = program.blocks[i == 0 ? 1 : 3];
program.blocks[i]->terminator.false_block = program.blocks[2];
}
auto plan = ExtractResourcePlan(program);
Check(plan.value_storage.size() == depth + 5 &&
plan.control_flow.size() == 4 &&
plan.control_flow[0].condition == plan.control_flow[1].condition &&
!plan.capture_specialization_reads,
"shared predicates expanded into trees or retained varying dependencies");
for (const uint32_t flag : {0u, 1u}) {
const std::array<uint32_t, 1> user_data{flag};
SrtWalker walker(plan, {.user_data = user_data});
uint32_t result = 99;
const bool conjunction = operation == ValueOpcode::LogicalAnd;
const bool known = conjunction ? flag == 0 : flag != 0;
Check(walker.Evaluate(plan.control_flow[0].condition, result) == known &&
(!known || result == uint32_t{conjunction}),
"shared predicate changed its uniform factor or cached runtime values");
std::vector<uint32_t> flat;
Check(walker.RefreshFlatBuffer(flat) &&
plan.visited_blocks == (known
? (conjunction ? std::vector<uint8_t>{1, 1, 0, 1}
: std::vector<uint8_t>{1, 0, 1, 0})
: std::vector<uint8_t>{1, 1, 1, 1}),
"shared predicate skipped an unknown branch or followed a rejected branch");
}
}
}
void TestFiniteImageRefreshReusesScalarReads() {
using namespace Libs::Graphics::ShaderRecompiler::IR;
Program program;
@@ -793,6 +851,7 @@ int main() {
for (const bool memory_condition : {false, true})
TestWrittenDescriptorPredicateReads(memory_condition);
TestFailedMaterializationRejectsStage();
TestSharedControlPredicates();
TestFiniteImageRefreshReusesScalarReads();
TestMixedSamplerVariantsShareRuntimeDescriptor();
std::puts("ResourceMaterializationTests: all cases passed");
+22 -19
View File
@@ -270,7 +270,7 @@ MakeIndirectImageFixture(bool malformed, uint32_t table_stride = 32, uint32_t ta
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
const auto sampled = fixture->Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture->ImageAddress()},
{image, sampler, fixture->ImageAddress(), Value(true)},
fixture->AddMemory(sample, 0x10f0));
const auto sampled_x =
fixture->Emit(ValueOpcode::CompositeExtractU32x4, {sampled, Value(0u)});
@@ -486,7 +486,8 @@ void TestWaterfallImageTable() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
const auto value = fixture->Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture->ImageAddress()}, fixture->AddMemory(memory, 0x92cu), block);
{image, sampler, fixture->ImageAddress(), block == entry ? active : guard},
fixture->AddMemory(memory, 0x92cu), block);
fixture->Emit(ValueOpcode::ReferenceU32,
{fixture->Emit(ValueOpcode::CompositeExtractU32x4, {value, Value(0u)}, 0, block)}, 0, block);
};
@@ -641,7 +642,7 @@ void TestGuardedDirectImageTable() {
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x128));
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
@@ -989,7 +990,7 @@ void TestBoundedComputeImageLoop() {
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture.ImageAddress()},
{image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(sample, 0x29c));
fixture.PlanAndTrack();
const auto source = fixture.program.info.images[0].source;
@@ -1325,7 +1326,9 @@ void TestUniformizedMaterialImageKeys() {
MemoryInfo image_memory;
image_memory.kind = ResourceKind::Image;
image_memory.image_dimension = Decoder::ImageDimension::Dim2D;
const auto sampled = fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address},
const auto sampled = fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, address,
variant == Variant::EmptyFirstEntry ? material_guard : sample_guard},
fixture.AddMemory(image_memory, 0x1aecu), sample);
if (first_lane) {
const auto component = fixture.Emit(
@@ -1477,7 +1480,7 @@ void TestImageDescriptorFields() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_r128 = r128;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x80));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
@@ -1571,7 +1574,7 @@ void TestUniformScalarBufferImage() {
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture.ImageAddress()}, fixture.AddMemory(sample, 0x228));
{image, sampler, fixture.ImageAddress(), Value(true)}, fixture.AddMemory(sample, 0x228));
fixture.PlanAndTrack();
const auto plan = ExtractResourcePlan(fixture.program);
@@ -1866,7 +1869,7 @@ void TestImagesSamplersAndAliases() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_sample_flags = sample_flags;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, image_address},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, image_address, Value(true)},
fixture.AddMemory(memory, pc));
return std::pair{image, sampler};
};
@@ -1938,14 +1941,14 @@ void TestSampleAdjustSamplerScratch() {
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_sample_flags = Decoder::ImageSampleFlagAdjust;
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, sampler, fixture.ImageAddress()},
{image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x1ec));
// PPSA24156's zero border payload leaves the reserved scratch shift as
// the entire DWORD after the frontend folds its OR with zero.
const auto shift_sampler = fixture.Sampler(
{Value(0x36u), Value(0xfff000u), Value(0x02500000u), scratch}, 0x200);
fixture.Emit(ValueOpcode::ImageSampleRaw,
{image, shift_sampler, fixture.ImageAddress()},
{image, shift_sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x200));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
@@ -1994,7 +1997,7 @@ void TestSampleAdjustSamplerScratch() {
rejected_memory.image_dimension = Decoder::ImageDimension::Dim2D;
rejected_memory.image_sample_flags = flags;
rejected.Emit(ValueOpcode::ImageSampleRaw,
{rejected_image, rejected_sampler, rejected.ImageAddress()},
{rejected_image, rejected_sampler, rejected.ImageAddress(), Value(true)},
rejected.AddMemory(rejected_memory, 0x200));
CheckFatal([&] { rejected.PlanAndTrack(); },
@@ -2112,7 +2115,7 @@ void TestGatherLodSamplerValidation() {
fixture.Emit(ValueOpcode::ImageGatherRaw,
{fixture.Image(image_words), fixture.Sampler(sampler_words),
fixture.ImageAddress(), image_words[1], image_words[3],
sampler_words[1], sampler_words[2]},
sampler_words[1], sampler_words[2], Value(true)},
fixture.AddMemory(memory, i * 4));
}
fixture.PlanAndTrack();
@@ -2514,7 +2517,7 @@ ResourcePlan ConditionalSamplerPlan(bool diamond, bool reverse, bool reverse_phi
sample.kind = ResourceKind::Image;
sample.image_dimension = Decoder::ImageDimension::Dim2D;
const auto result =
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, address, Value(true)},
fixture.AddMemory(sample, 0x4d4));
fixture.Emit(ValueOpcode::ReferenceU32,
{fixture.Emit(ValueOpcode::CompositeExtractU32x4,
@@ -2619,7 +2622,7 @@ void TestFiniteImagePhiCycle() {
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 4));
fixture.PlanAndTrack();
auto plan = ExtractResourcePlan(fixture.program);
@@ -2685,12 +2688,12 @@ void TestFiniteInlineSampler() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
if (format == F::k32Float)
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, previous, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, previous, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x440u));
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x1dd0u));
if (format == F::k32Float)
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, direct, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, direct, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 0x1dd8u));
}
fixture.PlanAndTrack();
@@ -2764,7 +2767,7 @@ void TestFiniteInlineSampler() {
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
memory.image_sample_flags = flags;
adjusted.Emit(ValueOpcode::ImageSampleRaw, {image, shared, adjusted.ImageAddress()},
adjusted.Emit(ValueOpcode::ImageSampleRaw, {image, shared, adjusted.ImageAddress(), Value(true)},
adjusted.AddMemory(memory, 0x1dd0u));
}
CheckFatal([&] { adjusted.PlanAndTrack(); }, "not a valid runtime value",
@@ -2817,7 +2820,7 @@ void TestFiniteImageBitScanSentinel() {
MemoryInfo memory;
memory.kind = ResourceKind::Image;
memory.image_dimension = Decoder::ImageDimension::Dim2D;
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress()},
fixture.Emit(ValueOpcode::ImageSampleRaw, {image, sampler, fixture.ImageAddress(), Value(true)},
fixture.AddMemory(memory, 4));
if (!nonzero) {
CheckFatal([&] { fixture.PlanAndTrack(); }, "not a valid runtime value",
+197 -26
View File
@@ -11083,6 +11083,78 @@ public:
context.ShutdownGpu();
LibKernel::Memory::InstallGpuResources(nullptr);
}
{
// PPSA01593 samples a DCC R32 color surface through a comparison sampler.
constexpr uint64_t comparison_dcc = base + 0x10000;
std::memset(mapped, 0, 0x10000);
std::memset(reinterpret_cast<void *>(comparison_dcc), 0xff, 0x1000);
RenderContext context(m_runtime_context);
context.InitializeGpu(nullptr);
LibKernel::Memory::InstallGpuResources(&context);
auto &scheduler = context.GetCommandScheduler();
HW::Context registers{};
HW::UserConfig user_config{};
HW::Shader shaders{};
scheduler.Begin(registers, user_config, shaders);
context.MapMemory(base, allocation_size);
auto &cache = context.GetTextureCache();
auto &executor = context.GetRenderExecutor();
const ShaderTextureResource descriptor{{static_cast<uint32_t>(base >> 8u),
0xc1600000u, 0x000fc00fu, 0x91b00204u, 0, 0x00700000u,
0x002b0000u | (static_cast<uint32_t>(comparison_dcc >> 8u) << 24u),
static_cast<uint32_t>(comparison_dcc >> 16u)}};
ShaderRecompiler::IR::DescriptorValue value{};
value.dword_count = 8;
std::copy_n(descriptor.fields, 8, value.dwords.begin());
ShaderRecompiler::IR::ImageResource resource{};
resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Storage;
resource.numeric_class = Prospero::TextureNumericClass::Float;
resource.dimension = ShaderRecompiler::Decoder::ImageDimension::Dim2D;
resource.written = true;
const auto color = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
(void)cache.FindTexture(color.image_id, color.desc);
vk::ClearValue paint{};
paint.color.float32 = std::array{0.375f, 0.f, 0.f, 0.f};
TextureCacheTestAccess::ClearImage(cache, scheduler.Current(), color.image_id,
{vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}, paint);
resource.resource_class = ShaderRecompiler::IR::ImageResourceClass::Sampled;
resource.written = false;
resource.read = true;
resource.depth_compare = true;
auto comparison = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
(void)cache.FindTexture(comparison.image_id, comparison.desc);
const auto &depth = cache.GetImage(comparison.image_id);
Require(name, "expanded DCC comparison alias",
comparison.image_id != color.image_id &&
depth.info.pixel_format == vk::Format::eD32Sfloat &&
depth.info.guest_format == Prospero::BufferFormat::k32Float &&
depth.info.tile_mode == Prospero::TileMode::kRenderTarget &&
depth.info.extent == vk::Extent3D{64, 64, 1} &&
depth.info.metadata.kind == ImageMetadataKind::Dcc &&
depth.info.metadata.range == GuestRange{comparison_dcc, 0x1000} &&
ReadCachedTexel(name, context, comparison.image_id) ==
std::vector<u32>{std::bit_cast<u32>(0.375f)},
"comparison promotion lost native color data or guest DCC layout");
for (const u32 code : {0x40404040u, 0x80808080u}) {
WriteMetadata(context, comparison_dcc, 0x1000, code);
comparison = RenderExecutorTestAccess::ResolveTexture(executor, resource, value);
(void)cache.FindTexture(comparison.image_id, comparison.desc);
Require(name, "DCC clear on a comparison backing",
ReadCachedTexel(name, context, comparison.image_id, {63, 63, 0}) ==
std::vector<u32>{code == 0x40404040u ? 0u : 0x3f800000u},
"DCC clear used the native depth format or color aspect");
std::array<uint8_t, 0x1000> metadata{};
Require(name, "comparison clear expands metadata",
LibKernel::Memory::TryReadBacking(comparison_dcc, metadata.data(),
metadata.size()) &&
std::ranges::all_of(metadata, [](uint8_t byte) { return byte == 0xff; }),
"comparison clear retained the native DCC clear key");
}
context.UnmapMemory(base, allocation_size);
scheduler.Finish();
context.ShutdownGpu();
LibKernel::Memory::InstallGpuResources(nullptr);
}
Require(name, "unmap",
Libs::LibKernel::Memory::KernelMunmap(base, allocation_size) == 0,
"captured scene mapping release failed");
@@ -34888,12 +34960,11 @@ TestCase ImageSampleOpcodeAliasUsesNormalCoords() {
return test;
}
TestCase ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu() {
template <u32 wave_size> TestCase ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu() {
using O = ShaderOpcode;
std::vector<u32> code;
AppendVMovU32(&code, 20,
1); // Non-constant +1 X offset is not a SPIR-V ConstOffset.
AppendVMovU32(&code, 20, 1); // +1 X remains a full dword with A16 coordinates.
AppendVMovLiteral(&code, 21, 0x36003900u); // x=0.625, y=0.375 packed as f16.
AppendVMovU32(&code, 22, 0);
code.push_back(EncodeMimg0(0x30, 0xf));
@@ -34904,18 +34975,63 @@ TestCase ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu() {
AppendEnd(&code);
auto image = MakeRgbaImage(4, 4);
SetRgbaPixel(&image, 4, 2, 1, 0x3f800000u, 0x40000000u, 0x40400000u,
SetRgbaPixel(&image, 4, 2, 1, 0x3f000000u, 0, 0, 0);
SetRgbaPixel(&image, 4, 3, 1, 0x3f800000u, 0x40000000u, 0x40400000u,
0x40800000u);
TestCase test;
test.name = "ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu";
test.name = wave_size == 32 ? "ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu32"
: "ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu64";
test.code = code;
test.compute_info.wave_size = wave_size;
test.has_compute_info = true;
test.expected = {0x3f800000u, 0x40000000u, 0x40400000u, 0x40800000u};
test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.sampled_image_rgba = image;
test.required_spirv = {"UnpackHalf2x16"};
test.forbidden_spirv = {"OpBitFieldSExtract"};
test.required_spirv = {"UnpackHalf2x16", "Lod|ConstOffset"};
test.forbidden_spirv = {"OpBitFieldSExtract", "OpImageQuerySizeLod", "OpFDiv"};
return test;
}
template <u32 wave_size> TestCase ImageSampleOffsetsReadFourNeighbors() {
using O = ShaderOpcode;
TestCase test;
test.name = wave_size == 32 ? "ImageSampleOffsetsReadFourNeighbors32"
: "ImageSampleOffsetsReadFourNeighbors64";
test.compute_info.wave_size = wave_size;
test.has_compute_info = true;
constexpr std::array offsets{0x01u, 0x3fu, 0x3f00u, 0x0100u};
constexpr std::array opcodes{0x37u, 0x37u, 0x34u, 0x32u};
for (u32 sample = 0; sample < offsets.size(); ++sample) {
AppendVMovLiteral(&test.code, 20, offsets[sample]);
u32 coordinate = 21;
if (opcodes[sample] == 0x32u) {
for (float gradient : {0.25f, 0.f, 0.f, 0.25f}) {
AppendVMovLiteral(&test.code, coordinate++, std::bit_cast<u32>(gradient));
}
}
// All instructions address the center of texel (2, 1) before applying the offset.
AppendVMovLiteral(&test.code, coordinate, std::bit_cast<u32>(0.625f));
AppendVMovLiteral(&test.code, coordinate + 1, std::bit_cast<u32>(0.375f));
if (opcodes[sample] == 0x34u) AppendVMovU32(&test.code, coordinate + 2, 0);
test.code.push_back(EncodeMimg0(opcodes[sample], 0x1));
test.code.push_back(EncodeMimg1(sample, 20));
AppendStoreVgpr(&test.code, sample, sample);
}
AppendEnd(&test.code);
test.sampled_image_rgba = MakeRgbaImage(4, 4);
SetRgbaPixel(&test.sampled_image_rgba, 4, 2, 1, std::bit_cast<u32>(16.f), 0, 0, 0);
constexpr std::array<std::array<u32, 2>, 4> neighbors{{{3, 1}, {1, 1}, {2, 0}, {2, 2}}};
for (u32 sample = 0; sample < neighbors.size(); ++sample) {
const auto value = std::bit_cast<u32>(float(sample + 1));
SetRgbaPixel(&test.sampled_image_rgba, 4, neighbors[sample][0], neighbors[sample][1],
value, 0, 0, 0);
test.expected.push_back(value);
}
test.opcodes = {O::V_MOV_B32, O::IMAGE_SAMPLE, O::BUFFER_STORE_DWORD, O::S_ENDPGM};
test.required_spirv = {"Lod|ConstOffset", "Grad|ConstOffset"};
test.forbidden_spirv = {"OpImageQuerySizeLod", "OpFDiv", "OpBitFieldSExtract", "OpSwitch"};
return test;
}
@@ -34986,7 +35102,8 @@ TestCase ImageGatherCompareOpcodes() {
O::V_MOV_B32, O::IMAGE_GATHER4_C, O::IMAGE_GATHER4_C_LZ,
O::IMAGE_GATHER4_C_O, O::IMAGE_GATHER4_C_LZ_O, O::BUFFER_STORE_DWORD,
O::S_ENDPGM};
test.required_spirv = {"OpImageDrefGather", "OpBitFieldSExtract"};
test.required_spirv = {"OpImageDrefGather", "ConstOffset"};
test.forbidden_spirv = {"OpBitFieldSExtract"};
test.compile_only = true;
return test;
}
@@ -35538,7 +35655,7 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
std::memcpy(&memory_flag_bits, &memory_flags, sizeof(memory_flags));
auto &sample = block->AppendNewInst(
ValueOpcode::ImageSampleRaw,
{Value(&image), Value(&sampler), Value(&address)}, memory_flag_bits);
{Value(&image), Value(&sampler), Value(&address), Value(true)}, memory_flag_bits);
auto &sample_x = block->AppendNewInst(ValueOpcode::CompositeExtractU32x4,
{Value(&sample), Value(0u)});
block->AppendNewInst(ValueOpcode::ReferenceU32, {Value(&sample_x)});
@@ -35749,29 +35866,63 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
resource == 4u ? vk::ImageViewType::e2DArray : vk::ImageViewType::e2D, layers));
}
const auto native_sampler = vulkan.CreateSampler(test.name);
for (const bool fetch : {false, true}) {
enum class Access { Sample, Gather, GatherHomogeneous, Fetch };
auto *access = &sample;
for (const auto mode : {Access::Sample, Access::Gather,
Access::GatherHomogeneous, Access::Fetch}) {
const bool fetch = mode == Access::Fetch;
const bool gather = mode == Access::Gather || mode == Access::GatherHomogeneous;
const bool homogeneous = mode == Access::GatherHomogeneous;
if (mode == Access::Gather) {
test.name = "IndirectImageGatherContiguousRuns";
const auto position = std::ranges::find_if(*block, [&](const Inst &inst) {
return &inst == access;
});
auto &read = *block->PrependNewInst(
position, ValueOpcode::ImageGatherRaw,
{Value(&image), Value(&sampler), Value(&address), Value(0u), Value(0u),
Value(0u), Value(0u), Value(true)}, memory_flag_bits);
access->ReplaceUsesWith(Value(&read), false);
access = &read;
compiled.program.info.images[4].cube = false;
auto &gather_memory = compiled.program.memory_info[0];
gather_memory.dmask = 1;
gather_memory.image_sample_flags = ShaderRecompiler::Decoder::ImageSampleFlagLevelZero |
ShaderRecompiler::Decoder::ImageSampleFlagOffset;
// GATHER4_LZ_O places its packed signed offset before the coordinates.
address.SetArg(0, Value(0x3fu));
address.SetArg(1, Value(std::bit_cast<u32>(0.75f)));
address.SetArg(2, Value(std::bit_cast<u32>(0.25f)));
address.SetArg(3, Value(0u));
}
auto &selected_resources = compiled.program.info.images[0].indirect_resources;
selected_resources = homogeneous ? std::vector<u32>{0u, 2u, 3u, 5u, 6u}
: root.indirect_resources;
if (homogeneous) test.name = "IndirectImageGatherHomogeneous";
compiled.resources.flattened_srt[6] = homogeneous ? 4u : ordinals[5];
if (fetch) {
test.name = "IndirectImageFetchContiguousRuns";
const auto position = std::ranges::find_if(*block, [&](const Inst &inst) {
return &inst == &sample;
return &inst == access;
});
auto &read = *block->PrependNewInst(
position, ValueOpcode::ImageRead,
{Value(&image), Value(&address), Value(true)}, memory_flag_bits);
sample.ReplaceUsesWith(Value(&read), false);
access->ReplaceUsesWith(Value(&read), false);
sampler.Invalidate();
compiled.program.info.samplers.clear();
compiled.program.info.sampled_pairs.clear();
compiled.program.info.images[4].cube = false;
compiled.program.memory_info[0].dmask = 0xf;
compiled.program.memory_info[0].image_sample_flags = 0;
compiled.program.memory_info[0].image_has_mip = true;
for (u32 component = 0; component < 4u; ++component)
address.SetArg(component, Value(0u));
// Native DIM=2DArray places LOD after the layer, even for a 2D candidate.
address.SetArg(3, Value(1u));
compiled.program.shader_info_complete = false;
CollectShaderInfo(compiled.program, {.compute = &compute});
}
compiled.program.shader_info_complete = false;
CollectShaderInfo(compiled.program, {.compute = &compute});
for (const u32 wave_size : {32u, 64u}) {
compute.wave_size = wave_size;
compute.host_subgroup_size = vulkan.SubgroupSize();
@@ -35782,31 +35933,38 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
compiled.packed_user_data.resize(compiled.program.bindings.ShaderDataDwords());
ValidateSpirv(test.name, compiled.spirv);
const auto halves = wave_size > compute.host_subgroup_size ? 2u : 1u;
const auto image_opcode = fetch ? "OpImageFetch" : gather ? "OpImageGather"
: "OpImageSampleExplicitLod";
Require(test.name, "native array image runs", tools.Disassemble(compiled.spirv, &text) &&
CountText(text, fetch ? "OpImageFetch" : "OpImageSampleExplicitLod") == 3u * halves,
CountText(text, image_opcode) == (homogeneous ? 1u : 3u) * halves,
"mixed runs expanded into per-image accesses");
if (fetch) {
if (homogeneous) {
Require(test.name, "homogeneous gather array", text.find("OpSwitch") == std::string::npos,
"homogeneous gather emitted a descriptor switch");
}
if (fetch || gather) {
std::vector<u32> decorated;
std::vector<u32> fetched_images;
std::vector<u32> image_operands;
for (size_t offset = 5; offset < compiled.spirv.size();) {
const auto words = std::span<const u32>(compiled.spirv).subspan(
offset, compiled.spirv[offset] >> 16u);
const auto opcode = static_cast<spv::Op>(words[0] & 0xffffu);
if (opcode == spv::OpDecorate && words[2] == spv::DecorationNonUniform)
decorated.push_back(words[1]);
if (opcode == spv::OpImageFetch) fetched_images.push_back(words[3]);
if (opcode == (fetch ? spv::OpImageFetch : spv::OpImageGather))
image_operands.push_back(words[3]);
offset += words.size();
}
Require(test.name, "nonuniform fetch operands",
std::ranges::count_if(fetched_images, [&](u32 operand) {
Require(test.name, "nonuniform image operands",
std::ranges::count_if(image_operands, [&](u32 operand) {
return std::ranges::find(decorated, operand) != decorated.end();
}) == 2u * halves,
"a dynamically selected fetch image lacks its nonuniform decoration");
}) == (homogeneous ? 1u : 2u) * halves,
"a dynamically selected image operand lacks its nonuniform decoration");
}
test.initial.assign(wave_size, 0xdeadbeefu);
test.expected.assign(wave_size, 0u);
for (u32 lane = 0; lane < ordinals.size(); ++lane) {
const auto resource = root.indirect_resources[ordinals[lane]];
const auto resource = selected_resources[compiled.resources.flattened_srt[1u + lane]];
test.expected[lane] = std::bit_cast<u32>(float(resource + (fetch && resource != 0u ? 10u : 0u)));
}
auto buffer = vulkan.CreateStorageBuffer(test.name, test.initial, test.initial.size());
@@ -35814,7 +35972,7 @@ void CheckIndirectImageKeySwitch(VulkanHarness &vulkan) {
fetch ? vk::Sampler{} : native_sampler, textures);
const auto actual = vulkan.ReadBuffer(test.name, buffer, test.expected.size());
vulkan.DestroyBuffer(&buffer);
CompareWords(test, "root, native slots, cube and mapping bounds", test.expected, actual);
CompareWords(test, "root, native slots, array runs and mapping bounds", test.expected, actual);
}
std::printf("[compute] %-32s ok\n", test.name);
}
@@ -37659,7 +37817,10 @@ std::vector<TestCase> MakeCases() {
AddCase(ImageD16GatherPacksHalfPairs);
AddCase(ImageSampleA16SamplerCoordsOnGpu);
AddCase(ImageSampleOpcodeAliasUsesNormalCoords);
AddCase(ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu);
AddCase(ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<32>);
AddCase(ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<64>);
AddCase(ImageSampleOffsetsReadFourNeighbors<32>);
AddCase(ImageSampleOffsetsReadFourNeighbors<64>);
AddCase(ImageSampleA16CompareBiasRdna2AddressOrder);
AddCase(ImageGatherCompareOpcodes);
AddCase(ImageStoreVariants);
@@ -40070,7 +40231,7 @@ void CheckImageSamplerSpecialization() {
sizeof(signed_memory_flags));
mixed_sampler_block->AppendNewInst(
ValueOpcode::ImageSampleRaw,
{Value(&image_handle), Value(&sampler_handle), Value(&image_address)},
{Value(&image_handle), Value(&sampler_handle), Value(&image_address), Value(true)},
signed_memory_flag_bits);
ShaderRecompiler::IR::DescriptorValue native_image_descriptor{};
@@ -43327,6 +43488,16 @@ int main(int argc, char **argv) {
RunCase(&vulkan, FlatVirtualAddressRebasesGuestAllocation());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--image-sample-offset-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, ImageSampleOffsetsReadFourNeighbors<32>());
RunCase(&vulkan, ImageSampleOffsetsReadFourNeighbors<64>());
RunCase(&vulkan, ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<32>());
RunCase(&vulkan, ImageSampleA16OffsetKeepsTexelOffset32BitOnGpu<64>());
RunCase(&vulkan, ImageSampleAndGather());
RunCase(nullptr, ImageGatherCompareOpcodes());
return 0;
}
if (argc == 2 && std::strcmp(argv[1], "--image-gather-lod-only") == 0) {
VulkanHarness vulkan;
RunCase(&vulkan, ImageGatherExplicitLod());
+1 -1
View File
@@ -1316,7 +1316,7 @@ void TestShaderFeatureCollection() {
{Value(0u), Value(0u), Value(0u)});
block->AppendNewInst(ValueOpcode::ImageGatherRaw,
{Value(0u), Value(0u), Value(0u), Value(0u),
Value(0u), Value(0u), Value(0u)});
Value(0u), Value(0u), Value(0u), Value(true)});
auto &shared = block->AppendNewInst(ValueOpcode::LoadSharedU32,
{Value(0u), Value(true)});
shared.SetFlags(MemoryFlags{.index = 0});