From bec67b78639dfaf212141479d0028921845de3c8 Mon Sep 17 00:00:00 2001 From: Pcniado <122915996+Pcniado@users.noreply.github.com> Date: Sun, 27 Sep 2026 00:13:30 +0200 Subject: [PATCH] Fix HITMAN 3 boot, layered presentation, and socket imports (#851) * videoout: synchronize layered flips with premultiplied composition * shader: support unsigned FFBH subword sources * shader: apply cube-face result scaling * shader: prune buffer reads proven out of bounds * net: implement send and receive exports with shared socket handling --------- Co-authored-by: nmzik --- CMakeLists.txt | 1 + src/graphics/guest_gpu/graphicsRun.cpp | 3 +- .../renderer/pipeline/descriptors.cpp | 26 +- src/graphics/host_gpu/renderer/sync.cpp | 2 +- .../shaders/gpu_video_out_overlay.frag | 15 + src/graphics/presentation/presenter.h | 12 +- src/graphics/presentation/videoOut.cpp | 268 +++++++--- src/graphics/presentation/videoOut.h | 5 +- .../presentation/window/swapchain.cpp | 474 ++++++++++++------ .../recompiler/backend/spirv/SpirvEmitter.cpp | 26 +- .../frontend/decode/VectorAluOps.cpp | 2 + .../recompiler/ir/passes/BindingLayout.cpp | 49 +- .../recompiler/ir/passes/BindingLayout.h | 3 + .../ir/passes/ResourceMaterialization.cpp | 45 ++ .../ir/passes/ResourceMaterialization.h | 1 + src/libs/libNet.cpp | 14 + src/libs/libVideoOut.cpp | 2 + src/libs/network.cpp | 1 + tests/KernelFileSystemTests.cpp | 72 ++- tests/ShaderRecompilerComputeTests.cpp | 218 +++++++- tests/shaderCfgTests.cpp | 7 +- 21 files changed, 960 insertions(+), 286 deletions(-) create mode 100644 src/graphics/host_gpu/shaders/gpu_video_out_overlay.frag diff --git a/CMakeLists.txt b/CMakeLists.txt index dcc17072..34a1a65f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -301,6 +301,7 @@ 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) diff --git a/src/graphics/guest_gpu/graphicsRun.cpp b/src/graphics/guest_gpu/graphicsRun.cpp index 62d03c77..94510e0b 100644 --- a/src/graphics/guest_gpu/graphicsRun.cpp +++ b/src/graphics/guest_gpu/graphicsRun.cpp @@ -1502,8 +1502,9 @@ void CommandProcessor::PrepareCpuFlip(uint64_t request_id) { ProcessorScope processor_scope(*this); m_renderer.GetVideoOut().PrepareFlip(request_id, command); + GetScheduler().DeferPriorityOperation( + [this, request_id] { m_renderer.GetVideoOut().CompleteFlip(request_id); }); GetScheduler().Flush(); - m_renderer.GetVideoOut().CompleteFlip(request_id); } void CommandProcessor::SynchronizeGpu() { diff --git a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp index 34310409..aeaf9472 100644 --- a/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp +++ b/src/graphics/host_gpu/renderer/pipeline/descriptors.cpp @@ -798,9 +798,14 @@ void RenderExecutor::FindBuffers(PreparedBindings& prepared) { auto& cache = m_context.GetBufferCache(); prepared.buffer_sources.clear(); - prepared.buffer_sources.reserve(program.info.buffers.size()); - for (uint32_t i = 0; i < program.info.buffers.size(); i++) { - auto descriptor = DecodeNativeDescriptor(snapshot.buffers[i]); + const auto& layout = program.bindings; + if (layout.memory_offset_count == 0) { + return; + } + const auto& resources = layout.descriptors.front().resources; + prepared.buffer_sources.reserve(resources.size()); + for (const auto resource: resources) { + auto descriptor = DecodeNativeDescriptor(snapshot.buffers[resource]); const auto address = descriptor.Base48(); const auto requested_size = descriptor.GetSize(); if (address == 0 || requested_size == 0) { @@ -818,10 +823,10 @@ void RenderExecutor::RebindBuffers(PreparedBindings& prepared) { const auto& program = *prepared.runtime->program; const auto& snapshot = *prepared.runtime->resources; const auto& layout = program.bindings; - EXIT_IF(prepared.buffer_sources.size() != program.info.buffers.size()); + EXIT_IF(prepared.buffer_sources.size() != layout.memory_offset_count); prepared.buffers.clear(); - prepared.buffers.reserve(program.info.buffers.size()); + prepared.buffers.reserve(layout.memory_offset_count); EXIT_IF(prepared.shader_data.size() != layout.ShaderDataDwords()); std::fill(prepared.shader_data.begin() + layout.memory_offset_dword, prepared.shader_data.end(), 0); @@ -830,11 +835,12 @@ void RenderExecutor::RebindBuffers(PreparedBindings& prepared) { const auto shift = (index % 4u) * 8u; prepared.shader_data[dword] |= offset << shift; }; - for (uint32_t i = 0; i < program.info.buffers.size(); i++) { + for (uint32_t i = 0; i < layout.memory_offset_count; i++) { + const auto resource = layout.descriptors.front().resources[i]; uint32_t buffer_offset = 0; prepared.buffers.push_back(NativeStorageBuffer(m_context, prepared.buffer_sources[i], - program.info.buffers[i], program.stage, i, - buffer_offset)); + program.info.buffers[resource], + program.stage, resource, buffer_offset)); pack_memory_offset(i, buffer_offset); } if (ShaderRecompiler::IR::FindBinding( @@ -1045,8 +1051,8 @@ void RenderExecutor::CommitBindings(CommandBuffer& buffer, } else { switch (binding.kind) { case BindingKind::Buffers: - for (const auto resource: binding.resources) { - const auto& view = descriptors.buffers.at(resource); + EXIT_IF(descriptors.buffers.size() != binding.resources.size()); + for (const auto& view: descriptors.buffers) { EXIT_IF(view.buffer == nullptr); m_descriptor_buffers.push_back(view); } diff --git a/src/graphics/host_gpu/renderer/sync.cpp b/src/graphics/host_gpu/renderer/sync.cpp index 5a6e89f6..5229746d 100644 --- a/src/graphics/host_gpu/renderer/sync.cpp +++ b/src/graphics/host_gpu/renderer/sync.cpp @@ -172,7 +172,7 @@ uint64_t PrepareVideoOutFlip(CommandBuffer& buffer, int handle, int index, int f "\n", result, handle, index, flip_mode, flip_arg); } - video_out.WaitForSubmitSlot(); + video_out.WaitForSubmitSlot(handle); } } diff --git a/src/graphics/host_gpu/shaders/gpu_video_out_overlay.frag b/src/graphics/host_gpu/shaders/gpu_video_out_overlay.frag new file mode 100644 index 00000000..63c33072 --- /dev/null +++ b/src/graphics/host_gpu/shaders/gpu_video_out_overlay.frag @@ -0,0 +1,15 @@ +#version 450 + +layout(location = 0) in vec2 uv; +layout(location = 0) out vec4 color; +layout(binding = 0) uniform sampler2D source; +layout(push_constant) uniform AlphaControl { + uint premultiplied; +}; + +void main() { + color = texture(source, uv); + if (premultiplied == 0u) { + color.rgb *= color.a; + } +} diff --git a/src/graphics/presentation/presenter.h b/src/graphics/presentation/presenter.h index 4399e58a..f5dc36b2 100644 --- a/src/graphics/presentation/presenter.h +++ b/src/graphics/presentation/presenter.h @@ -4,6 +4,7 @@ #include "common/common.h" #include +#include namespace Libs::Graphics { @@ -15,6 +16,11 @@ struct WindowContext; class Presenter final { public: struct Frame; + struct Layer { + Frame* frame; + int bus; + bool premultiplied_alpha; + }; explicit Presenter(WindowContext& window); ~Presenter(); @@ -23,11 +29,13 @@ public: [[nodiscard]] Frame& PrepareFrame(CommandBuffer& command, const ImageInfo& info); [[nodiscard]] Frame& PrepareBlankFrame(uint32_t width, uint32_t height, bool opaque, CommandBuffer* producer = nullptr); - [[nodiscard]] Frame* PrepareLastFrame(); + [[nodiscard]] bool PresentLastFrame(); [[nodiscard]] bool IsGuestPaused() const noexcept; [[nodiscard]] bool NeedsSystemOverlayRefresh() const noexcept; [[nodiscard]] RenderContext& Renderer() const noexcept; - void Present(Frame& frame, bool reuse = false); + void Present(Frame& frame); + void Present(std::span layers); + void ClearLayer(int bus); void Discard(Frame& frame); private: diff --git a/src/graphics/presentation/videoOut.cpp b/src/graphics/presentation/videoOut.cpp index 41ce0542..a73ba6e6 100644 --- a/src/graphics/presentation/videoOut.cpp +++ b/src/graphics/presentation/videoOut.cpp @@ -51,6 +51,8 @@ constexpr int VIDEO_OUT_BUS_TYPE_OVERLAY = 1; constexpr int VIDEO_OUT_BUS_TYPE_SUB = 2; constexpr int VIDEO_OUT_FLIP_MODE_VSYNC = 1; constexpr int VIDEO_OUT_FLIP_MODE_VSYNC_MULTI = 4; +constexpr int VIDEO_OUT_FLIP_MODE_SLAVE = 8; +constexpr int VIDEO_OUT_FLIP_MODE_MASTER = 9; constexpr int VIDEO_OUT_BUFFER_INDEX_BLACK = -2; constexpr int VIDEO_OUT_BUFFER_INDEX_BLANK = -1; constexpr int VIDEO_OUT_BUFFER_NUM_MAX = 16; @@ -63,6 +65,7 @@ constexpr uint64_t VIDEO_OUT_REFRESH_RATE_119_88HZ = 13; constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_UNCOMPRESSED = 0; constexpr int VIDEO_OUT_BUFFER_ATTRIBUTE_CATEGORY_COMPRESSED = 1; constexpr uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY = 8; +constexpr uint64_t VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED = 32; enum class VideoOutEventKind : uintptr_t { Flip = VIDEO_OUT_EVENT_FLIP, @@ -183,6 +186,9 @@ struct VideoOutConfig { uint64_t generation = 0; bool opened = false; bool closing = false; + int bus = 0; + VideoOutConfig* master = nullptr; + uint32_t slave_count = 0; int flip_rate = 0; uint64_t output_mode = VIDEO_OUT_OUTPUT_MODE_DEFAULT; float gamma = 1.0f; @@ -206,7 +212,7 @@ public: void Cancel(VideoOutConfig& cfg); void Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer); void Complete(uint64_t request_id); - void WaitForSubmitSlot(); + void WaitForSubmitSlot(VideoOutConfig& cfg); bool Flip(uint32_t micros); void GetFlipStatus(VideoOutConfig& cfg, VideoOutFlipStatus& out); void Wait(VideoOutConfig& cfg, int index); @@ -216,7 +222,9 @@ private: struct Request { uint64_t id; + uint64_t group; VideoOutConfig* cfg; + VideoOutConfig* master; uint64_t generation; int index; int64_t flip_arg; @@ -224,6 +232,7 @@ private: FlipRequestSource source; RequestState state; Graphics::Presenter::Frame* frame; + bool premultiplied_alpha; }; Graphics::Presenter& m_presenter; @@ -258,6 +267,7 @@ public: VideoOutConfig* Get(int handle); VideoOutConfig* Get(int handle, uint64_t& generation); bool IsOpened(int handle); + int SetFlipMaster(int slave_handle, int master_handle); void Init(uint32_t width, uint32_t height); FlipQueue& GetFlipQueue() { return m_flip_queue; } @@ -498,7 +508,8 @@ static bool IsSpecialBufferIndex(int index) { } static bool IsValidFlipMode(int mode) { - return mode >= VIDEO_OUT_FLIP_MODE_VSYNC && mode <= VIDEO_OUT_FLIP_MODE_VSYNC_MULTI; + return (mode >= VIDEO_OUT_FLIP_MODE_VSYNC && mode <= VIDEO_OUT_FLIP_MODE_VSYNC_MULTI) || + mode == VIDEO_OUT_FLIP_MODE_SLAVE || mode == VIDEO_OUT_FLIP_MODE_MASTER; } static int ReserveFlipRequest(VideoOutDriver::Impl& driver, int handle, int index, int flip_mode, @@ -508,13 +519,17 @@ static int ReserveFlipRequest(VideoOutDriver::Impl& driver, int handle, int inde return VIDEO_OUT_ERROR_INVALID_HANDLE; } if (!IsValidFlipMode(flip_mode)) { - return VIDEO_OUT_ERROR_INVALID_VALUE; + return VIDEO_OUT_ERROR_INVALID_FLIP_MODE; } if (!IsValidBufferIndex(index)) { return VIDEO_OUT_ERROR_INVALID_INDEX; } Common::LockGuard lock(video_out->mutex); + if ((video_out->master != nullptr) != (flip_mode == VIDEO_OUT_FLIP_MODE_SLAVE) || + (video_out->slave_count != 0) != (flip_mode == VIDEO_OUT_FLIP_MODE_MASTER)) { + return VIDEO_OUT_ERROR_INVALID_FLIP_MODE; + } if (video_out->closing || (!IsSpecialBufferIndex(index) && !video_out->buffers[index].Occupied())) { return VIDEO_OUT_ERROR_INVALID_INDEX; @@ -532,8 +547,8 @@ Graphics::ImageInfo BufferAttributeGroup::ImageInfo(const VideoOutBuffer& buffer if (attribute.reserved0 != 0 || attribute.aspect_ratio != 0 || attribute.width == 0 || attribute.height == 0 || attribute.width > 16384 || attribute.height > 16384 || attribute.pitch_in_pixel != 0 || - (attribute.option != 0 && - attribute.option != VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY) || + (attribute.option & ~(VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_STRICT_COLORIMETRY | + VIDEO_OUT_BUFFER_ATTRIBUTE_OPTION_ALPHA_PREMULTIPLIED)) != 0 || attribute.tiling_mode != 0 || attribute.pad0 != 0 || attribute.reserved1[0] != 0 || attribute.reserved1[1] != 0 || attribute.reserved1[2] != 0 || buffer.data_address == 0 || compression == Graphics::VideoOutCompression::Unsupported) { @@ -616,6 +631,11 @@ VideoOutDriver::Impl::~Impl() { m_present_thread.request_stop(); m_present_thread.join(); } + for (int handle = 1; handle < VIDEO_OUT_NUM_MAX; handle++) { + if (m_video_out_ctx[handle].master != nullptr) { + (void)Close(handle); + } + } for (int handle = 1; handle < VIDEO_OUT_NUM_MAX; handle++) { (void)Close(handle); } @@ -660,6 +680,7 @@ int VideoOutDriver::Impl::Open(int bus_type, int index) { config.closing = false; config.opened = true; + config.bus = bus_type; if (++config.generation == 0) { EXIT("video-out port generation wrapped\n"); } @@ -686,11 +707,13 @@ bool VideoOutDriver::Impl::Close(int handle) { VideoOutEventQueues pre_vblank_events; VideoOutEventQueues vblank_events; VideoOutEventQueues output_mode_events; + VideoOutConfig* master = nullptr; { Common::LockGuard config_lock(config.mutex); if (config.closing) { return false; } + EXIT_IF(config.slave_count != 0); config.opened = false; config.closing = true; if (++config.generation == 0) { @@ -703,7 +726,9 @@ bool VideoOutDriver::Impl::Close(int handle) { vblank_events = std::move(config.events->vblank); output_mode_events = std::move(config.events->output_mode); } - config.flip_rate = 0; + config.flip_rate = 0; + master = config.master; + config.master = nullptr; for (const auto& buffer: config.buffers) { if (buffer.Occupied() && @@ -720,8 +745,12 @@ bool VideoOutDriver::Impl::Close(int handle) { } config.vblank_cond.SignalAll(); } - + if (master != nullptr) { + Common::LockGuard master_lock(master->mutex); + master->slave_count--; + } m_flip_queue.Cancel(config); + m_presenter.ClearLayer(config.bus); DeleteVideoOutEvents(flip_events, VideoOutEventKind::Flip); DeleteVideoOutEvents(pre_vblank_events, VideoOutEventKind::PreVblankStart); DeleteVideoOutEvents(vblank_events, VideoOutEventKind::Vblank); @@ -759,6 +788,31 @@ bool VideoOutDriver::Impl::IsOpened(int handle) { return handle > 0 && handle < VIDEO_OUT_NUM_MAX && m_video_out_ctx[handle].opened; } +int VideoOutDriver::Impl::SetFlipMaster(int slave_handle, int master_handle) { + Common::LockGuard lock(m_mutex); + if (slave_handle <= 0 || slave_handle >= VIDEO_OUT_NUM_MAX || + master_handle <= 0 || master_handle >= VIDEO_OUT_NUM_MAX || + !m_video_out_ctx[slave_handle].opened || !m_video_out_ctx[master_handle].opened) { + return VIDEO_OUT_ERROR_INVALID_HANDLE; + } + auto& slave = m_video_out_ctx[slave_handle]; + auto& master = m_video_out_ctx[master_handle]; + if (&slave == &master) { + return VIDEO_OUT_ERROR_UNSUPPORTED_OPERATION; + } + Common::LockGuard first_lock(m_video_out_ctx[std::min(slave_handle, master_handle)].mutex); + Common::LockGuard second_lock(m_video_out_ctx[std::max(slave_handle, master_handle)].mutex); + if (slave.master != nullptr || slave.slave_count != 0 || master.master != nullptr) { + return VIDEO_OUT_ERROR_UNSUPPORTED_OPERATION; + } + if (slave.flip_status.flipPendingNum != 0 || master.flip_status.flipPendingNum != 0) { + return VIDEO_OUT_ERROR_RESOURCE_BUSY; + } + slave.master = &master; + master.slave_count++; + return OK; +} + void VideoOutDriver::Impl::VblankBegin() { Common::LockGuard lock(m_mutex); @@ -821,9 +875,7 @@ void VideoOutDriver::Impl::PresentThread(std::stop_token token) { const auto period = std::max(frequency / refresh, uint64_t {1}); if (m_presenter.IsGuestPaused()) { - if (auto* frame = m_presenter.PrepareLastFrame(); frame != nullptr) { - m_presenter.Present(*frame, true); - } + (void)m_presenter.PresentLastFrame(); const auto frame_end = Common::Timer::QueryPerformanceCounter(); total_wait += static_cast(period) - static_cast(frame_end - frame_begin); @@ -833,8 +885,7 @@ void VideoOutDriver::Impl::PresentThread(std::stop_token token) { VblankBegin(); bool presented = m_flip_queue.Flip(0); if (!presented && m_presenter.NeedsSystemOverlayRefresh()) { - if (auto* frame = m_presenter.PrepareLastFrame(); frame != nullptr) { - m_presenter.Present(*frame, true); + if (m_presenter.PresentLastFrame()) { presented = true; } else { uint32_t width = 0; @@ -877,25 +928,57 @@ bool FlipQueue::Reserve(VideoOutConfig& cfg, int index, int64_t flip_arg, FlipRe uint64_t& request_id) { Common::LockGuard lock(m_mutex); - if (m_requests.size() + m_cpu_requests.size() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) { + // A newer slave request replaces the one not yet captured by a master flip. + if (cfg.master != nullptr) { + for (auto* queue: {&m_requests, &m_cpu_requests}) { + for (auto it = queue->begin(); it != queue->end();) { + if (it->cfg != &cfg || it->group != 0) { + ++it; + continue; + } + cfg.flip_status.flipPendingNum--; + cfg.flip_status.gcQueueNum -= it->source == FlipRequestSource::GpuEop; + if (it->state == RequestState::Ready) { + m_presenter.Discard(*it->frame); + it = queue->erase(it); + } else { + auto cancelled = it++; + m_cancelled_requests.splice(m_cancelled_requests.end(), *queue, cancelled); + } + } + } + m_done_cond_var.SignalAll(); + } + if (cfg.flip_status.flipPendingNum >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) { return false; } auto& pending = source == FlipRequestSource::GpuEop ? m_requests : m_cpu_requests; Request r {}; r.id = m_next_request_id++; + r.group = cfg.master == nullptr ? r.id : 0; r.cfg = &cfg; + r.master = cfg.master; r.generation = cfg.generation; r.index = index; r.flip_arg = flip_arg; r.submit_ptc = LibKernel::KernelGetProcessTimeCounter(); r.source = source; r.state = RequestState::Reserved; + if (cfg.slave_count != 0) { + for (auto* queue: {&m_requests, &m_cpu_requests}) { + for (auto& slave: *queue) { + if (slave.master == &cfg && slave.group == 0) { + slave.group = r.id; + } + } + } + } pending.push_back(r); request_id = r.id; - cfg.flip_status.flipPendingNum = static_cast(m_requests.size() + m_cpu_requests.size()); + cfg.flip_status.flipPendingNum++; cfg.flip_status.submitProcessTimeCounter = r.submit_ptc; if (source == FlipRequestSource::GpuEop) { cfg.flip_status.gcQueueNum++; @@ -917,7 +1000,9 @@ FlipQueue::~FlipQueue() { void FlipQueue::Cancel(VideoOutConfig& cfg) { std::vector frames; m_mutex.Lock(); - while (m_processing && !m_requests.empty() && m_requests.front().cfg == &cfg) { + while (m_processing && std::any_of(m_requests.begin(), m_requests.end(), [&cfg](const auto& r) { + return r.cfg == &cfg && r.state == RequestState::Presenting; + })) { m_done_cond_var.Wait(&m_mutex); } for (auto* queue: {&m_requests, &m_cpu_requests}) { @@ -991,6 +1076,7 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) { uint32_t width = 0; uint32_t height = 0; bool current = false; + bool premultiplied_alpha = false; { Common::LockGuard lock(cfg->mutex); current = cfg->opened && !cfg->closing && cfg->generation == generation; @@ -1012,6 +1098,9 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) { 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; } } } @@ -1051,6 +1140,7 @@ void FlipQueue::Prepare(uint64_t request_id, Graphics::CommandBuffer& buffer) { EXIT("video-out request changed while recording, id=%" PRIu64 "\n", request_id); } prepared->frame = frame; + prepared->premultiplied_alpha = premultiplied_alpha; } void FlipQueue::Complete(uint64_t request_id) { @@ -1082,9 +1172,12 @@ void FlipQueue::Complete(uint64_t request_id) { } } -void FlipQueue::WaitForSubmitSlot() { +void FlipQueue::WaitForSubmitSlot(VideoOutConfig& cfg) { Common::LockGuard lock(m_mutex); - while (m_requests.size() + m_cpu_requests.size() >= VIDEO_OUT_FLIP_QUEUE_CAPACITY) { + auto belongs_to_port = [&cfg](const auto& request) { return request.cfg == &cfg; }; + while (std::count_if(m_requests.begin(), m_requests.end(), belongs_to_port) + + std::count_if(m_cpu_requests.begin(), m_cpu_requests.end(), belongs_to_port) >= + VIDEO_OUT_FLIP_QUEUE_CAPACITY) { if (m_requests.empty()) { EXIT("video-out queue is saturated by CPU flips queued behind the current EOP\n"); } @@ -1111,75 +1204,90 @@ bool FlipQueue::Flip(uint32_t micros) { m_mutex.Lock(); if (m_requests.empty()) { m_submit_cond_var.WaitFor(&m_mutex, micros); - - if (m_requests.empty()) { - m_mutex.Unlock(); - return false; - } } - if (m_processing) { - EXIT("video-out flip queue processing is already active\n"); - } - if (m_requests.front().state != RequestState::Ready) { + EXIT_IF(m_processing); + const auto owner = std::find_if(m_requests.begin(), m_requests.end(), + [](const auto& r) { return r.group == r.id; }); + if (owner == m_requests.end()) { m_mutex.Unlock(); return false; } - m_processing = true; - auto r = m_requests.front(); - m_mutex.Unlock(); - - r.cfg->mutex.Lock(); - if (!IsFlipDueLocked(*r.cfg, r.generation)) { - r.cfg->mutex.Unlock(); - Common::LockGuard queue_lock(m_mutex); - m_processing = false; - m_done_cond_var.SignalAll(); + const uint64_t group = owner->id; + if (std::any_of(m_cpu_requests.begin(), m_cpu_requests.end(), + [group](const auto& r) { return r.group == group; }) || + std::any_of(m_requests.begin(), m_requests.end(), [group](const auto& r) { + return r.group == group && r.state != RequestState::Ready; + })) { + m_mutex.Unlock(); return false; } - - m_mutex.Lock(); - if (m_requests.empty() || m_requests.front().id != r.id || - m_requests.front().state != RequestState::Ready || !m_processing) { - EXIT("video-out request changed before presentation, id=%" PRIu64 "\n", r.id); + std::array requests {}; + size_t count = 0; + for (auto& request: m_requests) { + if (request.group == group) { + EXIT_IF(count == requests.size()); + request.state = RequestState::Presenting; + requests[count++] = request; + } } - m_requests.front().state = RequestState::Presenting; + m_processing = true; m_mutex.Unlock(); - m_presenter.Present(*r.frame); + // Lock each port in bus order, then present and complete the whole group at one Vblank. + std::sort(requests.begin(), requests.begin() + count, + [](const auto& a, const auto& b) { return a.cfg->bus < b.cfg->bus; }); + bool due = true; + std::array layers {}; + for (size_t i = 0; i < count; i++) { + auto& r = requests[i]; + r.cfg->mutex.Lock(); + due &= r.cfg->opened && !r.cfg->closing && r.cfg->generation == r.generation; + if (r.id == group) { + due &= IsFlipDueLocked(*r.cfg, r.generation); + } + layers[i] = {r.frame, r.cfg->bus, r.premultiplied_alpha}; + } + if (due) { + m_presenter.Present(std::span(layers.data(), count)); + } m_mutex.Lock(); - if (m_requests.empty() || m_requests.front().id != r.id || - m_requests.front().state != RequestState::Presenting) { - EXIT("video-out flip queue changed while processing its front request\n"); + for (auto it = m_requests.begin(); it != m_requests.end();) { + if (it->group != group) { + ++it; + continue; + } + if (!due) { + it++->state = RequestState::Ready; + continue; + } + auto& r = *it; + r.cfg->flip_status.count++; + r.cfg->flip_status.processTime = LibKernel::KernelGetProcessTime(); + r.cfg->flip_status.processTimeCounter = LibKernel::KernelGetProcessTimeCounter(); + r.cfg->flip_status.submitProcessTimeCounter = r.submit_ptc; + r.cfg->flip_status.flipArg = r.flip_arg; + r.cfg->flip_status.currentBuffer = r.index; + r.cfg->flip_status.flipPendingNum--; + r.cfg->flip_status.gcQueueNum -= r.source == FlipRequestSource::GpuEop; + TriggerVideoOutEvents(*r.cfg, VideoOutEventKind::Flip, reinterpret_cast(r.flip_arg)); + it = m_requests.erase(it); } - m_requests.pop_front(); - - r.cfg->flip_status.count++; - r.cfg->flip_status.processTime = LibKernel::KernelGetProcessTime(); - r.cfg->flip_status.processTimeCounter = LibKernel::KernelGetProcessTimeCounter(); - r.cfg->flip_status.submitProcessTimeCounter = r.submit_ptc; - r.cfg->flip_status.flipArg = r.flip_arg; - r.cfg->flip_status.currentBuffer = r.index; - r.cfg->flip_status.flipPendingNum = static_cast(m_requests.size() + m_cpu_requests.size()); - if (r.source == FlipRequestSource::GpuEop && r.cfg->flip_status.gcQueueNum > 0) { - r.cfg->flip_status.gcQueueNum--; - } - TriggerVideoOutEvents(*r.cfg, VideoOutEventKind::Flip, reinterpret_cast(r.flip_arg)); - m_processing = false; m_done_cond_var.SignalAll(); - m_submit_slot_cond_var.Signal(); + m_submit_slot_cond_var.SignalAll(); m_mutex.Unlock(); - r.cfg->mutex.Unlock(); - - Graphics::RenderDocOnGuestFlip(m_presenter.Renderer()); - - if (Config::GraphicsDebugDumpEnabled() && - Config::GetPrintfDirection() != Config::LogDirection::Silent) { - LOGF("Flip done: %d\n", r.index); + for (size_t i = count; i != 0; i--) { + requests[i - 1].cfg->mutex.Unlock(); } - - return true; + if (due) { + Graphics::RenderDocOnGuestFlip(m_presenter.Renderer()); + if (Config::GraphicsDebugDumpEnabled() && + Config::GetPrintfDirection() != Config::LogDirection::Silent) { + LOGF("Flip done: %d\n", requests[0].index); + } + } + return due; } void FlipQueue::GetFlipStatus(VideoOutConfig& cfg, VideoOutFlipStatus& out) { @@ -1188,6 +1296,18 @@ void FlipQueue::GetFlipStatus(VideoOutConfig& cfg, VideoOutFlipStatus& out) { out = cfg.flip_status; } +KYTY_SYSV_ABI int VideoOutSetFlipMaster(int slave_handle, int master_handle) { + PRINT_NAME(); + return DriverState().SetFlipMaster(slave_handle, master_handle); +} + +KYTY_SYSV_ABI void VideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* attribute, + uint64_t option) { + PRINT_NAME(); + EXIT_IF(attribute == nullptr); + attribute->option |= option; +} + KYTY_SYSV_ABI int VideoOutOpen(int user_id, int bus_type, int index, const void* param) { PRINT_NAME(); @@ -1467,7 +1587,7 @@ KYTY_SYSV_ABI int VideoOutSubmitFlip(int handle, int index, int flip_mode, int64 uint64_t request_id = 0; const int result = ReserveFlipRequest(DriverState(), handle, index, flip_mode, flip_arg, FlipRequestSource::Cpu, request_id); - if (result == VIDEO_OUT_ERROR_INVALID_VALUE) { + if (result == VIDEO_OUT_ERROR_INVALID_FLIP_MODE) { LOGF("\t unsupported flip_mode = %d\n", flip_mode); } if (result != OK) { @@ -1504,8 +1624,10 @@ void VideoOutDriver::CompleteFlip(uint64_t request_id) { m_impl->GetFlipQueue().Complete(request_id); } -void VideoOutDriver::WaitForSubmitSlot() { - m_impl->GetFlipQueue().WaitForSubmitSlot(); +void VideoOutDriver::WaitForSubmitSlot(int handle) { + auto* cfg = m_impl->Get(handle); + EXIT_IF(cfg == nullptr); + m_impl->GetFlipQueue().WaitForSubmitSlot(*cfg); } void VideoOutDriver::WaitFlipDone(int handle, int index) { diff --git a/src/graphics/presentation/videoOut.h b/src/graphics/presentation/videoOut.h index 58f16c67..06f99506 100644 --- a/src/graphics/presentation/videoOut.h +++ b/src/graphics/presentation/videoOut.h @@ -37,7 +37,7 @@ public: void PrepareFlip(uint64_t request_id, Graphics::CommandBuffer& buffer); void CompleteFlip(uint64_t request_id); void SubmitFlipPreparation(uint64_t request_id); - void WaitForSubmitSlot(); + void WaitForSubmitSlot(int handle); void WaitFlipDone(int handle, int index); [[nodiscard]] Impl& State() noexcept; @@ -52,6 +52,9 @@ void VideoOutShutdown(); KYTY_SYSV_ABI int VideoOutOpen(int user_id, int bus_type, int index, const void* param); KYTY_SYSV_ABI int VideoOutClose(int handle); +KYTY_SYSV_ABI int VideoOutSetFlipMaster(int slave_handle, int master_handle); +KYTY_SYSV_ABI void VideoOutAddBufferAttributeOption(VideoOutBufferAttribute2* attribute, + uint64_t option); KYTY_SYSV_ABI void VideoOutSetBufferAttribute2(VideoOutBufferAttribute2* attribute, uint64_t pixel_format, uint32_t tiling_mode, uint32_t width, uint32_t height, uint64_t option, diff --git a/src/graphics/presentation/window/swapchain.cpp b/src/graphics/presentation/window/swapchain.cpp index dff7470c..7bce6b34 100644 --- a/src/graphics/presentation/window/swapchain.cpp +++ b/src/graphics/presentation/window/swapchain.cpp @@ -4,6 +4,8 @@ #include "common/logging/log.h" #include "common/profiler.h" #include "common/threads.h" +#include "gpu_blit_shaders/gpu_blit_fs_triangle_spv.h" +#include "gpu_blit_shaders/gpu_video_out_overlay_spv.h" #include "graphics/host_gpu/graphicContext.h" #include "graphics/host_gpu/renderer/render.h" #include "graphics/host_gpu/renderer/renderContext.h" @@ -14,7 +16,7 @@ #include "graphics/presentation/window/windowInternal.h" #include -#include +#include #include #include #include @@ -25,10 +27,10 @@ namespace Libs::Graphics { struct Presenter::Frame { - VulkanImage image; - uint64_t present_tick = 0; - bool busy = false; - bool reusing_last = false; + VulkanImage image; + vk::ImageView view = nullptr; + uint64_t present_tick = 0; + bool busy = false; void Configure(GraphicContext& graphics, vk::Extent2D extent, vk::Format format); void Transit(vk::CommandBuffer command, vk::ImageLayout layout, vk::AccessFlags2 access); @@ -43,6 +45,7 @@ public: ~FramePool() { m_scheduler.Wait(m_scheduler.CurrentTick() - 1); for (auto& frame: m_frames) { + m_window.graphic_ctx.device.destroyImageView(frame->view, nullptr); if (frame->image.image != nullptr) { m_window.graphic_ctx.DeleteImage(frame->image); } @@ -60,6 +63,7 @@ public: } m_format = format; m_frames.reserve(count); + m_free.reserve(count); for (uint32_t i = 0; i < count; i++) { auto frame = std::make_unique(); m_free.push_back(frame.get()); @@ -83,60 +87,45 @@ public: return m_format; } - Presenter::Frame* Acquire() { + Presenter::Frame* Acquire(vk::Extent2D extent, vk::Format format) { m_mutex.Lock(); if (m_frames.empty()) { EXIT("prepared-frame pool was used before swapchain initialization\n"); } - while (m_free.empty()) { - m_available.Wait(&m_mutex); + // A synchronized flip may need more frames than the swapchain has images. + // Queue capacity bounds growth; waiting here can prevent its master from recording. + if (m_free.empty()) { + auto frame = std::make_unique(); + m_free.push_back(frame.get()); + m_frames.push_back(std::move(frame)); } - auto* frame = m_free.front(); - m_free.pop_front(); + const auto compatible = std::ranges::find_if(m_free, [&](const auto* frame) { + return frame->image.extent.width == extent.width && + frame->image.extent.height == extent.height && frame->image.format == format; + }); + auto* frame = compatible == m_free.end() ? m_free.back() : *compatible; + if (compatible != m_free.end()) { + *compatible = m_free.back(); + } + m_free.pop_back(); if (frame->busy) { EXIT("prepared-frame pool returned an invalid frame\n"); } - if (m_last_frame == frame) { - m_last_frame = nullptr; - } - frame->busy = true; - frame->reusing_last = false; + frame->busy = true; m_mutex.Unlock(); - WaitForFrame(*frame); + m_scheduler.Wait(frame->present_tick); return frame; } - Presenter::Frame* AcquireLast() { - m_mutex.Lock(); - auto* frame = m_last_frame; - if (frame == nullptr) { - m_mutex.Unlock(); - return nullptr; - } - auto free = std::find(m_free.begin(), m_free.end(), frame); - if (free == m_free.end() || frame->busy) { - m_mutex.Unlock(); - EXIT("last submitted frame is not available for reuse\n"); - } - m_free.erase(free); - m_last_frame = nullptr; - frame->busy = true; - frame->reusing_last = true; - m_mutex.Unlock(); - - WaitForFrame(*frame); - return frame; - } - - void ValidateForPresent(Presenter::Frame* frame, bool reuse) { + void ValidateForPresent(Presenter::Frame* frame) { Common::LockGuard lock(m_mutex); - if (frame == nullptr || !frame->busy || frame->reusing_last != reuse) { + if (frame == nullptr || !frame->busy) { EXIT("prepared frame has invalid presentation ownership\n"); } } - void Release(Presenter::Frame* frame, bool make_last = false) { + void Release(Presenter::Frame* frame) { if (frame == nullptr) { EXIT("cannot release a null prepared frame\n"); } @@ -144,26 +133,17 @@ public: if (!frame->busy) { EXIT("prepared frame was released twice\n"); } - frame->busy = false; - frame->reusing_last = false; - if (make_last) { - m_last_frame = frame; - } + frame->busy = false; m_free.push_back(frame); - m_available.Signal(); } private: - void WaitForFrame(Presenter::Frame& frame) { m_scheduler.Wait(frame.present_tick); } - WindowContext& m_window; CommandScheduler& m_scheduler; Common::Mutex m_mutex; - Common::CondVar m_available; std::vector> m_frames; - std::deque m_free; - Presenter::Frame* m_last_frame = nullptr; - vk::Format m_format = vk::Format::eUndefined; + std::vector m_free; + vk::Format m_format = vk::Format::eUndefined; }; void Presenter::Frame::Configure(GraphicContext& graphics, vk::Extent2D extent, vk::Format format) { @@ -181,13 +161,16 @@ void Presenter::Frame::Configure(GraphicContext& graphics, vk::Extent2D extent, const auto features = graphics.GetFormatProperties(format).optimalTilingFeatures; const auto required = vk::FormatFeatureFlagBits::eBlitSrc | vk::FormatFeatureFlagBits::eSampledImageFilterLinear | - vk::FormatFeatureFlagBits::eTransferSrc | vk::FormatFeatureFlagBits::eTransferDst; + vk::FormatFeatureFlagBits::eSampledImage | vk::FormatFeatureFlagBits::eTransferSrc | + vk::FormatFeatureFlagBits::eTransferDst; if ((features & required) != required) { EXIT("prepared presentation format lacks optimal blit support: format=%d features=0x%x\n", static_cast(format), static_cast(features)); } if (dst.image != nullptr) { + graphics.device.destroyImageView(view, nullptr); + view = nullptr; graphics.DeleteImage(dst); } @@ -200,7 +183,8 @@ void Presenter::Frame::Configure(GraphicContext& graphics, vk::Extent2D extent, create.format = format; create.tiling = vk::ImageTiling::eOptimal; create.initialLayout = vk::ImageLayout::eUndefined; - create.usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst; + create.usage = vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst | + vk::ImageUsageFlagBits::eSampled; create.sharingMode = vk::SharingMode::eExclusive; create.samples = vk::SampleCountFlagBits::e1; if (!graphics.CreateImage(create, dst)) { @@ -258,7 +242,6 @@ void Presenter::Frame::CopyFrom(CommandBuffer& command_buffer, Image& source) { EXIT_IF(copy.srcSubresource.layerCount != copy.dstSubresource.layerCount); command.copyImage(source.backing.image, vk::ImageLayout::eTransferSrcOptimal, image.image, vk::ImageLayout::eTransferDstOptimal, copy); - Transit(command, vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead); } void Presenter::Frame::Clear(CommandBuffer& command_buffer, const vk::ClearColorValue& color) { @@ -267,7 +250,6 @@ void Presenter::Frame::Clear(CommandBuffer& command_buffer, const vk::ClearColor Transit(command, vk::ImageLayout::eTransferDstOptimal, vk::AccessFlagBits2::eTransferWrite); const vk::ImageSubresourceRange range {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}; command.clearColorImage(image.image, vk::ImageLayout::eTransferDstOptimal, &color, 1, &range); - Transit(command, vk::ImageLayout::eTransferSrcOptimal, vk::AccessFlagBits2::eTransferRead); } class Swapchain final { @@ -283,9 +265,9 @@ public: [[nodiscard]] bool NeedsResize() const; [[nodiscard]] Status AcquireNextImage(); [[nodiscard]] bool PrepareSystemOverlay(); - void RecordPresentCommands(CommandBuffer& command, VulkanImage& source, - bool draw_system_overlay); - uint64_t Submit(CommandScheduler& scheduler); + void RecordPresentCommands(CommandBuffer& command, Presenter::Frame* source, + const Presenter::Layer& overlay, bool draw_system_overlay); + uint64_t Submit(CommandScheduler& scheduler); [[nodiscard]] Status Present(); [[nodiscard]] uint32_t ImageCount() const noexcept { @@ -295,20 +277,25 @@ public: private: 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; - vk::Extent2D m_extent {}; + WindowContext& m_window; + vk::SwapchainKHR m_handle = nullptr; + vk::Format m_format = vk::Format::eUndefined; + vk::Extent2D m_extent {}; // Drawable pixel size observed when this swapchain was created. - vk::Extent2D m_window_extent {}; - std::vector m_images; - std::vector m_image_views; - std::vector m_image_acquired; - std::vector m_render_complete; + vk::Extent2D m_window_extent {}; + std::vector m_images; + std::vector m_image_views; + std::vector m_image_acquired; + std::vector m_render_complete; std::unique_ptr m_system_overlay; - uint32_t m_image_index = static_cast(-1); - uint32_t m_frame_index = 0; + vk::DescriptorSetLayout m_overlay_descriptors = nullptr; + vk::PipelineLayout m_overlay_layout = nullptr; + vk::Pipeline m_overlay_pipeline = nullptr; + vk::Sampler m_overlay_sampler = nullptr; + uint32_t m_image_index = static_cast(-1); + uint32_t m_frame_index = 0; }; struct Presenter::Impl { @@ -347,12 +334,15 @@ struct Presenter::Impl { cache.UpdateImage(image_id); return image; } + void Present(); RenderContext& renderer; WindowContext& window; Swapchain swapchain; CommandScheduler present_scheduler; FramePool frames; + Common::Mutex present_mutex; + std::array layers {}; std::atomic presented_overlay_revision {0}; }; @@ -370,7 +360,7 @@ void Swapchain::Create() { EXIT_IF(m_window_extent.width == 0); EXIT_IF(m_window_extent.height == 0); m_window.RefreshSurfaceCapabilities(); - const auto& surface = m_window.surface_capabilities; + const auto& surface = m_window.surface_capabilities; EXIT_NOT_IMPLEMENTED(surface.formats.empty()); m_extent = surface.capabilities.currentExtent; @@ -442,7 +432,7 @@ void Swapchain::Create() { LOGF("warning: requested present mode is unavailable; falling back to Fifo\n"); create_info.presentMode = vk::PresentModeKHR::eFifo; } - create_info.clipped = VK_TRUE; + create_info.clipped = VK_TRUE; RequireVulkanSuccess(graphics.device.createSwapchainKHR(&create_info, nullptr, &m_handle), "vkCreateSwapchainKHR"); EXIT_IF(m_handle == nullptr); @@ -483,8 +473,8 @@ void Swapchain::Create() { graphics.device.createSemaphore(&semaphore_info, nullptr, &m_render_complete[i]), "create swapchain render-complete semaphore"); } - m_image_index = static_cast(-1); - m_frame_index = 0; + m_image_index = static_cast(-1); + m_frame_index = 0; } Swapchain::~Swapchain() { @@ -505,6 +495,14 @@ void Swapchain::Destroy() { if (m_system_overlay != nullptr) { m_system_overlay->ReleaseVulkan(); } + graphics.device.destroyPipeline(m_overlay_pipeline, nullptr); + graphics.device.destroyPipelineLayout(m_overlay_layout, nullptr); + graphics.device.destroyDescriptorSetLayout(m_overlay_descriptors, nullptr); + graphics.device.destroySampler(m_overlay_sampler, nullptr); + m_overlay_pipeline = nullptr; + m_overlay_layout = nullptr; + m_overlay_descriptors = nullptr; + m_overlay_sampler = nullptr; for (const auto semaphore: m_image_acquired) { if (semaphore != nullptr) { @@ -544,8 +542,8 @@ void Swapchain::Recreate(bool surface_lost) { // Surface recreation goes through SDL_Vulkan_CreateSurface, which touches the // window's view/layer and must run on the main thread on macOS. EXIT_IF(!SDL_RunOnMainThread( - [](void* window) { static_cast(window)->RecreateSurface(); }, - &m_window, true)); + [](void* window) { static_cast(window)->RecreateSurface(); }, &m_window, + true)); #else m_window.RecreateSurface(); #endif @@ -592,14 +590,147 @@ bool Swapchain::PrepareSystemOverlay() { return m_system_overlay->PrepareFrame(m_extent, m_format, ImageCount()); } -void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& source, - bool draw_system_overlay) { - if (source.state.layout != vk::ImageLayout::eTransferSrcOptimal) { - EXIT("invalid prepared presentation image, vk_image=%p layout=%d\n", - static_cast(source.image), static_cast(source.state.layout)); +void Swapchain::DrawOverlay(vk::CommandBuffer command, const Presenter::Layer& layer) { + auto device = m_window.graphic_ctx.device; + if (m_overlay_pipeline == nullptr) { + vk::SamplerCreateInfo sampler {}; + sampler.magFilter = vk::Filter::eLinear; + sampler.minFilter = vk::Filter::eLinear; + sampler.addressModeU = vk::SamplerAddressMode::eClampToEdge; + sampler.addressModeV = vk::SamplerAddressMode::eClampToEdge; + sampler.addressModeW = vk::SamplerAddressMode::eClampToEdge; + RequireVulkanSuccess(device.createSampler(&sampler, nullptr, &m_overlay_sampler), + "create video-out overlay sampler"); + const vk::DescriptorSetLayoutBinding binding {0, vk::DescriptorType::eCombinedImageSampler, + 1, vk::ShaderStageFlagBits::eFragment}; + vk::DescriptorSetLayoutCreateInfo descriptors {}; + descriptors.flags = vk::DescriptorSetLayoutCreateFlagBits::ePushDescriptorKHR; + descriptors.bindingCount = 1; + descriptors.pBindings = &binding; + RequireVulkanSuccess( + device.createDescriptorSetLayout(&descriptors, nullptr, &m_overlay_descriptors), + "create video-out overlay descriptor layout"); + const vk::PushConstantRange alpha {vk::ShaderStageFlagBits::eFragment, 0, sizeof(uint32_t)}; + vk::PipelineLayoutCreateInfo layout {}; + layout.setLayoutCount = 1; + layout.pSetLayouts = &m_overlay_descriptors; + layout.pushConstantRangeCount = 1; + layout.pPushConstantRanges = α + RequireVulkanSuccess(device.createPipelineLayout(&layout, nullptr, &m_overlay_layout), + "create video-out overlay pipeline layout"); + const auto vertex = CompileSPV(GPU_BLIT_FS_TRIANGLE_SPV, device); + const auto fragment = CompileSPV(GPU_VIDEO_OUT_OVERLAY_SPV, device); + std::array stages {}; + stages[0].stage = vk::ShaderStageFlagBits::eVertex; + stages[0].module = vertex; + stages[0].pName = "main"; + stages[1].stage = vk::ShaderStageFlagBits::eFragment; + stages[1].module = fragment; + stages[1].pName = "main"; + vk::PipelineVertexInputStateCreateInfo vertex_input {}; + vk::PipelineInputAssemblyStateCreateInfo assembly {}; + assembly.topology = vk::PrimitiveTopology::eTriangleList; + vk::PipelineViewportStateCreateInfo viewport {}; + viewport.viewportCount = 1; + viewport.scissorCount = 1; + vk::PipelineRasterizationStateCreateInfo rasterization {}; + rasterization.lineWidth = 1.0f; + vk::PipelineMultisampleStateCreateInfo multisample {}; + multisample.rasterizationSamples = vk::SampleCountFlagBits::e1; + vk::PipelineColorBlendAttachmentState attachment {}; + attachment.blendEnable = VK_TRUE; + attachment.srcColorBlendFactor = vk::BlendFactor::eOne; + attachment.dstColorBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; + attachment.srcAlphaBlendFactor = vk::BlendFactor::eOne; + attachment.dstAlphaBlendFactor = vk::BlendFactor::eOneMinusSrcAlpha; + attachment.colorWriteMask = vk::ColorComponentFlagBits::eR | + vk::ColorComponentFlagBits::eG | + vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA; + vk::PipelineColorBlendStateCreateInfo blend {}; + blend.attachmentCount = 1; + blend.pAttachments = &attachment; + const std::array dynamic_states {vk::DynamicState::eViewport, vk::DynamicState::eScissor}; + vk::PipelineDynamicStateCreateInfo dynamic {}; + dynamic.dynamicStateCount = static_cast(dynamic_states.size()); + dynamic.pDynamicStates = dynamic_states.data(); + vk::PipelineRenderingCreateInfo rendering {}; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachmentFormats = &m_format; + vk::GraphicsPipelineCreateInfo create {}; + create.pNext = &rendering; + create.stageCount = static_cast(stages.size()); + create.pStages = stages.data(); + create.pVertexInputState = &vertex_input; + create.pInputAssemblyState = &assembly; + create.pViewportState = &viewport; + create.pRasterizationState = &rasterization; + create.pMultisampleState = &multisample; + create.pColorBlendState = &blend; + create.pDynamicState = &dynamic; + create.layout = m_overlay_layout; + RequireVulkanSuccess( + device.createGraphicsPipelines(nullptr, 1, &create, nullptr, &m_overlay_pipeline), + "create video-out overlay pipeline"); + device.destroyShaderModule(fragment, nullptr); + device.destroyShaderModule(vertex, nullptr); } + auto& frame = *layer.frame; + if (frame.view == nullptr) { + vk::ImageViewCreateInfo view {}; + view.image = frame.image.image; + view.viewType = vk::ImageViewType::e2D; + view.format = frame.image.format; + view.subresourceRange = {vk::ImageAspectFlagBits::eColor, 0, 1, 0, 1}; + RequireVulkanSuccess(device.createImageView(&view, nullptr, &frame.view), + "create video-out overlay image view"); + } + const vk::DescriptorImageInfo image {m_overlay_sampler, frame.view, + vk::ImageLayout::eShaderReadOnlyOptimal}; + vk::WriteDescriptorSet write {}; + write.dstBinding = 0; + write.descriptorCount = 1; + write.descriptorType = vk::DescriptorType::eCombinedImageSampler; + write.pImageInfo = ℑ + command.pushDescriptorSetKHR(vk::PipelineBindPoint::eGraphics, m_overlay_layout, 0, 1, &write); + const uint32_t premultiplied = layer.premultiplied_alpha; + command.pushConstants(m_overlay_layout, vk::ShaderStageFlagBits::eFragment, 0, + sizeof(premultiplied), &premultiplied); + command.bindPipeline(vk::PipelineBindPoint::eGraphics, m_overlay_pipeline); + vk::RenderingAttachmentInfo attachment {}; + attachment.imageView = m_image_views[m_image_index]; + attachment.imageLayout = vk::ImageLayout::eColorAttachmentOptimal; + attachment.loadOp = vk::AttachmentLoadOp::eLoad; + attachment.storeOp = vk::AttachmentStoreOp::eStore; + vk::RenderingInfo rendering {}; + rendering.renderArea.extent = m_extent; + rendering.layerCount = 1; + rendering.colorAttachmentCount = 1; + rendering.pColorAttachments = &attachment; + command.beginRendering(&rendering); + const vk::Viewport viewport { + 0.0f, 0.0f, static_cast(m_extent.width), static_cast(m_extent.height), + 0.0f, 1.0f}; + const vk::Rect2D scissor {{0, 0}, m_extent}; + command.setViewport(0, 1, &viewport); + command.setScissor(0, 1, &scissor); + command.draw(3, 1, 0, 0); + command.endRendering(); +} + +void Swapchain::RecordPresentCommands(CommandBuffer& command, Presenter::Frame* source, + const Presenter::Layer& overlay, bool draw_system_overlay) { EXIT_IF(m_image_index >= m_images.size()); - auto vk_command = command.Handle(); + auto vk_command = command.Handle(); + const bool draw_overlay = overlay.frame != nullptr; + const bool draw_attachment = draw_overlay || draw_system_overlay; + if (source != nullptr) { + source->Transit(vk_command, vk::ImageLayout::eTransferSrcOptimal, + vk::AccessFlagBits2::eTransferRead); + } + if (draw_overlay) { + overlay.frame->Transit(vk_command, vk::ImageLayout::eShaderReadOnlyOptimal, + vk::AccessFlagBits2::eShaderRead); + } vk::ImageMemoryBarrier to_transfer {}; to_transfer.sType = vk::StructureType::eImageMemoryBarrier; @@ -619,49 +750,68 @@ void Swapchain::RecordPresentCommands(CommandBuffer& command, VulkanImage& sourc vk::PipelineStageFlagBits::eTransfer, vk::DependencyFlags {}, 0, nullptr, 0, nullptr, 1, &to_transfer); - vk::ImageBlit region {}; - region.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; - region.srcSubresource.mipLevel = 0; - region.srcSubresource.baseArrayLayer = 0; - region.srcSubresource.layerCount = 1; - region.srcOffsets[1].x = static_cast(source.extent.width); - region.srcOffsets[1].y = static_cast(source.extent.height); - region.srcOffsets[1].z = 1; - region.dstSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; - region.dstSubresource.mipLevel = 0; - region.dstSubresource.baseArrayLayer = 0; - region.dstSubresource.layerCount = 1; - region.dstOffsets[1].x = static_cast(m_extent.width); - region.dstOffsets[1].y = static_cast(m_extent.height); - region.dstOffsets[1].z = 1; - vk_command.blitImage(source.image, vk::ImageLayout::eTransferSrcOptimal, - m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal, 1, ®ion, - vk::Filter::eLinear); + if (source != nullptr) { + vk::ImageBlit region {}; + region.srcSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; + region.srcSubresource.mipLevel = 0; + region.srcSubresource.baseArrayLayer = 0; + region.srcSubresource.layerCount = 1; + region.srcOffsets[1].x = static_cast(source->image.extent.width); + region.srcOffsets[1].y = static_cast(source->image.extent.height); + region.srcOffsets[1].z = 1; + region.dstSubresource.aspectMask = vk::ImageAspectFlagBits::eColor; + region.dstSubresource.mipLevel = 0; + region.dstSubresource.baseArrayLayer = 0; + region.dstSubresource.layerCount = 1; + region.dstOffsets[1].x = static_cast(m_extent.width); + region.dstOffsets[1].y = static_cast(m_extent.height); + region.dstOffsets[1].z = 1; + vk_command.blitImage(source->image.image, vk::ImageLayout::eTransferSrcOptimal, + m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal, 1, + ®ion, vk::Filter::eLinear); + } else { + const vk::ClearColorValue black {}; + vk_command.clearColorImage(m_images[m_image_index], vk::ImageLayout::eTransferDstOptimal, + &black, 1, &to_transfer.subresourceRange); + } vk::ImageMemoryBarrier to_present {}; - to_present.sType = vk::StructureType::eImageMemoryBarrier; - to_present.srcAccessMask = vk::AccessFlagBits::eTransferWrite; - to_present.dstAccessMask = draw_system_overlay ? vk::AccessFlagBits::eColorAttachmentRead | - vk::AccessFlagBits::eColorAttachmentWrite - : vk::AccessFlagBits::eMemoryRead; - to_present.oldLayout = vk::ImageLayout::eTransferDstOptimal; - to_present.newLayout = draw_system_overlay ? vk::ImageLayout::eColorAttachmentOptimal - : vk::ImageLayout::ePresentSrcKHR; - to_present.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - to_present.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; - to_present.image = m_images[m_image_index]; + to_present.sType = vk::StructureType::eImageMemoryBarrier; + to_present.srcAccessMask = vk::AccessFlagBits::eTransferWrite; + to_present.dstAccessMask = draw_attachment ? vk::AccessFlagBits::eColorAttachmentRead | + vk::AccessFlagBits::eColorAttachmentWrite + : vk::AccessFlagBits::eMemoryRead; + to_present.oldLayout = vk::ImageLayout::eTransferDstOptimal; + to_present.newLayout = draw_attachment ? vk::ImageLayout::eColorAttachmentOptimal + : vk::ImageLayout::ePresentSrcKHR; + to_present.srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + to_present.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + to_present.image = m_images[m_image_index]; to_present.subresourceRange.aspectMask = vk::ImageAspectFlagBits::eColor; to_present.subresourceRange.baseMipLevel = 0; to_present.subresourceRange.levelCount = 1; to_present.subresourceRange.baseArrayLayer = 0; to_present.subresourceRange.layerCount = 1; - vk_command.pipelineBarrier( - vk::PipelineStageFlagBits::eTransfer, - draw_system_overlay ? vk::PipelineStageFlagBits::eColorAttachmentOutput - : vk::PipelineStageFlagBits::eAllCommands, - vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1, &to_present); - if (draw_system_overlay) { - m_system_overlay->Record(vk_command, m_image_views[m_image_index]); + vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eTransfer, + draw_attachment ? vk::PipelineStageFlagBits::eColorAttachmentOutput + : vk::PipelineStageFlagBits::eAllCommands, + vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, nullptr, 1, + &to_present); + if (draw_attachment) { + if (draw_overlay) { + DrawOverlay(vk_command, overlay); + if (draw_system_overlay) { + to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; + to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal; + vk_command.pipelineBarrier(vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::PipelineStageFlagBits::eColorAttachmentOutput, + vk::DependencyFlagBits::eByRegion, 0, nullptr, 0, + nullptr, 1, &to_present); + } + } + if (draw_system_overlay) { + m_system_overlay->Record(vk_command, m_image_views[m_image_index]); + } to_present.srcAccessMask = vk::AccessFlagBits::eColorAttachmentWrite; to_present.dstAccessMask = vk::AccessFlagBits::eMemoryRead; to_present.oldLayout = vk::ImageLayout::eColorAttachmentOptimal; @@ -721,19 +871,18 @@ Presenter::~Presenter() = default; Presenter::Frame& Presenter::PrepareFrame(CommandBuffer& buffer, const ImageInfo& info) { KYTY_PROFILER_FUNCTION(); EXIT_IF(buffer.IsInvalid()); - auto* frame = m_impl->frames.Acquire(); - Common::LockGuard render_lock(m_impl->renderer.GetMutex()); - auto& image = m_impl->ResolveSurface(info); - if (image.backing.format == vk::Format::eUndefined) { - EXIT("unsupported presentation source, image=%p\n", static_cast(&image)); - } - auto frame_format = info.pixel_format; switch (frame_format) { case vk::Format::eR8G8B8A8Srgb: frame_format = vk::Format::eR8G8B8A8Unorm; break; case vk::Format::eB8G8R8A8Srgb: frame_format = vk::Format::eB8G8R8A8Unorm; break; default: break; } + auto* frame = m_impl->frames.Acquire({info.extent.width, info.extent.height}, frame_format); + Common::LockGuard render_lock(m_impl->renderer.GetMutex()); + auto& image = m_impl->ResolveSurface(info); + if (image.backing.format == vk::Format::eUndefined) { + EXIT("unsupported presentation source, image=%p\n", static_cast(&image)); + } frame->Configure(m_impl->window.graphic_ctx, {image.backing.extent.width, image.backing.extent.height}, frame_format); frame->CopyFrom(buffer, image); @@ -744,7 +893,7 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height, CommandBuffer* producer) { KYTY_PROFILER_FUNCTION(); auto format = m_impl->frames.GetFormat(); - auto* frame = m_impl->frames.Acquire(); + auto* frame = m_impl->frames.Acquire({width, height}, format); Common::LockGuard render_lock(m_impl->renderer.GetMutex()); frame->Configure(m_impl->window.graphic_ctx, {width, height}, format); vk::ClearColorValue clear {}; @@ -760,8 +909,13 @@ Presenter::Frame& Presenter::PrepareBlankFrame(uint32_t width, uint32_t height, return *frame; } -Presenter::Frame* Presenter::PrepareLastFrame() { - return m_impl->frames.AcquireLast(); +bool Presenter::PresentLastFrame() { + Common::LockGuard lock(m_impl->present_mutex); + if (m_impl->layers[0].frame == nullptr && m_impl->layers[1].frame == nullptr) { + return false; + } + m_impl->Present(); + return true; } bool Presenter::IsGuestPaused() const noexcept { @@ -778,44 +932,76 @@ RenderContext& Presenter::Renderer() const noexcept { return m_impl->renderer; } -void Presenter::Present(Frame& frame, bool reuse) { +void Presenter::Present(Frame& frame) { + const Layer layer {&frame, 0, false}; + Present(std::span(&layer, 1)); +} + +void Presenter::Present(std::span layers) { + Common::LockGuard lock(m_impl->present_mutex); + for (const auto& layer: layers) { + EXIT_IF(layer.bus < 0 || layer.bus >= static_cast(m_impl->layers.size())); + m_impl->frames.ValidateForPresent(layer.frame); + auto& previous = m_impl->layers[layer.bus]; + if (previous.frame != nullptr) { + m_impl->frames.Release(previous.frame); + } + previous = layer; + } + m_impl->Present(); +} + +void Presenter::ClearLayer(int bus) { + if (static_cast(bus) >= m_impl->layers.size()) { + return; + } + Common::LockGuard lock(m_impl->present_mutex); + auto& layer = m_impl->layers[bus]; + if (layer.frame != nullptr) { + m_impl->frames.Release(layer.frame); + layer = {}; + } +} + +void Presenter::Impl::Present() { KYTY_PROFILER_FUNCTION(); - m_impl->frames.ValidateForPresent(&frame, reuse); const auto overlay_visual = GetSystemOverlayVisualState(); - auto& swapchain = m_impl->swapchain; // Some window systems keep presenting an old swapchain after a resize. if (swapchain.NeedsResize()) { - m_impl->RecoverSwapchain(Swapchain::Status::Recreate); + RecoverSwapchain(Swapchain::Status::Recreate); } for (uint32_t attempt = 0; attempt < 2; attempt++) { auto status = swapchain.AcquireNextImage(); if (status != Swapchain::Status::Success) { - m_impl->RecoverSwapchain(status); + RecoverSwapchain(status); continue; } { - Common::LockGuard render_lock(m_impl->renderer.GetMutex()); - auto& command = m_impl->present_scheduler.BeginCommand(); + Common::LockGuard render_lock(renderer.GetMutex()); + auto& command = present_scheduler.BeginCommand(); const bool draw_system_overlay = overlay_visual.active && swapchain.PrepareSystemOverlay(); - swapchain.RecordPresentCommands(command, frame.image, draw_system_overlay); - frame.present_tick = swapchain.Submit(m_impl->present_scheduler); + swapchain.RecordPresentCommands(command, layers[0].frame, layers[1], + draw_system_overlay); + const auto tick = swapchain.Submit(present_scheduler); + for (const auto& layer: layers) { + if (layer.frame != nullptr) { + layer.frame->present_tick = tick; + } + } } status = swapchain.Present(); if (status != Swapchain::Status::Success) { - m_impl->RecoverSwapchain(status); + RecoverSwapchain(status); continue; } - m_impl->presented_overlay_revision.store(overlay_visual.revision, - std::memory_order_release); - m_impl->window.UpdateTitle(); - m_impl->frames.Release(&frame, true); + presented_overlay_revision.store(overlay_visual.revision, std::memory_order_release); + window.UpdateTitle(); return; } LOGF("Vulkan presentation retry exhausted; dropping frame\n"); - m_impl->frames.Release(&frame, reuse); } void Presenter::Discard(Frame& frame) { diff --git a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp index 95e11db9..e7b9b61d 100644 --- a/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp +++ b/src/graphics/shader/recompiler/backend/spirv/SpirvEmitter.cpp @@ -3,6 +3,7 @@ #include "common/assert.h" #include "graphics/shader/recompiler/backend/spirv/spirvEmitterInternal.h" #include "graphics/shader/recompiler/ir/ShaderIR.h" +#include "graphics/shader/recompiler/ir/passes/BindingLayout.h" #include #include @@ -34,9 +35,6 @@ void ValidateNativeProgram(const IR::Program& program) { present[index] = true; expected[index] = std::move(resources); }; - if (!program.info.buffers.empty()) { - Expect(Kind::Buffers, Dense(program.info.buffers.size())); - } for (uint32_t i = 0; i < program.info.images.size(); i++) { const auto kind = IR::DescriptorBindingForImage(program.info.images[i]); if (!kind.has_value()) { @@ -54,23 +52,9 @@ void ValidateNativeProgram(const IR::Program& program) { if (!program.info.samplers.empty()) { Expect(Kind::Samplers, Dense(program.info.samplers.size())); } - bool uses_gds = false; - for (const auto* block: program.blocks) { - for (const auto& inst: *block) { - if (IR::SharedAccessOf(inst.GetOpcode()) == IR::SharedAccess::None) { - continue; - } - const auto index = inst.Flags().index; - if (index >= program.memory_info.size()) { - Fail(program, "shared operation has invalid memory metadata"); - } - const auto kind = program.memory_info[index].kind; - if (kind != IR::ResourceKind::Lds && kind != IR::ResourceKind::Gds) { - Fail(program, "shared operation has invalid resource kind"); - } - uses_gds |= kind == IR::ResourceKind::Gds; - } - } + auto& buffers = expected[static_cast(Kind::Buffers)]; + const bool uses_gds = IR::CollectMemoryResources(program, buffers); + present[static_cast(Kind::Buffers)] = !buffers.empty(); if (uses_gds) { Expect(Kind::Gds); } @@ -109,7 +93,7 @@ void ValidateNativeProgram(const IR::Program& program) { if ((program.bindings.UsesPushData() && !IR::PushData::CanFit(program.bindings.push_data_start_dword, shader_data_dwords)) || program.bindings.memory_offset_dword != program.bindings.user_data_registers.size() || - program.bindings.memory_offset_count != program.info.buffers.size() || + program.bindings.memory_offset_count != buffers.size() || has_shader_data_storage != (shader_data_dwords != 0 && !program.bindings.UsesPushData()) || !std::is_sorted(program.bindings.user_data_registers.begin(), program.bindings.user_data_registers.end()) || diff --git a/src/graphics/shader/recompiler/frontend/decode/VectorAluOps.cpp b/src/graphics/shader/recompiler/frontend/decode/VectorAluOps.cpp index 2ff04b8e..2213f29d 100644 --- a/src/graphics/shader/recompiler/frontend/decode/VectorAluOps.cpp +++ b/src/graphics/shader/recompiler/frontend/decode/VectorAluOps.cpp @@ -537,6 +537,7 @@ constexpr Vop1SdwaRule VOP1_SDWA_RULES[] = { {Opcode::V_NOT_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelBytes() | SdwaSelWords(), SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), false}, {Opcode::V_FFBL_B32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false}, + {Opcode::V_FFBH_U32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), 0, 0, false}, {Opcode::V_CVT_F32_F16, SdwaSelWords() | SdwaSelFull(), 0, 0, true}, {Opcode::V_CVT_F16_F32, SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), SdwaSelWords(), SdwaSelBytes() | SdwaSelWords() | SdwaSelFull(), true}, @@ -1379,6 +1380,7 @@ bool SupportsNativeVop3ResultModifiers(Opcode opcode) { case Opcode::V_MAD_F32: case Opcode::V_FMA_F32: case Opcode::V_FMA_F16: + case Opcode::V_CUBEID_F32: case Opcode::V_CVT_PKRTZ_F16_F32: case Opcode::V_LDEXP_F32: case Opcode::V_MIN3_F32: diff --git a/src/graphics/shader/recompiler/ir/passes/BindingLayout.cpp b/src/graphics/shader/recompiler/ir/passes/BindingLayout.cpp index 0c341f70..02259156 100644 --- a/src/graphics/shader/recompiler/ir/passes/BindingLayout.cpp +++ b/src/graphics/shader/recompiler/ir/passes/BindingLayout.cpp @@ -46,29 +46,45 @@ void AddBinding(BindingLayout& layout, DescriptorBindingKind kind, layout.descriptors.push_back({kind, std::move(resources)}); } -bool UsesGds(const Program& program) { +} // namespace + +bool CollectMemoryResources(const Program& program, std::vector& buffers) { + std::array live_buffers {}; bool uses_gds = false; for (const auto* block: program.blocks) { for (const auto& inst: *block) { - if (SharedAccessOf(inst.GetOpcode()) == SharedAccess::None) { + const auto op = inst.GetOpcode(); + if (BufferAccessOf(op) == BufferAccess::None && + SharedAccessOf(op) == SharedAccess::None) { continue; } const auto index = inst.Flags().index; if (index >= program.memory_info.size()) { - BindingFail("typed shader contains invalid shared-memory metadata"); + BindingFail("typed shader contains invalid memory metadata"); } - const auto kind = program.memory_info[index].kind; - if (kind != ResourceKind::Lds && kind != ResourceKind::Gds) { - BindingFail("typed shader contains invalid shared-memory metadata"); + const auto& memory = program.memory_info[index]; + if (memory.planning_only) { + continue; } - uses_gds |= kind == ResourceKind::Gds; + if (SharedAccessOf(op) != SharedAccess::None) { + if (memory.kind != ResourceKind::Lds && memory.kind != ResourceKind::Gds) { + BindingFail("typed shader contains invalid shared-memory metadata"); + } + uses_gds |= memory.kind == ResourceKind::Gds; + } else if (memory.kind == ResourceKind::Buffer || memory.kind == ResourceKind::ScalarBuffer) { + EXIT_IF(memory.resource >= program.info.buffers.size()); + live_buffers.at(memory.resource) = true; + } + } + } + for (uint32_t i = 0; i < program.info.buffers.size(); i++) { + if (live_buffers.at(i)) { + buffers.push_back(i); } } return uses_gds; } -} // namespace - void AllocateBindings(Program& program, uint32_t push_data_start_dword) { if (!program.shader_info_complete || program.binding_layout_complete) { EXIT("shader binding layout failed: %s", !program.shader_info_complete @@ -76,18 +92,17 @@ void AllocateBindings(Program& program, uint32_t push_data_start_dword) { : "binding layout already allocated"); } BindingLayout next; + std::vector buffers; + const bool uses_gds = CollectMemoryResources(program, buffers); next.user_data_registers = CollectUserData(program); next.memory_offset_dword = static_cast(next.user_data_registers.size()); - next.memory_offset_count = static_cast(program.info.buffers.size()); + next.memory_offset_count = static_cast(buffers.size()); next.push_data_start_dword = PushData::StartFor(push_data_start_dword, next.ShaderDataDwords()); - if (!program.info.buffers.empty()) { - std::vector resources(program.info.buffers.size()); - for (uint32_t i = 0; i < resources.size(); i++) { - resources[i] = i; - } - AddBinding(next, DescriptorBindingKind::Buffers, std::move(resources)); + if (!buffers.empty()) { + // Draw binding accesses this first group directly when memory_offset_count is nonzero. + AddBinding(next, DescriptorBindingKind::Buffers, std::move(buffers)); } std::array, ImageBindingCount> image_groups; @@ -123,7 +138,7 @@ void AllocateBindings(Program& program, uint32_t push_data_start_dword) { } AddBinding(next, DescriptorBindingKind::Samplers, std::move(resources)); } - if (UsesGds(program)) { + if (uses_gds) { AddBinding(next, DescriptorBindingKind::Gds); } if (program.info.uses_dma) { diff --git a/src/graphics/shader/recompiler/ir/passes/BindingLayout.h b/src/graphics/shader/recompiler/ir/passes/BindingLayout.h index 9999c443..3ccdf33a 100644 --- a/src/graphics/shader/recompiler/ir/passes/BindingLayout.h +++ b/src/graphics/shader/recompiler/ir/passes/BindingLayout.h @@ -7,6 +7,9 @@ namespace Libs::Graphics::ShaderRecompiler::IR { void AllocateBindings(Program& program, uint32_t push_data_start_dword = 0); +// Collect live direct buffer indices and report whether the shader accesses GDS. +bool CollectMemoryResources(const Program& program, std::vector& buffers); + const DescriptorBinding* FindBinding(const BindingLayout& layout, DescriptorBindingKind kind); } // namespace Libs::Graphics::ShaderRecompiler::IR diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp index 1047c297..48510238 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.cpp @@ -2,6 +2,7 @@ #include "common/assert.h" #include "graphics/guest_gpu/gpu_format.h" +#include "graphics/shader/recompiler/BufferFormat.h" #include "graphics/shader/recompiler/ir/ShaderIR.h" #include "graphics/shader/shaderBindings.h" @@ -446,6 +447,7 @@ static bool BuildResourceSpecialization(const ResourcePlan& program, ResourceSna : Prospero::BufferFormat::kInvalid, .descriptor_swizzle = program.info.buffers[i].formatted ? descriptor.DstSelXYZW() : DstSel(4, 5, 6, 7), + .zero_stride_oob = descriptor.OutOfBounds() == 0u && stride == 0u, }); } for (uint32_t i = 0; i < specialization.images.size(); i++) { @@ -1127,6 +1129,49 @@ void ApplyResourceSpecialization(Program& program, const ResourceSpecialization& for (auto* block: program.blocks) { for (auto it = block->begin(); it != block->end(); ++it) { auto& inst = *it; + if (BufferAccessOf(inst.GetOpcode()) == BufferAccess::Read) { + const auto& memory = memory_info[inst.Flags().index]; + if (memory.kind == ResourceKind::Buffer && + specialization.buffers[memory.resource].zero_stride_oob) { + // Bounds mode 0 checks offset >= stride, so zero stride + // makes every vector read out of bounds regardless of its address. + const auto count = BufferComponentCount(inst.GetOpcode()); + std::array values {Value(0u), Value(0u), Value(0u), Value(0u)}; + if (memory.formatted && !memory.typed) { + const auto& buffer = buffers[memory.resource]; + const auto format = Format::GetFormatInfo(buffer.descriptor_format); + for (uint32_t component = 0; component < count; component++) { + if (format.type == Format::ComponentType::Unknown || + GetDstSel(buffer.descriptor_swizzle, component) != 1u) continue; + const auto one = Format::FormattedConstantBits( + format, Format::FormattedSourceKind::One); + values[component] = Value(&*block->PrependNewInst( + it, ValueOpcode::SelectU32, + {inst.Arg(inst.NumArgs() - 1), Value(one), Value(0u)})); + } + } + Value result = values[0]; + switch (inst.GetType()) { + case Type::U8: result = Value(uint8_t {0}); break; + case Type::U16: result = Value(uint16_t {0}); break; + case Type::U32x2: + result = Value(&*block->PrependNewInst(it, ValueOpcode::CompositeConstructU32x2, + {values[0], values[1]})); + break; + case Type::U32x3: + result = Value(&*block->PrependNewInst(it, ValueOpcode::CompositeConstructU32x3, + {values[0], values[1], values[2]})); + break; + case Type::U32x4: + result = Value(&*block->PrependNewInst(it, ValueOpcode::CompositeConstructU32x4, + {values[0], values[1], values[2], values[3]})); + break; + default: break; + } + inst.ReplaceUsesWith(result); + } + continue; + } const auto image_opcode = ImageOpcodeInfoOf(inst.GetOpcode()); if (image_opcode.access == ImageAccess::None) { continue; diff --git a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h index 812e633c..7f9e6125 100644 --- a/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h +++ b/src/graphics/shader/recompiler/ir/passes/ResourceMaterialization.h @@ -12,6 +12,7 @@ struct ResourceSpecialization { uint32_t packed_stride = 0; Prospero::BufferFormat descriptor_format = Prospero::BufferFormat::kInvalid; uint32_t descriptor_swizzle = DstSel(4, 5, 6, 7); + bool zero_stride_oob = false; bool operator==(const Buffer&) const = default; }; diff --git a/src/libs/libNet.cpp b/src/libs/libNet.cpp index 570ccf78..e2cd98a9 100644 --- a/src/libs/libNet.cpp +++ b/src/libs/libNet.cpp @@ -7,9 +7,11 @@ #include "libs/network.h" #include "loader/symbolDatabase.h" +#include #include #include #include +#include #include #include #include @@ -163,6 +165,16 @@ int KYTY_SYSV_ABI NetSetsockopt(int s, int level, int optname, const void* optva return FinishSocketCall(Net::Setsockopt(s, level, optname, optval, optlen)); } +int KYTY_SYSV_ABI NetSend(int s, const void* buf, size_t len, int flags) { + const auto size = std::min(len, std::numeric_limits::max()); + return FinishSocketCall(static_cast(Net::Send(s, buf, size, flags | 0x20000))); +} + +int KYTY_SYSV_ABI NetRecv(int s, void* buf, size_t len, int flags) { + const auto size = std::min(len, std::numeric_limits::max()); + return FinishSocketCall(static_cast(Net::Recv(s, buf, size, flags))); +} + uint32_t KYTY_SYSV_ABI NetHtonl(uint32_t host32) { return ((host32 & 0x000000ffu) << 24u) | ((host32 & 0x0000ff00u) << 8u) | ((host32 & 0x00ff0000u) >> 8u) | ((host32 & 0xff000000u) >> 24u); @@ -207,6 +219,8 @@ LIB_DEFINE(InitNet_1_Net) { LIB_FUNC("Q4qBuN-c0ZM", LibNet::NetSocket); LIB_FUNC("45ggEzakPJQ", LibNet::NetSocketClose); LIB_FUNC("2mKX2Spso7I", LibNet::NetSetsockopt); + LIB_FUNC("beRjXBn-z+o", LibNet::NetSend); + LIB_FUNC("9wO9XrMsNhc", LibNet::NetRecv); LIB_FUNC("9T2pDF2Ryqg", LibNet::NetHtonl); LIB_FUNC("iWQWrwiSt8A", LibNet::NetHtons); LIB_FUNC("pQGpHYopAIY", LibNet::NetNtohl); diff --git a/src/libs/libVideoOut.cpp b/src/libs/libVideoOut.cpp index 550aef38..3c18c9a1 100644 --- a/src/libs/libVideoOut.cpp +++ b/src/libs/libVideoOut.cpp @@ -15,6 +15,8 @@ LIB_DEFINE(InitVideoOut_1) { LIB_FUNC("Up36PTk687E", VideoOut::VideoOutOpen); LIB_FUNC("uquVH4-Du78", VideoOut::VideoOutClose); + LIB_FUNC("WkYtyOg30do", VideoOut::VideoOutSetFlipMaster); + LIB_FUNC("MBlv8YWoyOk", VideoOut::VideoOutAddBufferAttributeOption); LIB_FUNC("PjS5uASwcV8", VideoOut::VideoOutSetBufferAttribute2); LIB_FUNC("rKBUtgRrtbk", VideoOut::VideoOutRegisterBuffers2); LIB_FUNC("HuViW4HnrOw", VideoOut::VideoOutSubmitChangeBufferAttribute2); diff --git a/src/libs/network.cpp b/src/libs/network.cpp index beb675da..fae5a91b 100644 --- a/src/libs/network.cpp +++ b/src/libs/network.cpp @@ -1003,6 +1003,7 @@ static int ConvertHostSocketError(int error) { case ENOBUFS: posix_error = Posix::POSIX_ENOBUFS; break; case ENOMEM: posix_error = Posix::POSIX_ENOMEM; break; case ENOTSOCK: posix_error = Posix::POSIX_ENOTSOCK; break; + case EPIPE: posix_error = Posix::POSIX_EPIPE; break; case EPROTONOSUPPORT: posix_error = Posix::POSIX_EPROTONOSUPPORT; break; default: break; } diff --git a/tests/KernelFileSystemTests.cpp b/tests/KernelFileSystemTests.cpp index 7d12f869..4722a82d 100644 --- a/tests/KernelFileSystemTests.cpp +++ b/tests/KernelFileSystemTests.cpp @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -31,6 +32,10 @@ namespace Libs::LibKernelApr { void InitLibKernel_1_Apr(Loader::SymbolDatabase *symbols); } +namespace Libs::LibNet { +void InitNet_1_Net(Loader::SymbolDatabase *symbols); +} + namespace { namespace FileSystem = Libs::LibKernel::FileSystem; @@ -407,6 +412,22 @@ void CheckAprPaths(const std::filesystem::path &root) { void CheckSocketWakeup() { namespace Net = Libs::Network::Net; + Loader::SymbolDatabase symbols; + Libs::LibNet::InitNet_1_Net(&symbols); + const auto *send_symbol = symbols.Find( + {"beRjXBn-z+o", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func}); + const auto *recv_symbol = symbols.Find( + {"9wO9XrMsNhc", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func}); + const auto *errno_symbol = symbols.Find( + {"HQOwnfMGipQ", "Net", 1, "Net", 1, 1, Loader::SymbolType::Func}); + Check(send_symbol && recv_symbol && errno_symbol, + "Net send, receive and errno exports resolve with the guest ABI versions"); + using Send = int (KYTY_SYSV_ABI *)(int, const void *, size_t, int); + using Recv = int (KYTY_SYSV_ABI *)(int, void *, size_t, int); + using Errno = int *(KYTY_SYSV_ABI *)(); + const auto net_send = reinterpret_cast(send_symbol->vaddr); + const auto net_recv = reinterpret_cast(recv_symbol->vaddr); + auto *net_errno = reinterpret_cast(errno_symbol->vaddr)(); // Guest sockaddr_in: length, family, network-order port/address, padding. std::array address {16, 2, 0, 0, 127, 0, 0, 1}; const int listener = Net::Socket(2, 1, 0); @@ -441,26 +462,61 @@ void CheckSocketWakeup() { immediate.data()) == 0 && readable[reader / 64] == 0, "empty socket is not readable"); const char payload[] = "wake"; - Check(Net::Send(writer, payload, sizeof(payload), 0x20000) == sizeof(payload), - "send wake bytes with guest MSG_NOSIGNAL"); + *net_errno = Libs::Posix::POSIX_EINVAL; + Check(net_send(writer, payload, sizeof(payload), 0) == sizeof(payload) && + *net_errno == Libs::Posix::POSIX_EINVAL, + "Net send forwards bytes and preserves errno on success"); readable[reader / 64] = bit; const std::array deadline {1, 0}; Check(Net::Select(reader + 1, readable.data(), nullptr, nullptr, deadline.data()) == 1 && readable[reader / 64] == bit, "select reports the guest descriptor after wake"); std::array received {}; - Check(Net::Recv(reader, received.data(), received.size(), 0x42) == sizeof(payload) && + Check(net_recv(reader, received.data(), received.size(), 0x42) == sizeof(payload) && + std::memcmp(received.data(), payload, sizeof(payload)) == 0 && + *net_errno == Libs::Posix::POSIX_EINVAL, + "Net receive forwards PEEK and WAITALL without consuming bytes"); + received.fill(0); + Check(net_recv(reader, received.data(), received.size(), 0x40) == sizeof(payload) && std::memcmp(received.data(), payload, sizeof(payload)) == 0, - "guest PEEK and WAITALL preserve the wake bytes"); - Check(Net::Recv(reader, received.data(), received.size(), 0x40) == sizeof(payload), - "consume wake bytes with guest WAITALL"); + "Net receive consumes the same bytes after peeking"); #if !defined(_WIN32) Check(Net::Recv(reader, received.data(), received.size(), 0x80) == -1 && *Libs::Posix::GetErrorAddr() == Libs::Posix::POSIX_EWOULDBLOCK, "empty nonblocking receive translates guest errno"); + Check(net_recv(reader, received.data(), received.size(), 0x80) == + Libs::Network::NET_ERROR_EWOULDBLOCK && + *net_errno == Libs::Posix::POSIX_EWOULDBLOCK, + "Net nonblocking receive translates POSIX failure and Net errno"); #endif - Check(Net::SocketClose(reader) == 0 && Net::SocketClose(writer) == 0, - "close wake sockets"); + Check(net_send(-1, payload, sizeof(payload), 0) == Libs::Network::NET_ERROR_EBADF && + *net_errno == Libs::Posix::POSIX_EBADF, + "Net send translates an invalid socket instead of returning POSIX minus one"); + Check(net_recv(reader, nullptr, received.size(), 0) == Libs::Network::NET_ERROR_EFAULT && + *net_errno == Libs::Posix::POSIX_EFAULT, + "Net receive translates an invalid output buffer"); + Check(net_send(writer, payload, sizeof(payload), 0x100000) == + Libs::Network::NET_ERROR_EOPNOTSUPP && + *net_errno == Libs::Posix::POSIX_EOPNOTSUPP, + "Net send preserves the backend's unsupported crypto flag error"); +#if defined(__linux__) + const int disconnected = Net::Socket(2, 1, 0); + Check(disconnected >= 0, "create unconnected socket for broken pipe check"); + const auto previous_sigpipe = std::signal(SIGPIPE, SIG_DFL); + Check(previous_sigpipe != SIG_ERR, "set default SIGPIPE disposition for Net send"); + const auto broken_send = net_send(disconnected, payload, sizeof(payload), 0); + std::signal(SIGPIPE, previous_sigpipe); + Check(broken_send == Libs::Network::NET_ERROR_EPIPE && + *net_errno == Libs::Posix::POSIX_EPIPE, + "Net send reports a broken pipe without raising host SIGPIPE"); + Check(Net::SocketClose(disconnected) == 0, "close unconnected socket"); +#endif + Check(Net::SocketClose(writer) == 0, "close wake writer"); + *net_errno = Libs::Posix::POSIX_EINVAL; + Check(net_recv(reader, received.data(), received.size(), 0) == 0 && + *net_errno == Libs::Posix::POSIX_EINVAL, + "Net receive returns EOF without replacing errno"); + Check(Net::SocketClose(reader) == 0, "close wake reader"); readable[reader / 64] = bit; Check(Net::Select(reader + 1, readable.data(), nullptr, nullptr, immediate.data()) == -1 && diff --git a/tests/ShaderRecompilerComputeTests.cpp b/tests/ShaderRecompilerComputeTests.cpp index 6366dd3b..eb73f35f 100644 --- a/tests/ShaderRecompilerComputeTests.cpp +++ b/tests/ShaderRecompilerComputeTests.cpp @@ -1208,6 +1208,7 @@ struct TestCase { std::vector> decoded_counts; std::vector> ir_counts; u32 expected_storage_mip_descriptors = 0; + std::optional> expected_buffer_resources; }; struct GraphicsCase { @@ -1241,7 +1242,9 @@ struct CompiledShader { std::array MakeNativeUserData(const std::array *source) { std::array data{}; + data[3] = 3u << 28u; data[50] = 1u << 20u; + data[51] = 3u << 28u; if (source != nullptr) { data = *source; } @@ -1564,18 +1567,26 @@ CompiledShader CompileCase(const TestCase &test, u32 host_subgroup_size = 64) { "recompiler returned empty SPIR-V"); ValidateSpirv(test.name, result.spirv); CheckSpirvText(test, result.spirv); + const auto *buffer_binding = ShaderRecompiler::IR::FindBinding( + result.program.bindings, ShaderRecompiler::IR::DescriptorBindingKind::Buffers); + if (test.expected_buffer_resources) { + Require(test.name, "live buffer bindings", + buffer_binding != nullptr && + buffer_binding->resources == *test.expected_buffer_resources, + "dead buffer changed the surviving descriptor order"); + } std::vector packed_user_data; for (const auto reg : result.program.bindings.user_data_registers) { packed_user_data.push_back( resources.user_data[reg - result.program.user_data_base]); } packed_user_data.resize(result.program.bindings.ShaderDataDwords()); - const auto buffer_count = - static_cast(result.program.info.buffers.size()); + const auto buffer_count = result.program.bindings.memory_offset_count; for (u32 i = 0; i < buffer_count; i++) { u32 offset = 0; - if (i < test.storage_buffer_offsets.size()) { - offset = test.storage_buffer_offsets[i]; + const auto resource = buffer_binding->resources[i]; + if (resource < test.storage_buffer_offsets.size()) { + offset = test.storage_buffer_offsets[resource]; } Require(test.name, "shader data", offset % sizeof(u32) == 0 && offset < 256, "storage buffer offset is not representable"); @@ -1593,7 +1604,8 @@ std::array MakeStructuredStorageBufferData(u32 stride_bytes, std::array data{}; data[1] = (stride_bytes & 0x3fffu) << 16u; data[2] = num_records; - data[3] = DstSel(4, 5, 6, 7) | (1u << 24u); + data[3] = DstSel(4, 5, 6, 7) | (1u << 24u) | + (stride_bytes == 0 ? 3u << 28u : 0u); if (add_tid) { data[3] |= 1u << 23u; } @@ -13233,8 +13245,9 @@ public: info.offset = 0; info.range = buffer.size; if (test.storage_buffer_range_dwords != 0) { - const auto offset = i < test.storage_buffer_offsets.size() - ? test.storage_buffer_offsets[i] + const auto resource = buffers->resources[i]; + const auto offset = resource < test.storage_buffer_offsets.size() + ? test.storage_buffer_offsets[resource] : 0u; info.range = static_cast( test.storage_buffer_range_dwords * sizeof(u32) + offset); @@ -18876,6 +18889,31 @@ TestCase VectorFfbhI32NativeAndVop3OnGpu() { return test; } +TestCase Vop1SdwaFfbhCapturedScalarLowWordSource() { + using O = ShaderOpcode; + + constexpr std::array inputs = {0xdead0000u, 0xffff0001u, 0x00008000u, + 0x00010008u, 0xffff4000u, 0x0000ffffu}; + std::vector code; + for (u32 i = 0; i < inputs.size(); i++) { + AppendSmemLoadOpcode(&code, 0x08, 39, i * 4u); + code.insert(code.end(), {0x7e0072f9u, 0x00840627u}); // v_ffbh_u32 v0, s39.word0 + AppendStoreVgpr(&code, 0, i); + } + AppendEnd(&code); + + TestCase test; + test.name = "Vop1SdwaFfbhCapturedScalarLowWordSource"; + test.code = std::move(code); + test.initial.assign(inputs.begin(), inputs.end()); + test.expected = {0xffffffffu, 31u, 16u, 28u, 17u, 16u}; + test.opcodes = {O::S_BUFFER_LOAD_DWORD, O::V_FFBH_U32, O::V_MOV_B32, + O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + test.decoded_counts = {{"V_FFBH_U32 v0, s39.sdwa(sel=4,sext=0)", inputs.size()}}; + test.required_spirv = {"OpBitFieldUExtract", "FindUMsb"}; + return test; +} + TestCase Vop1SdwaFfblCapturedHighWordSource() { using O = ShaderOpcode; @@ -21192,6 +21230,37 @@ TestCase Vop1MoveRelDestination() { return test; } +TestCase CubeIdCapturedNegationAndOutputScale() { + using O = ShaderOpcode; + + constexpr std::array directions = { + std::array{4.0f, 2.0f, 1.0f}, std::array{-4.0f, 2.0f, 1.0f}, + std::array{1.0f, -4.0f, 2.0f}, std::array{1.0f, 4.0f, 2.0f}, + std::array{1.0f, 2.0f, -4.0f}, std::array{1.0f, 2.0f, 4.0f}}; + constexpr std::array registers = {47u, 49u, 48u}; + TestCase test; + test.name = "CubeIdCapturedNegationAndOutputScale"; + for (u32 i = 0; i < directions.size(); i++) { + for (u32 component = 0; component < registers.size(); component++) { + test.initial.push_back(std::bit_cast(directions[i][component])); + AppendVMovU32(&test.code, 30, (i * 3u + component) * 4u); + AppendBufferLoadDword(&test.code, registers[component], 30); + } + // The captured instruction negates Y/Z and divides the floating face ID by two. + test.code.insert(test.code.end(), {0xd544000bu, 0xdcc2632fu}); + AppendStoreVgpr(&test.code, 11, i); + } + AppendEnd(&test.code); + test.expected = {0x00000000u, 0x3f000000u, 0x3f800000u, + 0x3fc00000u, 0x40000000u, 0x40200000u}; + test.opcodes = {O::V_MOV_B32, O::BUFFER_LOAD_DWORD, O::V_CUBEID_F32, + O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + test.decoded_counts = { + {"V_CUBEID_F32 v11.omod(3), v47, v49.neg, v48.neg", directions.size()}}; + test.required_spirv = {"OpFNegate", "OpFMul"}; + return test; +} + TestCase VectorFloatSpecialOps() { using O = ShaderOpcode; @@ -24503,6 +24572,113 @@ TestCase BufferStoreFormatXDropsOutOfRangeRecord() { return test; } +TestCase TBufferCapturedZeroStrideOob(bool raw_bounds = false, bool scalar = false) { + using O = ShaderOpcode; + TestCase test; + test.name = scalar ? "TBufferZeroStrideOobPreservesScalarRead" + : raw_bounds ? "TBufferZeroStrideRawBoundsStillLoads" + : "TBufferCapturedZeroStrideOob"; + test.initial = {0x11223344u, 0x55667788u, 0x99aabbccu, 0xddeeff00u, + 0x12345678u, 0xdeadbeefu, 0xdeadbeefu}; + test.expected = test.initial; + for (u32 i = 0; i < 4; i++) { + test.expected[1 + i] = raw_bounds ? test.initial[i] : 0u; + } + test.expected[5] = test.initial[4]; + if (scalar) test.expected[6] = test.initial[0]; + test.user_data = MakeStructuredStorageBufferData(4, test.initial.size()); + std::copy_n(test.user_data.begin(), 4, test.user_data.begin() + 4); + test.user_data[4] = 0x2000u; + // Captured HITMAN descriptor in s[32:35]: mode 0, zero stride, arbitrary base. + test.user_data[32] = 0x4a398620u; + test.user_data[33] = 0; + test.user_data[34] = 0x2b5u; + test.user_data[35] = 0x0004d000u | (raw_bounds ? 3u << 28u : 0u); + test.has_user_data = true; + test.storage_buffer_offsets = {0, 0, 16}; + test.expected_buffer_resources = raw_bounds || scalar + ? std::vector{0, 1, 2} + : std::vector{0, 2}; + auto &code = test.code; + AppendVMovLiteral(&code, 0, test.initial[0]); + AppendStoreVgpr(&code, 0, 0); // Keep resource zero before the removed middle slot. + AppendVMovU32(&code, 30, 0); + code.push_back(0xea6b2000u); + code.push_back(0x8008191eu); // Exact pc 0x47c: typed float4, s[32:35], v30. + AppendVMovU32(&code, 20, 0); + code.push_back(EncodeMubuf0(0x0cu)); + code.push_back(EncodeMubuf1(33, 1, 20)); + if (scalar) { + code.push_back(EncodeSmem0(0x08u, 40, 16)); + code.push_back(EncodeSmem1(0)); + code.push_back(EncodeVop1(0x01u, 32, 40)); + } + for (u32 i = 0; i < 4; i++) AppendStoreVgpr(&code, 25 + i, 1 + i); + AppendStoreVgpr(&code, 33, 5); + if (scalar) AppendStoreVgpr(&code, 32, 6); + AppendEnd(&code); + test.opcodes = {O::V_MOV_B32, O::TBUFFER_LOAD_FORMAT_XYZW, + O::BUFFER_LOAD_DWORD, O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + if (scalar) test.opcodes.push_back(O::S_BUFFER_LOAD_DWORD); + test.ir_counts = {{"LoadBufferU32x4", raw_bounds ? 1u : 0u}}; + return test; +} + +TestCase BufferZeroStrideOobFormatsAndWidths() { + using O = ShaderOpcode; + TestCase test; + test.name = "BufferZeroStrideOobFormatsAndWidths"; + test.initial = std::vector(20, 0xdeadbeefu); + test.expected = std::vector(20, 0); + test.expected[10] = 0x3f800000u; + test.expected[12] = 0x3f800000u; + test.expected[14] = 1; + test.expected[16] = 1; + test.expected[18] = 0xabcdef01u; + test.expected[19] = 0xdeadbeefu; + test.user_data = MakeStructuredStorageBufferData(4, 20); + for (u32 base : {4u, 8u}) { + test.user_data[base] = 0x4a398620u; + test.user_data[base + 2] = 0x2b5u; + const u32 format = base == 4 ? 77u : 75u; // Four 32-bit float or unsigned integer components. + test.user_data[base + 3] = (format << 12u) | DstSel(1, 4, 1, 0); + } + test.has_user_data = true; + test.expected_buffer_resources = std::vector{0}; + auto &code = test.code; + AppendVMovU32(&code, 0, 0); + AppendStoreVgpr(&code, 0, 0); + AppendVMovU32(&code, 20, 0); + u32 destination = 0; + for (const auto [opcode, width] : + {std::pair{0x08u, 1u}, {0x0au, 1u}, {0x0cu, 1u}, {0x0du, 2u}, {0x0fu, 3u}}) { + code.push_back(EncodeMubuf0(opcode)); + code.push_back(EncodeMubuf1(destination, 1, 20)); + destination += width; + } + AppendVMovU32(&code, 8, 0); + AppendVMovU32(&code, 9, 0); + for (u32 base : {1u, 2u}) { + code.push_back(EncodeMubuf0(0x03u)); + code.push_back(EncodeMubuf1(base == 1 ? 10 : 14, base, 20)); + } + AppendVMovLiteral(&code, 18, 0xabcdef01u); + code.push_back(EncodeSop1(0x04u, 60, 126u)); // Preserve the partial wave's active lanes. + code.push_back(EncodeSop1(0x04u, 126, InlineU32(0))); + code.push_back(EncodeMubuf0(0x00u)); + code.push_back(EncodeMubuf1(18, 1, 20)); + code.push_back(EncodeSop1(0x04u, 126, 60u)); + for (u32 i = 0; i < 19; i++) AppendStoreVgpr(&code, i, i); + AppendEnd(&code); + test.opcodes = {O::V_MOV_B32, O::S_MOV_B64, O::BUFFER_LOAD_UBYTE, + O::BUFFER_LOAD_USHORT, O::BUFFER_LOAD_DWORD, + O::BUFFER_LOAD_DWORDX2, O::BUFFER_LOAD_DWORDX3, + O::BUFFER_LOAD_FORMAT_X, O::BUFFER_LOAD_FORMAT_XYZW, + O::BUFFER_STORE_DWORD, O::S_ENDPGM}; + test.ir_counts = {{"LoadBuffer", 0}}; + return test; +} + TestCase TBufferLoadVariants() { using O = ShaderOpcode; @@ -29001,6 +29177,7 @@ std::vector MakeCases() { AddCase(VectorVop3MoveAppliesFloatSourceModifiers); AddCase(VectorIntegerOps); AddCase(VectorFfbhI32NativeAndVop3OnGpu); + AddCase(Vop1SdwaFfbhCapturedScalarLowWordSource); AddCase(Vop1SdwaFfblCapturedHighWordSource); AddCase(Vop1SdwaNotCapturedByte0Source); AddCase(Vop1SdwaNotPreservesHighWordDestination); @@ -29072,6 +29249,7 @@ std::vector MakeCases() { AddCase(Vop1MoveRelSource); AddCase(Vop1MoveRelDestination); AddCase(VectorFloatSpecialOps); + AddCase(CubeIdCapturedNegationAndOutputScale); AddCase(MadMixF16LiteralHalfSourceUsesOpsel); AddCase(MadMixF16NegHiIsAbsAndNegIsIndependent); AddCase(VectorVop3FmaF16UsesRdna2Opcode34b); @@ -29173,6 +29351,10 @@ std::vector MakeCases() { AddCase(BufferStoreFormatXAddTidUsesLaneIndex); AddCase(BufferStoreFormatXDropsOutOfRangeRecord); AddCase(TBufferLoadVariants); + AddCase([] { return TBufferCapturedZeroStrideOob(); }); + AddCase([] { return TBufferCapturedZeroStrideOob(true); }); + AddCase([] { return TBufferCapturedZeroStrideOob(false, true); }); + AddCase(BufferZeroStrideOobFormatsAndWidths); AddCase(TBufferLoadFormatXyzwSnapshotsOverlappingAddress); AddCase(TBufferLoadFormatXyzwPackedSnapshotsOverlappingAddress); AddCase(TBufferLoadFormatX8UintZeroExtendsByte); @@ -34160,6 +34342,28 @@ int main(int argc, char **argv) { RunCase(&vulkan, Vop1SdwaMovByteDestinations()); return 0; } + if (argc == 2 && std::strcmp(argv[1], "--sdwa-ffbh-only") == 0) { + VulkanHarness vulkan; + RunCase(&vulkan, Vop1SdwaFfbhCapturedScalarLowWordSource()); + RunCase(&vulkan, VectorFfbhI32NativeAndVop3OnGpu()); + RunCase(&vulkan, Vop1SdwaFfblCapturedHighWordSource()); + return 0; + } + if (argc == 2 && std::strcmp(argv[1], "--cubeid-omod-only") == 0) { + VulkanHarness vulkan; + RunCase(&vulkan, CubeIdCapturedNegationAndOutputScale()); + RunCase(&vulkan, VectorFloatSpecialOps()); + return 0; + } + if (argc == 2 && std::strcmp(argv[1], "--zero-stride-oob-only") == 0) { + VulkanHarness vulkan; + RunCase(&vulkan, TBufferCapturedZeroStrideOob()); + RunCase(&vulkan, TBufferCapturedZeroStrideOob(true)); + RunCase(&vulkan, TBufferCapturedZeroStrideOob(false, true)); + RunCase(&vulkan, BufferZeroStrideOobFormatsAndWidths()); + RunCase(&vulkan, TBufferLoadFormatXIdxenUsesDescriptorStride()); + return 0; + } if (argc == 2 && std::strcmp(argv[1], "--sdwa-ashr-only") == 0) { VulkanHarness vulkan; RunCase(&vulkan, Vop2SdwaAshrrevCapturedWord0SignExtends()); diff --git a/tests/shaderCfgTests.cpp b/tests/shaderCfgTests.cpp index eebdb918..85a3c61b 100644 --- a/tests/shaderCfgTests.cpp +++ b/tests/shaderCfgTests.cpp @@ -86,7 +86,11 @@ ShaderRecompiler::CompileOptions MakeCompileOptions(ShaderType stage) { static const ShaderVertexInputInfo vertex{}; static const ShaderPixelInputInfo pixel{}; static const ShaderComputeInputInfo compute{}; - static const std::array user_data{}; + static const auto user_data = [] { + std::array data{}; + data[3] = 3u << 28u; // Default fixture buffer uses raw offset bounds. + return data; + }(); ShaderRecompiler::CompileOptions options; options.stage = stage; @@ -6512,6 +6516,7 @@ void TestNewShaderRecompilerNativeWideScalarMemoryIr() { void TestNewShaderRecompilerNativeWideBufferIr() { const uint32_t shader[] = { + EncodeSMovB32(83, 255), 3u << 28u, // Raw bounds for the s[80:83] fixture. EncodeMubuf0(0x0d, 0), EncodeMubuf1(0, 20, 1), // buffer_load_dwordx2 v[0:1] EncodeMubuf0(0x1d, 16),