diff options
| author | CamilleLaVey <camillelavey99@gmail.com> | 2026-08-17 00:30:19 +0200 |
|---|---|---|
| committer | crueter <crueter@eden-emu.dev> | 2026-08-17 00:30:19 +0200 |
| commit | 2000fdfb7b27e5e2546098b44268fc74887b22a5 (patch) | |
| tree | 924d533de5e75f9f08e02f67bddc946690ea5665 /src/video_core/renderer_vulkan/present | |
| parent | dc95cd09eea9749250fe31a3072684d341d19417 (diff) | |
[vulkan, android] Initial implementation of LSFG-VK (#4263)
Pretty much self explanatory, own implementation based on LSFG-VK available with some additional reverse engineer work for the actual use of shaders inside Lossless.dll, we don't store/ bundle any tools that bypass security and self ownership: to use this feature is required to own your own copy of Lossless Scaling and I encourage to support developers behind this work.
Special thanks:
1.- THS - Original developer behind Lossless Scaling
2.- PancakeTAS - Developer behind LSFG-VK
3.- The Big Smolio The Sleeper (@gidoly)
Co-authored-by: lizzie <lizzie@eden-emu.dev>
Reviewed-on: https://git.eden-emu.dev/eden-emu/eden/pulls/4263
Diffstat (limited to 'src/video_core/renderer_vulkan/present')
24 files changed, 2861 insertions, 6 deletions
diff --git a/src/video_core/renderer_vulkan/present/frame_gen.cpp b/src/video_core/renderer_vulkan/present/frame_gen.cpp new file mode 100644 index 0000000000..b0d16819e5 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen.cpp @@ -0,0 +1,361 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <algorithm> +#include <string> +#include <vector> + +#include "common/fs/file.h" +#include "common/fs/fs.h" +#include "common/fs/path_util.h" +#include "common/settings.h" +#include "video_core/renderer_vulkan/present/frame_gen.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/renderer_vulkan/vk_present_manager.h" +#include "video_core/renderer_vulkan/vk_scheduler.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr size_t COLOR_CHANNELS = 4; +constexpr u64 LSFG_REQUIRED_FRAMES = 2; +constexpr u32 LSFG_RECURRENCE_FRAMES = 2; + +[[nodiscard]] f32 ManualFlowScale() { + return static_cast<f32>(Settings::values.frame_gen_flow_scale.GetValue()) / 100.0f; +} + +[[nodiscard]] f32 ConfiguredFlowScale(VkExtent2D guest_extent, VkExtent2D presented_extent) { + if (!Settings::values.frame_gen_flow_scale_auto.GetValue()) { + return ManualFlowScale(); + } + if (guest_extent.width == 0 || presented_extent.width == 0) { + return 1.0f; + } + + const f32 rendered_width = static_cast<f32>(guest_extent.width) * + Settings::values.resolution_info.up_factor; + const f32 ratio = rendered_width / static_cast<f32>(presented_extent.width); + + constexpr f32 FLOW_SCALE_STEPS = 20.0f; + const f32 stepped = std::ceil(ratio * FLOW_SCALE_STEPS) / FLOW_SCALE_STEPS; + return std::clamp(stepped, 0.25f, 1.0f); +} + +bool IsBlueFirst(VkFormat format) { + return format == VK_FORMAT_B8G8R8A8_UNORM || format == VK_FORMAT_B8G8R8A8_SRGB; +} + +VkDeviceSize BytesPerTexel(VkFormat format) { + switch (format) { + case VK_FORMAT_R8_UNORM: + return 1; + case VK_FORMAT_R16G16B16A16_SFLOAT: + return 8; + default: + return COLOR_CHANNELS; + } +} + +void WritePortablePixmap(const std::filesystem::path& path, const std::string& magic, + VkExtent2D extent, std::span<const u8> pixels) { + Common::FS::IOFile file{path, Common::FS::FileAccessMode::Write, + Common::FS::FileType::BinaryFile}; + if (!file.IsOpen()) { + return; + } + + const std::string header = magic + "\n" + std::to_string(extent.width) + " " + + std::to_string(extent.height) + "\n255\n"; + if (file.Write(header) != header.size()) { + return; + } + + void(file.Write(pixels)); + void(file.Flush()); +} + +void WriteGrayscalePgm(const std::filesystem::path& path, VkExtent2D extent, + std::span<const u8> pixels) { + const size_t expected = static_cast<size_t>(extent.width) * extent.height; + WritePortablePixmap(path, "P5", extent, pixels.subspan(0, std::min(expected, pixels.size()))); +} + +void WriteRaw(const std::filesystem::path& path, std::span<const u8> pixels) { + Common::FS::IOFile file{path, Common::FS::FileAccessMode::Write, + Common::FS::FileType::BinaryFile}; + if (!file.IsOpen()) { + return; + } + void(file.Write(pixels)); + void(file.Flush()); +} + +void WriteColorPpm(const std::filesystem::path& path, VkExtent2D extent, + std::span<const u8> pixels, bool blue_first) { + const size_t pixel_count = static_cast<size_t>(extent.width) * extent.height; + if (pixels.size() < pixel_count * COLOR_CHANNELS) { + return; + } + + std::vector<u8> rgb(pixel_count * 3); + for (size_t i = 0; i < pixel_count; ++i) { + const u8 first = pixels[i * COLOR_CHANNELS]; + const u8 green = pixels[i * COLOR_CHANNELS + 1]; + const u8 third = pixels[i * COLOR_CHANNELS + 2]; + rgb[i * 3] = blue_first ? third : first; + rgb[i * 3 + 1] = green; + rgb[i * 3 + 2] = blue_first ? first : third; + } + + WritePortablePixmap(path, "P6", extent, rgb); +} + +VkImageMemoryBarrier MakeTransitionBarrier(VkImage image, VkAccessFlags src_access, + VkAccessFlags dst_access, VkImageLayout old_layout, + VkImageLayout new_layout) { + return VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = src_access, + .dstAccessMask = dst_access, + .oldLayout = old_layout, + .newLayout = new_layout, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; +} + +VkImageCopy MakeCopyRegion(VkExtent2D extent) { + return VkImageCopy{ + .srcSubresource{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .srcOffset = {}, + .dstSubresource{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .mipLevel = 0, + .baseArrayLayer = 0, + .layerCount = 1, + }, + .dstOffset = {}, + .extent = {.width = extent.width, .height = extent.height, .depth = 1}, + }; +} + +void CopyPresentedFrame(vk::CommandBuffer cmdbuf, VkImage source, LsfgImage& destination, + VkExtent2D extent) { + const auto make_barrier = MakeTransitionBarrier; + + const std::array before{ + make_barrier(source, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, VK_ACCESS_TRANSFER_READ_BIT, + VK_IMAGE_LAYOUT_GENERAL, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL), + make_barrier(destination.Handle(), VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_TRANSFER_WRITE_BIT, + destination.Layout(), VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_TRANSFER_BIT, 0, {}, {}, before); + + cmdbuf.CopyImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.Handle(), + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, MakeCopyRegion(extent)); + + const std::array after{ + make_barrier(source, VK_ACCESS_TRANSFER_READ_BIT, VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT, + VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + make_barrier(destination.Handle(), VK_ACCESS_TRANSFER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT, + VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, VK_IMAGE_LAYOUT_GENERAL), + }; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_TRANSFER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + 0, {}, {}, after); + + destination.SetLayout(VK_IMAGE_LAYOUT_GENERAL); +} + +} // Anonymous namespace + +FrameGen::FrameGen(MemoryAllocator& memory_allocator_, Scheduler& scheduler_) + : memory_allocator{memory_allocator_}, scheduler{scheduler_} {} + +FrameGen::~FrameGen() = default; + +void FrameGen::Process(const Device& device, Frame* frame, VkFormat format, + VkExtent2D guest_extent) { + generated = false; + + if (unavailable || !Settings::values.frame_gen.GetValue()) { + if (chain) { + scheduler.Finish(); + chain.reset(); + } + warm_streak = 0; + return; + } + + if (!frame->storage_view) { + unavailable = true; + return; + } + + if (!shaders) { + shaders.emplace(device); + if (!shaders->IsValid()) { + unavailable = true; + return; + } + } + + peak_guest_extent.width = std::max(peak_guest_extent.width, guest_extent.width); + peak_guest_extent.height = std::max(peak_guest_extent.height, guest_extent.height); + + const VkExtent2D extent{.width = frame->width, .height = frame->height}; + const f32 flow_scale = ConfiguredFlowScale(peak_guest_extent, extent); + if (!chain || built_extent.width != extent.width || built_extent.height != extent.height || + built_format != format || built_flow_scale != flow_scale) { + Rebuild(device, extent, format, flow_scale); + } + + const u64 count = frame_count++; + last_count = count; + last_generations = plan.generations; + + const bool warm = plan.warm && count + 1 >= LSFG_REQUIRED_FRAMES; + warm_streak = warm ? warm_streak + 1 : 0; + generated = warm && warm_streak >= LSFG_RECURRENCE_FRAMES && plan.generations > 0; + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, source = *frame->image, extent, count, + dispatch = warm](vk::CommandBuffer cmdbuf) { + CopyPresentedFrame(cmdbuf, source, chain->Input(count), extent); + if (dispatch) { + chain->DispatchShared(cmdbuf, count); + } + }); + + const bool dump_requested = generated && Settings::values.frame_gen_dump_flow.GetValue(); + if (!dump_requested) { + dumped = false; + } else if (!dumped) { + DumpDebugImages(count); + dumped = true; + } +} + +size_t FrameGen::WantedGenerations(size_t capacity) { + if (unavailable) { + plan = {}; + return 0; + } + plan = pacer.Plan(capacity); + return plan.generations; +} + +size_t FrameGen::GeneratedFrameCount() const { + return generated ? last_generations : 0; +} + +void FrameGen::GenerateInto(const Device& device, Frame* destination, size_t generation) { + chain->SetTarget(device, last_generations, generation, destination->index, + *destination->storage_view); + + const VkExtent2D extent{.width = destination->width, .height = destination->height}; + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record([this, count = last_count, generation_count = last_generations, generation, + target = destination->index, image = *destination->image, + extent](vk::CommandBuffer cmdbuf) { + chain->DispatchGeneration(cmdbuf, count, generation_count, generation, target, image, + extent); + }); +} + +void FrameGen::Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale) { + scheduler.Finish(); + chain.reset(); + + built_flow_scale = flow_scale; + + chain.emplace(device, memory_allocator, *shaders, extent, format, built_flow_scale); + built_extent = extent; + built_format = format; + frame_count = 0; + warm_streak = 0; + generated = false; +} + +void FrameGen::DumpDebugImages(u64 count) { + const std::filesystem::path directory = + Common::FS::GetEdenPath(Common::FS::EdenPath::LosslessDir) / "debug"; + if (!Common::FS::CreateDirs(directory)) { + return; + } + + const auto dump = [&](const std::string& name, LsfgImage& image) { + const VkExtent2D extent = image.Extent(); + const VkFormat format = image.Format(); + const VkDeviceSize texel_size = BytesPerTexel(format); + const VkDeviceSize size = + static_cast<VkDeviceSize>(extent.width) * extent.height * texel_size; + + vk::Buffer buffer = CreateWrappedBuffer(memory_allocator, size, MemoryUsage::Download); + + scheduler.RequestOutsideRenderPassOperationContext(); + scheduler.Record( + [handle = image.Handle(), dst = *buffer, extent](vk::CommandBuffer cmdbuf) { + DownloadColorImage( + cmdbuf, handle, dst, + VkExtent3D{.width = extent.width, .height = extent.height, .depth = 1}); + }); + scheduler.Finish(); + + buffer.Invalidate(); + const std::span<u8> mapped = buffer.Mapped(); + + if (format == LSFG_FLOW_FORMAT) { + WriteGrayscalePgm(directory / (name + ".pgm"), extent, mapped); + } else if (texel_size == COLOR_CHANNELS) { + WriteColorPpm(directory / (name + ".ppm"), extent, mapped, IsBlueFirst(format)); + } else { + WriteRaw(directory / (name + "_" + std::to_string(extent.width) + "x" + + std::to_string(extent.height) + ".f16"), + mapped.subspan(0, std::min<size_t>(size, mapped.size()))); + } + }; + + dump("in0", chain->Input(0)); + dump("in1", chain->Input(1)); + + for (size_t level = 0; level < LSFG_MIP_LEVELS; ++level) { + dump("flow_mip" + std::to_string(level), chain->FlowLevel(level)); + } + for (size_t index = 0; index < 2; ++index) { + dump("alpha0_" + std::to_string(index), chain->AlphaOutput(0, count, index)); + dump("alpha6_" + std::to_string(index), chain->AlphaOutput(LSFG_MIP_LEVELS - 1, count, + index)); + } + for (size_t level = 0; level < LSFG_BETA_OUTPUTS; ++level) { + dump("beta_" + std::to_string(level), chain->BetaOutput(level)); + } + + dump("gamma0", chain->GammaOutput(0)); + dump("gamma6", chain->GammaOutput(LSFG_MIP_LEVELS - 1)); + dump("delta2_out1", chain->DeltaOutput1(LSFG_DELTA_INSTANCES - 1)); + dump("delta2_out2", chain->DeltaOutput2(LSFG_DELTA_INSTANCES - 1)); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/frame_gen.h b/src/video_core/renderer_vulkan/present/frame_gen.h new file mode 100644 index 0000000000..19c6d04289 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen.h @@ -0,0 +1,57 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <optional> + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/frame_gen_pacer.h" +#include "video_core/renderer_vulkan/present/lsfg_chain.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" + +namespace Vulkan { + +class Device; +class Scheduler; +struct Frame; + +class FrameGen { +public: + explicit FrameGen(MemoryAllocator& memory_allocator, Scheduler& scheduler); + ~FrameGen(); + + void Process(const Device& device, Frame* frame, VkFormat format, VkExtent2D guest_extent); + + [[nodiscard]] size_t WantedGenerations(size_t capacity); + + [[nodiscard]] size_t GeneratedFrameCount() const; + + void GenerateInto(const Device& device, Frame* destination, size_t generation); + +private: + void Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale); + void DumpDebugImages(u64 count); + + MemoryAllocator& memory_allocator; + Scheduler& scheduler; + + std::optional<LsfgShaders> shaders; + std::optional<LsfgChain> chain; + FrameGenPacer pacer; + FrameGenPlan plan{}; + VkExtent2D peak_guest_extent{}; + VkExtent2D built_extent{}; + VkFormat built_format{VK_FORMAT_UNDEFINED}; + f32 built_flow_scale{}; + u64 frame_count{}; + u64 last_count{}; + size_t last_generations{}; + u32 warm_streak{}; + bool generated{}; + bool unavailable{}; + bool dumped{}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp b/src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp new file mode 100644 index 0000000000..78a8c83b5a --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen_pacer.cpp @@ -0,0 +1,240 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <algorithm> +#include <cmath> +#include <utility> + +#include "common/settings.h" +#include "video_core/renderer_vulkan/present/frame_gen_pacer.h" + +namespace Vulkan { + +namespace { + +using Clock = std::chrono::steady_clock; + +constexpr f32 INTERVAL_SMOOTHING = 0.25f; +constexpr f32 MINIMUM_BASE_RATE = 10.0f; +constexpr f32 BURST_CADENCE_RATIO = 3.0f; +constexpr f32 BURST_TARGET_RATIO = 2.0f; +constexpr f32 PROBE_THROUGHPUT_TOLERANCE = 0.95f; +constexpr f32 PROBE_BASE_COLLAPSE_RATIO = 0.70f; +constexpr f32 PROBE_MARGINAL_GAIN = 1.15f; +constexpr f32 TARGET_SATISFIED_RATIO = 0.95f; +constexpr f32 UNLOADED_BASE_RETENTION = 0.75f; +constexpr f32 CREDIT_EPSILON = 1.0e-4f; +constexpr u32 MAX_PROBE_FAILURES = 4; + +constexpr auto STABILIZATION_DURATION = std::chrono::seconds(1); +constexpr auto PROBE_DURATION = std::chrono::seconds(1); +constexpr auto DEFICIT_DURATION = std::chrono::seconds(1); +constexpr auto PROBE_STEP_DELAY = std::chrono::milliseconds(250); + +[[nodiscard]] Clock::duration ProbeBackoff(u32 failures) { + switch (failures) { + case 1: + return std::chrono::seconds(5); + case 2: + return std::chrono::seconds(15); + case 3: + return std::chrono::seconds(30); + default: + return std::chrono::seconds(60); + } +} + +} // Anonymous namespace + +FrameGenPlan FrameGenPacer::Plan(size_t capacity) { + const size_t ceiling = std::min(capacity, Settings::FrameGenMaxGenerations()); + if (ceiling == 0) { + Reset(); + return {}; + } + + const Clock::time_point now = Clock::now(); + const size_t previous_generations = std::exchange(issued_generations, 0); + if (!last_frame) { + last_frame = now; + return {}; + } + + const Clock::duration interval = now - *last_frame; + const f32 interval_seconds = std::chrono::duration<f32>(interval).count(); + last_frame = now; + + if (interval_seconds <= 0.0f) { + Stabilize(now); + return {}; + } + + const f32 target_rate = static_cast<f32>(Settings::values.frame_gen_target_rate.GetValue()); + + if (smoothed_interval > 0.0f) { + f32 burst_threshold = BURST_CADENCE_RATIO / smoothed_interval; + if (target_rate > 0.0f) { + burst_threshold = std::max(burst_threshold, target_rate * BURST_TARGET_RATIO); + } + if (1.0f / interval_seconds > burst_threshold) { + DeferEvaluations(interval); + output_credit = 0.0f; + return {}; + } + } + + if (interval_seconds > 1.0f / MINIMUM_BASE_RATE) { + Stabilize(now); + return {}; + } + + smoothed_interval = smoothed_interval > 0.0f + ? smoothed_interval + + (interval_seconds - smoothed_interval) * INTERVAL_SMOOTHING + : interval_seconds; + + if (previous_generations == 0) { + const f32 measured = 1.0f / smoothed_interval; + unloaded_base_rate = + unloaded_base_rate > 0.0f + ? unloaded_base_rate + (measured - unloaded_base_rate) * INTERVAL_SMOOTHING + : measured; + } + + if (stable_until) { + if (now < *stable_until) { + return {}; + } + stable_until.reset(); + } + + if (target_rate == 0.0f) { + limit = std::min(Settings::FrameGenGenerations(), ceiling); + output_credit = 0.0f; + issued_generations = limit; + return {.generations = limit, .warm = limit > 0}; + } + + UpdateLimit(now, 1.0f / smoothed_interval, target_rate, ceiling); + + const size_t allowed = std::min(limit, ceiling); + const f32 desired_outputs = smoothed_interval * target_rate; + if (allowed == 0 || desired_outputs <= 1.0f) { + output_credit = 0.0f; + return {}; + } + + output_credit += desired_outputs; + const size_t outputs = + std::max<size_t>(1, static_cast<size_t>(std::floor(output_credit + CREDIT_EPSILON))); + const size_t generations = std::min(outputs - 1, allowed); + + output_credit -= static_cast<f32>(generations + 1); + if (output_credit < 0.0f) { + output_credit = 0.0f; + } else if (generations == allowed && output_credit >= 1.0f) { + output_credit = std::fmod(output_credit, 1.0f); + } + + issued_generations = generations; + return {.generations = generations, .warm = true}; +} + +void FrameGenPacer::UpdateLimit(Clock::time_point now, f32 base_rate, f32 target_rate, + size_t ceiling) { + limit = std::min(limit, ceiling); + + if (probe_until) { + if (now < *probe_until) { + return; + } + probe_until.reset(); + output_credit = 0.0f; + + const f32 previous_output = + std::min(target_rate, probe_base_rate * static_cast<f32>(probe_previous_limit + 1)); + const f32 current_output = + std::min(target_rate, base_rate * static_cast<f32>(limit + 1)); + + const bool throughput_regressed = + current_output < previous_output * PROBE_THROUGHPUT_TOLERANCE; + const bool collapsed_for_marginal_gain = + base_rate < probe_base_rate * PROBE_BASE_COLLAPSE_RATIO && + current_output < previous_output * PROBE_MARGINAL_GAIN; + const bool emulation_slowed = unloaded_base_rate > 0.0f && + base_rate < unloaded_base_rate * UNLOADED_BASE_RETENTION; + + if (throughput_regressed || collapsed_for_marginal_gain || emulation_slowed) { + limit = probe_previous_limit; + probe_failures = std::min(probe_failures + 1, MAX_PROBE_FAILURES); + next_probe = now + ProbeBackoff(probe_failures); + deficit_since.reset(); + return; + } + + probe_failures = 0; + next_probe = now + PROBE_STEP_DELAY; + } + + if (base_rate * static_cast<f32>(limit + 1) >= target_rate * TARGET_SATISFIED_RATIO || + limit >= ceiling) { + deficit_since.reset(); + return; + } + + if (!deficit_since) { + deficit_since = now; + return; + } + if (now - *deficit_since < DEFICIT_DURATION) { + return; + } + if (next_probe && now < *next_probe) { + return; + } + + probe_previous_limit = limit; + probe_base_rate = base_rate; + ++limit; + probe_until = now + PROBE_DURATION; + deficit_since.reset(); + output_credit = 0.0f; +} + +void FrameGenPacer::DeferEvaluations(Clock::duration amount) { + const auto defer = [amount](std::optional<Clock::time_point>& deadline) { + if (deadline) { + *deadline += amount; + } + }; + defer(stable_until); + defer(probe_until); + defer(next_probe); + deficit_since.reset(); +} + +void FrameGenPacer::Stabilize(Clock::time_point now) { + stable_until = now + STABILIZATION_DURATION; + probe_until.reset(); + deficit_since.reset(); + smoothed_interval = 0.0f; + output_credit = 0.0f; +} + +void FrameGenPacer::Reset() { + last_frame.reset(); + stable_until.reset(); + probe_until.reset(); + next_probe.reset(); + deficit_since.reset(); + smoothed_interval = 0.0f; + output_credit = 0.0f; + probe_base_rate = 0.0f; + unloaded_base_rate = 0.0f; + issued_generations = 0; + probe_previous_limit = 0; + limit = 0; + probe_failures = 0; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/frame_gen_pacer.h b/src/video_core/renderer_vulkan/present/frame_gen_pacer.h new file mode 100644 index 0000000000..7f4b23920c --- /dev/null +++ b/src/video_core/renderer_vulkan/present/frame_gen_pacer.h @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <chrono> +#include <optional> + +#include "common/common_types.h" + +namespace Vulkan { + +struct FrameGenPlan { + size_t generations{}; + bool warm{}; +}; + +class FrameGenPacer { +public: + [[nodiscard]] FrameGenPlan Plan(size_t capacity); + + void Reset(); + +private: + using Clock = std::chrono::steady_clock; + + void Stabilize(Clock::time_point now); + void DeferEvaluations(Clock::duration amount); + void UpdateLimit(Clock::time_point now, f32 base_rate, f32 target_rate, size_t ceiling); + + std::optional<Clock::time_point> last_frame; + std::optional<Clock::time_point> stable_until; + std::optional<Clock::time_point> probe_until; + std::optional<Clock::time_point> next_probe; + std::optional<Clock::time_point> deficit_since; + f32 smoothed_interval{}; + f32 output_credit{}; + f32 probe_base_rate{}; + f32 unloaded_base_rate{}; + size_t issued_generations{}; + size_t probe_previous_limit{}; + size_t limit{}; + u32 probe_failures{}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_alpha.cpp b/src/video_core/renderer_vulkan/present/lsfg_alpha.cpp new file mode 100644 index 0000000000..a735dd11f4 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_alpha.cpp @@ -0,0 +1,140 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <vector> + +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_alpha.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DISPATCH_TILE_SHIFT = 3; + +[[nodiscard]] u32 GroupCount(u32 size) { + return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT; +} + +[[nodiscard]] VkExtent2D HalveExtent(VkExtent2D extent) { + return VkExtent2D{ + .width = (extent.width + 1) >> 1, + .height = (extent.height + 1) >> 1, + }; +} + +} // Anonymous namespace + +LsfgAlphaPasses::LsfgAlphaPasses(const Device& device, const LsfgShaders& shaders) { + using namespace VideoCore::FrameGen::PerformanceShader; + + passes[0] = LsfgPass(device, shaders, ALPHA[0], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[1] = LsfgPass(device, shaders, ALPHA[1], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[2] = LsfgPass(device, shaders, ALPHA[2], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[3] = LsfgPass(device, shaders, ALPHA[3], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); +} + +LsfgAlpha::LsfgAlpha(const Device& device, MemoryAllocator& memory_allocator, + const LsfgAlphaPasses& passes_, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImage& input_) + : passes{&passes_}, input{&input_} { + const VkExtent2D half_extent = HalveExtent(input->Extent()); + const VkExtent2D quarter_extent = HalveExtent(half_extent); + + temp1 = LsfgImage(device, memory_allocator, half_extent); + temp2 = LsfgImage(device, memory_allocator, half_extent); + for (size_t i = 0; i < temp3.size(); ++i) { + temp3[i] = LsfgImage(device, memory_allocator, quarter_extent); + for (size_t j = 0; j < LSFG_HISTORY_SLOTS; ++j) { + out_images[j][i] = LsfgImage(device, memory_allocator, quarter_extent); + } + } + + std::vector<VkDescriptorSetLayout> layouts; + for (size_t i = 0; i < LSFG_ALPHA_STAGES - 1; ++i) { + layouts.push_back(passes->Get(i).SetLayout()); + } + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + layouts.push_back(passes->Get(3).SetLayout()); + } + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + for (size_t i = 0; i < LSFG_ALPHA_STAGES - 1; ++i) { + descriptor_sets[i] = owned_sets[i]; + } + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + last_descriptor_sets[i] = owned_sets[LSFG_ALPHA_STAGES - 1 + i]; + } + + const VkSampler sampler = resources.GetSampler(); + + LsfgDescriptorWriter(descriptor_sets[0]) + .AddSampler(sampler) + .AddSampledImage(*input) + .AddStorageImage(temp1) + .Build(device); + LsfgDescriptorWriter(descriptor_sets[1]) + .AddSampler(sampler) + .AddSampledImage(temp1) + .AddStorageImage(temp2) + .Build(device); + LsfgDescriptorWriter(descriptor_sets[2]) + .AddSampler(sampler) + .AddSampledImage(temp2) + .AddStorageImages(temp3) + .Build(device); + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + LsfgDescriptorWriter(last_descriptor_sets[i]) + .AddSampler(sampler) + .AddSampledImages(temp3) + .AddStorageImages(out_images[i]) + .Build(device); + } +} + +void LsfgAlpha::PushBarriers(LsfgBarriers& barriers, u64 frame_count, size_t stage) { + switch (stage) { + case 0: + barriers.WriteToRead(*input).ReadToWrite(temp1); + break; + case 1: + barriers.WriteToRead(temp1).ReadToWrite(temp2); + break; + case 2: + barriers.WriteToRead(temp2).ReadToWriteAll(temp3); + break; + default: + barriers.WriteToReadAll(temp3).ReadToWriteAll(out_images[frame_count % LSFG_HISTORY_SLOTS]); + break; + } +} + +void LsfgAlpha::DispatchStage(vk::CommandBuffer cmdbuf, u64 frame_count, size_t stage) { + const VkExtent2D extent = stage < 2 ? temp1.Extent() : temp3[0].Extent(); + const VkDescriptorSet set = stage < LSFG_ALPHA_STAGES - 1 + ? descriptor_sets[stage] + : last_descriptor_sets[frame_count % LSFG_HISTORY_SLOTS]; + + passes->Get(stage).BindSet(cmdbuf, set); + cmdbuf.Dispatch(GroupCount(extent.width), GroupCount(extent.height), 1); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_alpha.h b/src/video_core/renderer_vulkan/present/lsfg_alpha.h new file mode 100644 index 0000000000..1dc8864431 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_alpha.h @@ -0,0 +1,62 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr size_t LSFG_ALPHA_STAGES = 4; + +class LsfgAlphaPasses { +public: + LsfgAlphaPasses() = default; + LsfgAlphaPasses(const Device& device, const LsfgShaders& shaders); + + [[nodiscard]] const LsfgPass& Get(size_t stage) const { + return passes[stage]; + } + +private: + std::array<LsfgPass, LSFG_ALPHA_STAGES> passes; +}; + +class LsfgAlpha { +public: + LsfgAlpha() = default; + LsfgAlpha(const Device& device, MemoryAllocator& memory_allocator, + const LsfgAlphaPasses& passes_, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImage& input); + + void PushBarriers(LsfgBarriers& barriers, u64 frame_count, size_t stage); + void DispatchStage(vk::CommandBuffer cmdbuf, u64 frame_count, size_t stage); + + [[nodiscard]] LsfgImageHistory& Outputs() { + return out_images; + } + +private: + const LsfgAlphaPasses* passes{}; + LsfgImage* input{}; + + std::array<VkDescriptorSet, LSFG_ALPHA_STAGES - 1> descriptor_sets{}; + std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> last_descriptor_sets{}; + vk::DescriptorSets owned_sets; + + LsfgImage temp1; + LsfgImage temp2; + LsfgImagePair temp3; + LsfgImageHistory out_images; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_beta.cpp b/src/video_core/renderer_vulkan/present/lsfg_beta.cpp new file mode 100644 index 0000000000..6dddc7c21d --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_beta.cpp @@ -0,0 +1,147 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <vector> + +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_beta.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DISPATCH_TILE_SHIFT = 3; +constexpr u32 OUTPUT_TILE_SHIFT = 5; + +[[nodiscard]] u32 GroupCount(u32 size, u32 shift) { + return (size + (1u << shift) - 1) >> shift; +} + +} // Anonymous namespace + +LsfgBeta::LsfgBeta(const Device& device, MemoryAllocator& memory_allocator, + const LsfgShaders& shaders, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImageHistory& inputs_) + : inputs{&inputs_} { + using namespace VideoCore::FrameGen::PerformanceShader; + + passes[0] = LsfgPass(device, shaders, BETA[0], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {6, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + for (size_t i = 1; i < LSFG_BETA_STAGES - 1; ++i) { + passes[i] = LsfgPass(device, shaders, BETA[i], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + } + passes[4] = LsfgPass(device, shaders, BETA[4], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {6, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + + const VkExtent2D extent = (*inputs)[0][0].Extent(); + for (size_t i = 0; i < temp1.size(); ++i) { + temp1[i] = LsfgImage(device, memory_allocator, extent); + temp2[i] = LsfgImage(device, memory_allocator, extent); + } + for (size_t i = 0; i < LSFG_BETA_OUTPUTS; ++i) { + const VkExtent2D level_extent{ + .width = extent.width >> i, + .height = extent.height >> i, + }; + out_images[i] = LsfgImage(device, memory_allocator, level_extent, LSFG_FLOW_FORMAT); + } + + std::vector<VkDescriptorSetLayout> layouts; + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + layouts.push_back(passes[0].SetLayout()); + } + for (size_t i = 1; i < LSFG_BETA_STAGES; ++i) { + layouts.push_back(passes[i].SetLayout()); + } + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + first_descriptor_sets[i] = owned_sets[i]; + } + for (size_t i = 0; i < LSFG_BETA_STAGES - 1; ++i) { + descriptor_sets[i] = owned_sets[LSFG_HISTORY_SLOTS + i]; + } + + const VkSampler sampler = resources.GetSampler(); + const VkSampler border_sampler = resources.GetSampler( + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, VK_COMPARE_OP_NEVER, true); + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + LsfgDescriptorWriter(first_descriptor_sets[i]) + .AddSampler(border_sampler) + .AddSampledImages((*inputs)[(i + 1) % LSFG_HISTORY_SLOTS]) + .AddSampledImages((*inputs)[(i + 2) % LSFG_HISTORY_SLOTS]) + .AddSampledImages((*inputs)[i % LSFG_HISTORY_SLOTS]) + .AddStorageImages(temp1) + .Build(device); + } + LsfgDescriptorWriter(descriptor_sets[0]) + .AddSampler(sampler) + .AddSampledImages(temp1) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(descriptor_sets[1]) + .AddSampler(sampler) + .AddSampledImages(temp2) + .AddStorageImages(temp1) + .Build(device); + LsfgDescriptorWriter(descriptor_sets[2]) + .AddSampler(sampler) + .AddSampledImages(temp1) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(descriptor_sets[3]) + .AddUniformBuffer(resources.GetBuffer(0.5f), LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampledImages(temp2) + .AddStorageImages(out_images) + .Build(device); +} + +void LsfgBeta::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count) { + const VkExtent2D extent = temp1[0].Extent(); + const u32 groups_x = GroupCount(extent.width, DISPATCH_TILE_SHIFT); + const u32 groups_y = GroupCount(extent.height, DISPATCH_TILE_SHIFT); + + LsfgBarriers barriers(cmdbuf); + for (auto& slot : *inputs) { + barriers.WriteToReadAll(slot); + } + barriers.ReadToWriteAll(temp1).Build(); + + passes[0].Bind(cmdbuf, first_descriptor_sets[frame_count % LSFG_HISTORY_SLOTS]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build(); + passes[1].Bind(cmdbuf, descriptor_sets[0]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp2).ReadToWriteAll(temp1).Build(); + passes[2].Bind(cmdbuf, descriptor_sets[1]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build(); + passes[3].Bind(cmdbuf, descriptor_sets[2]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp2).ReadToWriteAll(out_images).Build(); + passes[4].Bind(cmdbuf, descriptor_sets[3]); + cmdbuf.Dispatch(GroupCount(extent.width, OUTPUT_TILE_SHIFT), + GroupCount(extent.height, OUTPUT_TILE_SHIFT), 1); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_beta.h b/src/video_core/renderer_vulkan/present/lsfg_beta.h new file mode 100644 index 0000000000..1e24b235cf --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_beta.h @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr size_t LSFG_BETA_STAGES = 5; +constexpr size_t LSFG_BETA_OUTPUTS = 6; + +class LsfgBeta { +public: + LsfgBeta() = default; + LsfgBeta(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders, + LsfgResources& resources, vk::DescriptorPool& descriptor_pool, + LsfgImageHistory& inputs); + + void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count); + + [[nodiscard]] LsfgImage& Output(size_t level) { + return out_images[level]; + } + +private: + LsfgImageHistory* inputs{}; + + std::array<LsfgPass, LSFG_BETA_STAGES> passes; + std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> first_descriptor_sets{}; + std::array<VkDescriptorSet, LSFG_BETA_STAGES - 1> descriptor_sets{}; + vk::DescriptorSets owned_sets; + + LsfgImagePair temp1; + LsfgImagePair temp2; + std::array<LsfgImage, LSFG_BETA_OUTPUTS> out_images; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_chain.cpp b/src/video_core/renderer_vulkan/present/lsfg_chain.cpp new file mode 100644 index 0000000000..972b13a1c4 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_chain.cpp @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <algorithm> + +#include "video_core/renderer_vulkan/present/lsfg_chain.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 FIXED_DESCRIPTOR_SETS = 64; +constexpr u32 DESCRIPTOR_SETS_PER_SLOT = 112; +constexpr size_t FIRST_DELTA_LEVEL = 4; + +} // Anonymous namespace + +LsfgChain::LsfgChain(const Device& device, MemoryAllocator& memory_allocator, + const LsfgShaders& shaders, VkExtent2D extent, VkFormat format, + f32 flow_scale) + : resources{device, memory_allocator, flow_scale}, + descriptor_pool{CreateLsfgDescriptorPool( + device, FIXED_DESCRIPTOR_SETS + + DESCRIPTOR_SETS_PER_SLOT * static_cast<u32>(LSFG_GENERATION_SLOTS))} { + for (auto& image : frames) { + image = LsfgImage(device, memory_allocator, extent, format); + } + + mipmaps = LsfgMipmaps(device, memory_allocator, shaders, resources, descriptor_pool, frames, + flow_scale); + + alpha_passes = LsfgAlphaPasses(device, shaders); + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + alpha[i] = LsfgAlpha(device, memory_allocator, alpha_passes, resources, descriptor_pool, + mipmaps.Output(i)); + } + + beta = LsfgBeta(device, memory_allocator, shaders, resources, descriptor_pool, + alpha[0].Outputs()); + + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + const size_t level = LSFG_MIP_LEVELS - 1 - i; + gamma[i] = LsfgGamma(device, memory_allocator, shaders, resources, descriptor_pool, + alpha[level].Outputs(), + beta.Output(std::min(level, LSFG_BETA_OUTPUTS - 1)), + i == 0 ? nullptr : &gamma[i - 1].Output()); + + if (i < FIRST_DELTA_LEVEL) { + continue; + } + + const size_t index = i - FIRST_DELTA_LEVEL; + delta[index] = LsfgDelta( + device, memory_allocator, shaders, resources, descriptor_pool, alpha[level].Outputs(), + beta.Output(level), i == FIRST_DELTA_LEVEL ? nullptr : &gamma[i - 1].Output(), + i == FIRST_DELTA_LEVEL ? nullptr : &delta[index - 1].Output1(), + i == FIRST_DELTA_LEVEL ? nullptr : &delta[index - 1].Output2()); + } + + generate = LsfgGenerate(device, shaders, resources, descriptor_pool, frames, + gamma[LSFG_MIP_LEVELS - 1].Output(), + delta[LSFG_DELTA_INSTANCES - 1].Output1(), + delta[LSFG_DELTA_INSTANCES - 1].Output2()); +} + +void LsfgChain::DispatchShared(vk::CommandBuffer cmdbuf, u64 frame_count) { + mipmaps.Dispatch(cmdbuf, frame_count); + + for (size_t stage = 0; stage < LSFG_ALPHA_STAGES; ++stage) { + LsfgBarriers barriers(cmdbuf); + for (auto& level : alpha) { + level.PushBarriers(barriers, frame_count, stage); + } + barriers.Build(); + + alpha_passes.Get(stage).BindPipeline(cmdbuf); + for (auto& level : alpha) { + level.DispatchStage(cmdbuf, frame_count, stage); + } + } + + beta.Dispatch(cmdbuf, frame_count); +} + +void LsfgChain::DispatchGeneration(vk::CommandBuffer cmdbuf, u64 frame_count, + size_t generation_count, size_t generation, u32 target, + VkImage image, VkExtent2D extent) { + const size_t slot = LsfgGenerationSlot(generation_count, generation); + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + gamma[i].Dispatch(cmdbuf, frame_count, slot); + if (i >= FIRST_DELTA_LEVEL) { + delta[i - FIRST_DELTA_LEVEL].Dispatch(cmdbuf, frame_count, slot); + } + } + generate.Dispatch(cmdbuf, frame_count, slot, target, image, extent); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_chain.h b/src/video_core/renderer_vulkan/present/lsfg_chain.h new file mode 100644 index 0000000000..c8879f74a9 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_chain.h @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_alpha.h" +#include "video_core/renderer_vulkan/present/lsfg_beta.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" +#include "video_core/renderer_vulkan/present/lsfg_delta.h" +#include "video_core/renderer_vulkan/present/lsfg_gamma.h" +#include "video_core/renderer_vulkan/present/lsfg_generate.h" +#include "video_core/renderer_vulkan/present/lsfg_mipmaps.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr size_t LSFG_DELTA_INSTANCES = 3; + +class LsfgChain { +public: + LsfgChain(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders, + VkExtent2D extent, VkFormat format, f32 flow_scale); + + LsfgChain(const LsfgChain&) = delete; + LsfgChain& operator=(const LsfgChain&) = delete; + + void DispatchShared(vk::CommandBuffer cmdbuf, u64 frame_count); + + void DispatchGeneration(vk::CommandBuffer cmdbuf, u64 frame_count, size_t generation_count, + size_t generation, u32 target, VkImage image, VkExtent2D extent); + + void SetTarget(const Device& device, size_t generation_count, size_t generation, u32 target, + VkImageView view) { + generate.SetTarget(device, LsfgGenerationSlot(generation_count, generation), target, view); + } + + [[nodiscard]] LsfgImage& Input(u64 frame_count) { + return frames[frame_count % frames.size()]; + } + + [[nodiscard]] LsfgImage& FlowLevel(size_t level) { + return mipmaps.Output(level); + } + + [[nodiscard]] LsfgImage& AlphaOutput(size_t level, u64 frame_count, size_t index) { + return alpha[level].Outputs()[frame_count % LSFG_HISTORY_SLOTS][index]; + } + + [[nodiscard]] LsfgImage& BetaOutput(size_t level) { + return beta.Output(level); + } + + [[nodiscard]] LsfgImage& GammaOutput(size_t index) { + return gamma[index].Output(); + } + + [[nodiscard]] LsfgImage& DeltaOutput1(size_t index) { + return delta[index].Output1(); + } + + [[nodiscard]] LsfgImage& DeltaOutput2(size_t index) { + return delta[index].Output2(); + } + +private: + LsfgResources resources; + vk::DescriptorPool descriptor_pool; + + LsfgImagePair frames; + LsfgMipmaps mipmaps; + LsfgAlphaPasses alpha_passes; + std::array<LsfgAlpha, LSFG_MIP_LEVELS> alpha; + LsfgBeta beta; + std::array<LsfgGamma, LSFG_MIP_LEVELS> gamma; + std::array<LsfgDelta, LSFG_DELTA_INSTANCES> delta; + LsfgGenerate generate; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_common.cpp b/src/video_core/renderer_vulkan/present/lsfg_common.cpp new file mode 100644 index 0000000000..d2df2755e5 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_common.cpp @@ -0,0 +1,339 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <algorithm> +#include <cstring> + +#include "video_core/renderer_vulkan/present/lsfg_common.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DESCRIPTORS_PER_TYPE = 4096; + +struct LsfgConstants { + std::array<u32, 2> input_offset; + u32 first_iter; + u32 first_iter_s; + u32 advanced_color_kind; + u32 hdr_support; + f32 resolution_inv_scale; + f32 timestamp; + f32 ui_threshold; + std::array<u32, 3> padding; +}; +static_assert(sizeof(LsfgConstants) == 48); + +vk::Image CreateChainImage(MemoryAllocator& memory_allocator, VkExtent2D extent, VkFormat format) { + const VkImageCreateInfo image_ci{ + .sType = VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .imageType = VK_IMAGE_TYPE_2D, + .format = format, + .extent = {.width = extent.width, .height = extent.height, .depth = 1}, + .mipLevels = 1, + .arrayLayers = 1, + .samples = VK_SAMPLE_COUNT_1_BIT, + .tiling = VK_IMAGE_TILING_OPTIMAL, + .usage = VK_IMAGE_USAGE_TRANSFER_SRC_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT | + VK_IMAGE_USAGE_STORAGE_BIT | VK_IMAGE_USAGE_SAMPLED_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + .initialLayout = VK_IMAGE_LAYOUT_UNDEFINED, + }; + return memory_allocator.CreateImage(image_ci); +} + +vk::Buffer CreateUniformBuffer(MemoryAllocator& memory_allocator, VkDeviceSize size) { + const VkBufferCreateInfo buffer_ci{ + .sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .size = size, + .usage = VK_BUFFER_USAGE_UNIFORM_BUFFER_BIT | VK_BUFFER_USAGE_TRANSFER_DST_BIT, + .sharingMode = VK_SHARING_MODE_EXCLUSIVE, + .queueFamilyIndexCount = 0, + .pQueueFamilyIndices = nullptr, + }; + return memory_allocator.CreateBuffer(buffer_ci, MemoryUsage::Upload); +} + +VkImageMemoryBarrier MakeBarrier(const LsfgImage& image, VkAccessFlags src_access, + VkAccessFlags dst_access) { + return VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = src_access, + .dstAccessMask = dst_access, + .oldLayout = image.Layout(), + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image.Handle(), + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; +} + +} // Anonymous namespace + +LsfgImage::LsfgImage(const Device& device, MemoryAllocator& memory_allocator, VkExtent2D extent_, + VkFormat format_) + : extent{std::max(1u, extent_.width), std::max(1u, extent_.height)}, format{format_} { + image = CreateChainImage(memory_allocator, extent, format); + view = CreateWrappedImageView(device, image, format); +} + +LsfgBarriers& LsfgBarriers::Push(LsfgImage& image, VkAccessFlags src_access, + VkAccessFlags dst_access) { + barriers.push_back(MakeBarrier(image, src_access, dst_access)); + image.SetLayout(VK_IMAGE_LAYOUT_GENERAL); + return *this; +} + +LsfgBarriers& LsfgBarriers::WriteToRead(LsfgImage& image) { + return Push(image, VK_ACCESS_SHADER_WRITE_BIT, VK_ACCESS_SHADER_READ_BIT); +} + +LsfgBarriers& LsfgBarriers::ReadToWrite(LsfgImage& image) { + return Push(image, VK_ACCESS_SHADER_READ_BIT, VK_ACCESS_SHADER_WRITE_BIT); +} + +LsfgBarriers& LsfgBarriers::WriteToRead(LsfgImage* image) { + return image == nullptr ? *this : WriteToRead(*image); +} + +LsfgBarriers& LsfgBarriers::ReadToWrite(LsfgImage* image) { + return image == nullptr ? *this : ReadToWrite(*image); +} + +LsfgBarriers& LsfgBarriers::DiscardToWrite(VkImage image) { + barriers.push_back(VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = 0, + .dstAccessMask = VK_ACCESS_SHADER_WRITE_BIT, + .oldLayout = VK_IMAGE_LAYOUT_UNDEFINED, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }); + return *this; +} + +VkDeviceSize LsfgResources::BufferSize() { + return sizeof(LsfgConstants); +} + +VkSampler LsfgResources::GetSampler(VkSamplerAddressMode address_mode, VkCompareOp compare_op, + bool white_border) { + const u64 key = static_cast<u64>(address_mode) | (static_cast<u64>(compare_op) << 8) | + (static_cast<u64>(white_border) << 16); + + const auto it = samplers.find(key); + if (it != samplers.end()) { + return *it->second; + } + + const auto [entry, inserted] = + samplers.emplace(key, CreateLsfgSampler(*device, address_mode, compare_op, white_border)); + return *entry->second; +} + +VkBuffer LsfgResources::GetBuffer(f32 timestamp, bool first_iter, bool first_iter_s) { + u32 timestamp_bits{}; + std::memcpy(×tamp_bits, ×tamp, sizeof(timestamp_bits)); + const u64 key = static_cast<u64>(timestamp_bits) | (static_cast<u64>(first_iter) << 32) | + (static_cast<u64>(first_iter_s) << 33); + + const auto it = buffers.find(key); + if (it != buffers.end()) { + return *it->second; + } + + vk::Buffer buffer = CreateUniformBuffer(*memory_allocator, sizeof(LsfgConstants)); + + const LsfgConstants constants{ + .input_offset = {0, 0}, + .first_iter = first_iter ? 1u : 0u, + .first_iter_s = first_iter_s ? 1u : 0u, + .advanced_color_kind = 0, + .hdr_support = 0, + .resolution_inv_scale = 1.0f / flow_scale, + .timestamp = timestamp, + .ui_threshold = 0.5f, + .padding = {0, 0, 0}, + }; + + const std::span<u8> mapped = buffer.Mapped(); + std::memcpy(mapped.data(), &constants, sizeof(constants)); + buffer.Flush(); + + const auto [entry, inserted] = buffers.emplace(key, std::move(buffer)); + return *entry->second; +} + +void LsfgBarriers::Build() { + if (barriers.empty()) { + return; + } + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, 0, {}, {}, barriers); + barriers.clear(); +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::PushImage(VkDescriptorType type, VkSampler sampler, + VkImageView view) { + image_infos.push_back(VkDescriptorImageInfo{ + .sampler = sampler, + .imageView = view, + .imageLayout = view == VK_NULL_HANDLE ? VK_IMAGE_LAYOUT_UNDEFINED + : VK_IMAGE_LAYOUT_GENERAL, + }); + writes.push_back(VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .pNext = nullptr, + .dstSet = set, + .dstBinding = binding++, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = type, + .pImageInfo = &image_infos.back(), + .pBufferInfo = nullptr, + .pTexelBufferView = nullptr, + }); + return *this; +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::AddSampler(VkSampler sampler) { + return PushImage(VK_DESCRIPTOR_TYPE_SAMPLER, sampler, VK_NULL_HANDLE); +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::AddSampledImage(const LsfgImage& image) { + return PushImage(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_NULL_HANDLE, image.View()); +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::AddSampledImage(const LsfgImage* image) { + return PushImage(VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_NULL_HANDLE, + image == nullptr ? VK_NULL_HANDLE : image->View()); +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::AddStorageImage(const LsfgImage& image) { + return PushImage(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_NULL_HANDLE, image.View()); +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::AddStorageView(VkImageView view) { + return PushImage(VK_DESCRIPTOR_TYPE_STORAGE_IMAGE, VK_NULL_HANDLE, view); +} + +LsfgDescriptorWriter& LsfgDescriptorWriter::AddUniformBuffer(VkBuffer buffer, VkDeviceSize size) { + buffer_infos.push_back(VkDescriptorBufferInfo{ + .buffer = buffer, + .offset = 0, + .range = size, + }); + writes.push_back(VkWriteDescriptorSet{ + .sType = VK_STRUCTURE_TYPE_WRITE_DESCRIPTOR_SET, + .pNext = nullptr, + .dstSet = set, + .dstBinding = binding++, + .dstArrayElement = 0, + .descriptorCount = 1, + .descriptorType = VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, + .pImageInfo = nullptr, + .pBufferInfo = &buffer_infos.back(), + .pTexelBufferView = nullptr, + }); + return *this; +} + +void LsfgDescriptorWriter::Build(const Device& device) { + if (writes.empty()) { + return; + } + device.GetLogical().UpdateDescriptorSets(writes, {}); + writes.clear(); +} + +LsfgPass::LsfgPass(const Device& device, const LsfgShaders& shaders, u32 shader_id, + LsfgBindings bindings) { + std::vector<VkDescriptorType> types; + for (const auto& [count, type] : bindings) { + types.insert(types.end(), count, type); + } + descriptor_count = static_cast<u32>(types.size()); + + descriptor_set_layout = CreateWrappedDescriptorSetLayout( + device, std::span<const VkDescriptorType>{types}, VK_SHADER_STAGE_COMPUTE_BIT); + pipeline_layout = CreateWrappedPipelineLayout(device, descriptor_set_layout); + pipeline = CreateWrappedComputePipeline(device, pipeline_layout, shaders.Get(shader_id)); +} + +void LsfgPass::Bind(vk::CommandBuffer cmdbuf, VkDescriptorSet set) const { + BindPipeline(cmdbuf); + BindSet(cmdbuf, set); +} + +void LsfgPass::BindPipeline(vk::CommandBuffer cmdbuf) const { + cmdbuf.BindPipeline(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline); +} + +void LsfgPass::BindSet(vk::CommandBuffer cmdbuf, VkDescriptorSet set) const { + cmdbuf.BindDescriptorSets(VK_PIPELINE_BIND_POINT_COMPUTE, *pipeline_layout, 0, set, {}); +} + +vk::DescriptorPool CreateLsfgDescriptorPool(const Device& device, u32 max_sets) { + return CreateWrappedDescriptorPool( + device, DESCRIPTORS_PER_TYPE, max_sets, + {VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER, VK_DESCRIPTOR_TYPE_SAMPLER, + VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}); +} + +vk::Sampler CreateLsfgSampler(const Device& device, VkSamplerAddressMode address_mode, + VkCompareOp compare_op, bool white_border) { + return device.GetLogical().CreateSampler(VkSamplerCreateInfo{ + .sType = VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .magFilter = VK_FILTER_LINEAR, + .minFilter = VK_FILTER_LINEAR, + .mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR, + .addressModeU = address_mode, + .addressModeV = address_mode, + .addressModeW = address_mode, + .mipLodBias = 0.0f, + .anisotropyEnable = VK_FALSE, + .maxAnisotropy = 0.0f, + .compareEnable = VK_FALSE, + .compareOp = compare_op, + .minLod = 0.0f, + .maxLod = VK_LOD_CLAMP_NONE, + .borderColor = white_border ? VK_BORDER_COLOR_FLOAT_OPAQUE_WHITE + : VK_BORDER_COLOR_FLOAT_TRANSPARENT_BLACK, + .unnormalizedCoordinates = VK_FALSE, + }); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_common.h b/src/video_core/renderer_vulkan/present/lsfg_common.h new file mode 100644 index 0000000000..d1e3271a6d --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_common.h @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> +#include <deque> +#include <initializer_list> +#include <map> +#include <utility> +#include <vector> + +#include "common/common_types.h" +#include "video_core/vulkan_common/vulkan_memory_allocator.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr VkFormat LSFG_DEFAULT_FORMAT = VK_FORMAT_R8G8B8A8_UNORM; +constexpr VkFormat LSFG_FLOW_FORMAT = VK_FORMAT_R8_UNORM; +constexpr VkFormat LSFG_MOTION_FORMAT = VK_FORMAT_R16G16B16A16_SFLOAT; + +constexpr size_t LSFG_HISTORY_SLOTS = 3; +constexpr size_t LSFG_MAX_TARGETS = 7; +constexpr size_t LSFG_MAX_GENERATIONS = 3; + +constexpr size_t LSFG_GENERATION_SLOTS = LSFG_MAX_GENERATIONS * (LSFG_MAX_GENERATIONS + 1) / 2; + +[[nodiscard]] constexpr size_t LsfgGenerationSlot(size_t generation_count, size_t generation) { + return (generation_count - 1) * generation_count / 2 + generation; +} + +[[nodiscard]] constexpr f32 LsfgTimestamp(size_t generation, size_t generation_count) { + return static_cast<f32>(generation + 1) / static_cast<f32>(generation_count + 1); +} + +[[nodiscard]] constexpr size_t LsfgSlotCount(size_t slot) { + size_t count = 1; + while (LsfgGenerationSlot(count + 1, 0) <= slot) { + ++count; + } + return count; +} + +[[nodiscard]] constexpr f32 LsfgSlotTimestamp(size_t slot) { + const size_t count = LsfgSlotCount(slot); + return LsfgTimestamp(slot - LsfgGenerationSlot(count, 0), count); +} + +class LsfgImage { +public: + LsfgImage() = default; + LsfgImage(const Device& device, MemoryAllocator& memory_allocator, VkExtent2D extent_, + VkFormat format = LSFG_DEFAULT_FORMAT); + + [[nodiscard]] VkImage Handle() const { + return *image; + } + + [[nodiscard]] VkImageView View() const { + return *view; + } + + [[nodiscard]] VkExtent2D Extent() const { + return extent; + } + + [[nodiscard]] VkFormat Format() const { + return format; + } + + [[nodiscard]] VkImageLayout Layout() const { + return layout; + } + + void SetLayout(VkImageLayout new_layout) { + layout = new_layout; + } + +private: + vk::Image image; + vk::ImageView view; + VkExtent2D extent{}; + VkFormat format{VK_FORMAT_UNDEFINED}; + VkImageLayout layout{VK_IMAGE_LAYOUT_UNDEFINED}; +}; + +using LsfgImagePair = std::array<LsfgImage, 2>; +using LsfgImageHistory = std::array<LsfgImagePair, LSFG_HISTORY_SLOTS>; + +class LsfgResources { +public: + LsfgResources() = default; + LsfgResources(const Device& device_, MemoryAllocator& memory_allocator_, f32 flow_scale_) + : device{&device_}, memory_allocator{&memory_allocator_}, flow_scale{flow_scale_} {} + + [[nodiscard]] VkSampler GetSampler( + VkSamplerAddressMode address_mode = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, + VkCompareOp compare_op = VK_COMPARE_OP_NEVER, bool white_border = false); + + [[nodiscard]] VkBuffer GetBuffer(f32 timestamp = 0.0f, bool first_iter = false, + bool first_iter_s = false); + + [[nodiscard]] static VkDeviceSize BufferSize(); + +private: + const Device* device{}; + MemoryAllocator* memory_allocator{}; + f32 flow_scale{1.0f}; + + std::map<u64, vk::Sampler> samplers; + std::map<u64, vk::Buffer> buffers; +}; + +class LsfgBarriers { +public: + explicit LsfgBarriers(vk::CommandBuffer cmdbuf_) : cmdbuf{cmdbuf_} {} + + LsfgBarriers& WriteToRead(LsfgImage& image); + LsfgBarriers& ReadToWrite(LsfgImage& image); + LsfgBarriers& WriteToRead(LsfgImage* image); + LsfgBarriers& ReadToWrite(LsfgImage* image); + LsfgBarriers& DiscardToWrite(VkImage image); + + template <typename Range> + LsfgBarriers& WriteToReadAll(Range& images) { + for (auto& image : images) { + WriteToRead(image); + } + return *this; + } + + template <typename Range> + LsfgBarriers& ReadToWriteAll(Range& images) { + for (auto& image : images) { + ReadToWrite(image); + } + return *this; + } + + void Build(); + +private: + LsfgBarriers& Push(LsfgImage& image, VkAccessFlags src_access, VkAccessFlags dst_access); + + vk::CommandBuffer cmdbuf; + std::vector<VkImageMemoryBarrier> barriers; +}; + +class LsfgDescriptorWriter { +public: + explicit LsfgDescriptorWriter(VkDescriptorSet set_) : set{set_} {} + + LsfgDescriptorWriter& AddSampler(VkSampler sampler); + LsfgDescriptorWriter& AddSampledImage(const LsfgImage& image); + LsfgDescriptorWriter& AddSampledImage(const LsfgImage* image); + LsfgDescriptorWriter& AddStorageImage(const LsfgImage& image); + LsfgDescriptorWriter& AddStorageView(VkImageView view); + LsfgDescriptorWriter& AddUniformBuffer(VkBuffer buffer, VkDeviceSize size); + + template <typename Range> + LsfgDescriptorWriter& AddSampledImages(const Range& images) { + for (const auto& image : images) { + AddSampledImage(image); + } + return *this; + } + + template <typename Range> + LsfgDescriptorWriter& AddStorageImages(const Range& images) { + for (const auto& image : images) { + AddStorageImage(image); + } + return *this; + } + + void Build(const Device& device); + +private: + LsfgDescriptorWriter& PushImage(VkDescriptorType type, VkSampler sampler, VkImageView view); + + VkDescriptorSet set; + u32 binding{}; + std::deque<VkDescriptorImageInfo> image_infos; + std::deque<VkDescriptorBufferInfo> buffer_infos; + std::vector<VkWriteDescriptorSet> writes; +}; + +using LsfgBindings = std::initializer_list<std::pair<u32, VkDescriptorType>>; + +class LsfgPass { +public: + LsfgPass() = default; + LsfgPass(const Device& device, const LsfgShaders& shaders, u32 shader_id, + LsfgBindings bindings); + + [[nodiscard]] VkDescriptorSetLayout SetLayout() const { + return *descriptor_set_layout; + } + + [[nodiscard]] u32 DescriptorCount() const { + return descriptor_count; + } + + void Bind(vk::CommandBuffer cmdbuf, VkDescriptorSet set) const; + void BindPipeline(vk::CommandBuffer cmdbuf) const; + void BindSet(vk::CommandBuffer cmdbuf, VkDescriptorSet set) const; + +private: + vk::DescriptorSetLayout descriptor_set_layout; + vk::PipelineLayout pipeline_layout; + vk::Pipeline pipeline; + u32 descriptor_count{}; +}; + +[[nodiscard]] vk::DescriptorPool CreateLsfgDescriptorPool(const Device& device, u32 max_sets); + +[[nodiscard]] vk::Sampler CreateLsfgSampler(const Device& device, VkSamplerAddressMode address_mode, + VkCompareOp compare_op, bool white_border); + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_delta.cpp b/src/video_core/renderer_vulkan/present/lsfg_delta.cpp new file mode 100644 index 0000000000..dc6eec383f --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_delta.cpp @@ -0,0 +1,283 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <vector> + +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_delta.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DISPATCH_TILE_SHIFT = 3; + +[[nodiscard]] u32 GroupCount(u32 size) { + return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT; +} + +} // Anonymous namespace + +LsfgDelta::LsfgDelta(const Device& device, MemoryAllocator& memory_allocator, + const LsfgShaders& shaders, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImageHistory& inputs_, + LsfgImage& flow_input_, LsfgImage* previous_gamma_, LsfgImage* previous1_, + LsfgImage* previous2_) + : inputs{&inputs_}, flow_input{&flow_input_}, previous_gamma{previous_gamma_}, + previous1{previous1_}, previous2{previous2_} { + using namespace VideoCore::FrameGen::PerformanceShader; + + passes[0] = LsfgPass(device, shaders, DELTA[0], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {5, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {3, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[1] = LsfgPass(device, shaders, DELTA[1], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {3, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[2] = LsfgPass(device, shaders, DELTA[2], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[3] = LsfgPass(device, shaders, DELTA[3], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[4] = LsfgPass(device, shaders, DELTA[4], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {4, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[5] = LsfgPass(device, shaders, DELTA[5], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {6, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + for (size_t i = 6; i < LSFG_DELTA_STAGES - 1; ++i) { + passes[i] = LsfgPass(device, shaders, DELTA[i], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + } + passes[9] = LsfgPass(device, shaders, DELTA[9], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + + const VkExtent2D extent = (*inputs)[0][0].Extent(); + for (auto& image : temp1) { + image = LsfgImage(device, memory_allocator, extent); + } + for (auto& image : temp2) { + image = LsfgImage(device, memory_allocator, extent); + } + out_image1 = LsfgImage(device, memory_allocator, extent, LSFG_MOTION_FORMAT); + out_image2 = LsfgImage(device, memory_allocator, extent, LSFG_MOTION_FORMAT); + + std::vector<VkDescriptorSetLayout> layouts; + for (size_t slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + layouts.push_back(passes[0].SetLayout()); + } + for (size_t i = 1; i <= 4; ++i) { + layouts.push_back(passes[i].SetLayout()); + } + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + layouts.push_back(passes[5].SetLayout()); + } + for (size_t i = 6; i < LSFG_DELTA_STAGES; ++i) { + layouts.push_back(passes[i].SetLayout()); + } + } + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + const VkSampler sampler = resources.GetSampler(); + const VkSampler border_sampler = resources.GetSampler( + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, VK_COMPARE_OP_NEVER, true); + const VkSampler edge_sampler = + resources.GetSampler(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VK_COMPARE_OP_ALWAYS, false); + size_t next = 0; + for (size_t slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + Generation& pass = generations[slot]; + const VkBuffer buffer = + resources.GetBuffer(LsfgSlotTimestamp(slot), false, previous_gamma == nullptr); + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + pass.first_descriptor_sets[i] = owned_sets[next++]; + } + for (size_t i = 0; i < 4; ++i) { + pass.descriptor_sets[i] = owned_sets[next++]; + } + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + pass.sixth_descriptor_sets[i] = owned_sets[next++]; + } + for (size_t i = 4; i < LSFG_DELTA_STAGES - 2; ++i) { + pass.descriptor_sets[i] = owned_sets[next++]; + } + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + LsfgDescriptorWriter(pass.first_descriptor_sets[i]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(border_sampler) + .AddSampler(edge_sampler) + .AddSampledImages((*inputs)[(i + 2) % LSFG_HISTORY_SLOTS]) + .AddSampledImages((*inputs)[i % LSFG_HISTORY_SLOTS]) + .AddSampledImage(previous_gamma) + .AddStorageImages(temp1) + .Build(device); + LsfgDescriptorWriter(pass.sixth_descriptor_sets[i]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(border_sampler) + .AddSampler(edge_sampler) + .AddSampledImages((*inputs)[(i + 2) % LSFG_HISTORY_SLOTS]) + .AddSampledImages((*inputs)[i % LSFG_HISTORY_SLOTS]) + .AddSampledImage(previous_gamma) + .AddSampledImage(previous1) + .AddStorageImage(temp2[0]) + .Build(device); + } + LsfgDescriptorWriter(pass.descriptor_sets[0]) + .AddSampler(sampler) + .AddSampledImages(temp1) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[1]) + .AddSampler(sampler) + .AddSampledImages(temp2) + .AddStorageImage(temp1[0]) + .AddStorageImage(temp1[1]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[2]) + .AddSampler(sampler) + .AddSampledImage(temp1[0]) + .AddSampledImage(temp1[1]) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[3]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampler(edge_sampler) + .AddSampledImages(temp2) + .AddSampledImage(previous_gamma) + .AddSampledImage(*flow_input) + .AddStorageImage(out_image1) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[4]) + .AddSampler(sampler) + .AddSampledImage(temp2[0]) + .AddStorageImage(temp1[0]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[5]) + .AddSampler(sampler) + .AddSampledImage(temp1[0]) + .AddStorageImage(temp2[0]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[6]) + .AddSampler(sampler) + .AddSampledImage(temp2[0]) + .AddStorageImage(temp1[0]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[7]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampler(edge_sampler) + .AddSampledImage(temp1[0]) + .AddSampledImage(previous2) + .AddStorageImage(out_image2) + .Build(device); + } +} + +void LsfgDelta::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot) { + const Generation& pass = generations[slot]; + + const VkExtent2D extent = temp1[0].Extent(); + const u32 groups_x = GroupCount(extent.width); + const u32 groups_y = GroupCount(extent.height); + + const size_t history = frame_count % LSFG_HISTORY_SLOTS; + const size_t previous_history = (frame_count + 2) % LSFG_HISTORY_SLOTS; + + LsfgBarriers(cmdbuf) + .WriteToReadAll((*inputs)[previous_history]) + .WriteToReadAll((*inputs)[history]) + .WriteToRead(previous_gamma) + .ReadToWriteAll(temp1) + .Build(); + passes[0].Bind(cmdbuf, pass.first_descriptor_sets[history]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build(); + passes[1].Bind(cmdbuf, pass.descriptor_sets[0]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp2).ReadToWriteAll(temp1).Build(); + passes[2].Bind(cmdbuf, pass.descriptor_sets[1]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build(); + passes[3].Bind(cmdbuf, pass.descriptor_sets[2]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .WriteToRead(previous_gamma) + .WriteToRead(*flow_input) + .ReadToWrite(out_image1) + .Build(); + passes[4].Bind(cmdbuf, pass.descriptor_sets[3]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll((*inputs)[previous_history]) + .WriteToReadAll((*inputs)[history]) + .WriteToRead(previous_gamma) + .WriteToRead(previous1) + .ReadToWriteAll(temp2) + .Build(); + passes[5].Bind(cmdbuf, pass.sixth_descriptor_sets[history]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .ReadToWrite(temp1[0]) + .ReadToWrite(temp1[1]) + .Build(); + passes[6].Bind(cmdbuf, pass.descriptor_sets[4]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToRead(temp1[0]) + .WriteToRead(temp1[1]) + .ReadToWriteAll(temp2) + .Build(); + passes[7].Bind(cmdbuf, pass.descriptor_sets[5]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .ReadToWrite(temp1[0]) + .ReadToWrite(temp1[1]) + .Build(); + passes[8].Bind(cmdbuf, pass.descriptor_sets[6]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToRead(temp1[0]) + .WriteToRead(temp1[1]) + .WriteToRead(previous2) + .ReadToWrite(out_image2) + .Build(); + passes[9].Bind(cmdbuf, pass.descriptor_sets[7]); + cmdbuf.Dispatch(groups_x, groups_y, 1); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_delta.h b/src/video_core/renderer_vulkan/present/lsfg_delta.h new file mode 100644 index 0000000000..1bbcfe6745 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_delta.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr size_t LSFG_DELTA_STAGES = 10; +constexpr size_t LSFG_DELTA_TEMPS = 3; + +class LsfgDelta { +public: + LsfgDelta() = default; + LsfgDelta(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders, + LsfgResources& resources, vk::DescriptorPool& descriptor_pool, + LsfgImageHistory& inputs, LsfgImage& flow_input, LsfgImage* previous_gamma, + LsfgImage* previous1, LsfgImage* previous2); + + void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot); + + [[nodiscard]] LsfgImage& Output1() { + return out_image1; + } + + [[nodiscard]] LsfgImage& Output2() { + return out_image2; + } + +private: + struct Generation { + std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> first_descriptor_sets{}; + std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> sixth_descriptor_sets{}; + std::array<VkDescriptorSet, LSFG_DELTA_STAGES - 2> descriptor_sets{}; + }; + + LsfgImageHistory* inputs{}; + LsfgImage* flow_input{}; + LsfgImage* previous_gamma{}; + LsfgImage* previous1{}; + LsfgImage* previous2{}; + + std::array<LsfgPass, LSFG_DELTA_STAGES> passes; + std::array<Generation, LSFG_GENERATION_SLOTS> generations{}; + vk::DescriptorSets owned_sets; + + std::array<LsfgImage, LSFG_DELTA_TEMPS> temp1; + LsfgImagePair temp2; + LsfgImage out_image1; + LsfgImage out_image2; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_gamma.cpp b/src/video_core/renderer_vulkan/present/lsfg_gamma.cpp new file mode 100644 index 0000000000..067d124b6f --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_gamma.cpp @@ -0,0 +1,185 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <vector> + +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_gamma.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DISPATCH_TILE_SHIFT = 3; + +[[nodiscard]] u32 GroupCount(u32 size) { + return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT; +} + +} // Anonymous namespace + +LsfgGamma::LsfgGamma(const Device& device, MemoryAllocator& memory_allocator, + const LsfgShaders& shaders, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImageHistory& inputs_, + LsfgImage& flow_input_, LsfgImage* previous_) + : inputs{&inputs_}, flow_input{&flow_input_}, previous{previous_} { + using namespace VideoCore::FrameGen::PerformanceShader; + + passes[0] = LsfgPass(device, shaders, GAMMA[0], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {5, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {3, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[1] = LsfgPass(device, shaders, GAMMA[1], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {3, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[2] = LsfgPass(device, shaders, GAMMA[2], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[3] = LsfgPass(device, shaders, GAMMA[3], + {{1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {2, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + passes[4] = LsfgPass(device, shaders, GAMMA[4], + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {4, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + + const VkExtent2D extent = (*inputs)[0][0].Extent(); + for (auto& image : temp1) { + image = LsfgImage(device, memory_allocator, extent); + } + for (auto& image : temp2) { + image = LsfgImage(device, memory_allocator, extent); + } + out_image = LsfgImage(device, memory_allocator, extent, LSFG_MOTION_FORMAT); + + std::vector<VkDescriptorSetLayout> layouts; + for (size_t slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + layouts.push_back(passes[0].SetLayout()); + } + for (size_t i = 1; i < LSFG_GAMMA_STAGES; ++i) { + layouts.push_back(passes[i].SetLayout()); + } + } + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + const VkSampler sampler = resources.GetSampler(); + const VkSampler border_sampler = resources.GetSampler( + VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_BORDER, VK_COMPARE_OP_NEVER, true); + const VkSampler edge_sampler = + resources.GetSampler(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VK_COMPARE_OP_ALWAYS, false); + + size_t next = 0; + for (size_t slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + Generation& pass = generations[slot]; + const VkBuffer buffer = + resources.GetBuffer(LsfgSlotTimestamp(slot), previous == nullptr); + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + pass.first_descriptor_sets[i] = owned_sets[next++]; + } + for (size_t i = 0; i < LSFG_GAMMA_STAGES - 1; ++i) { + pass.descriptor_sets[i] = owned_sets[next++]; + } + + for (size_t i = 0; i < LSFG_HISTORY_SLOTS; ++i) { + LsfgDescriptorWriter(pass.first_descriptor_sets[i]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(border_sampler) + .AddSampler(edge_sampler) + .AddSampledImages((*inputs)[(i + 2) % LSFG_HISTORY_SLOTS]) + .AddSampledImages((*inputs)[i % LSFG_HISTORY_SLOTS]) + .AddSampledImage(previous) + .AddStorageImages(temp1) + .Build(device); + } + LsfgDescriptorWriter(pass.descriptor_sets[0]) + .AddSampler(sampler) + .AddSampledImages(temp1) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[1]) + .AddSampler(sampler) + .AddSampledImages(temp2) + .AddStorageImage(temp1[0]) + .AddStorageImage(temp1[1]) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[2]) + .AddSampler(sampler) + .AddSampledImage(temp1[0]) + .AddSampledImage(temp1[1]) + .AddStorageImages(temp2) + .Build(device); + LsfgDescriptorWriter(pass.descriptor_sets[3]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampler(edge_sampler) + .AddSampledImages(temp2) + .AddSampledImage(previous) + .AddSampledImage(*flow_input) + .AddStorageImage(out_image) + .Build(device); + } +} + +void LsfgGamma::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot) { + const Generation& pass = generations[slot]; + + const VkExtent2D extent = temp1[0].Extent(); + const u32 groups_x = GroupCount(extent.width); + const u32 groups_y = GroupCount(extent.height); + + const size_t history = frame_count % LSFG_HISTORY_SLOTS; + const size_t previous_history = (frame_count + 2) % LSFG_HISTORY_SLOTS; + + LsfgBarriers(cmdbuf) + .WriteToReadAll((*inputs)[previous_history]) + .WriteToReadAll((*inputs)[history]) + .WriteToRead(previous) + .ReadToWriteAll(temp1) + .Build(); + passes[0].Bind(cmdbuf, pass.first_descriptor_sets[history]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf).WriteToReadAll(temp1).ReadToWriteAll(temp2).Build(); + passes[1].Bind(cmdbuf, pass.descriptor_sets[0]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .ReadToWrite(temp1[0]) + .ReadToWrite(temp1[1]) + .Build(); + passes[2].Bind(cmdbuf, pass.descriptor_sets[1]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToRead(temp1[0]) + .WriteToRead(temp1[1]) + .ReadToWriteAll(temp2) + .Build(); + passes[3].Bind(cmdbuf, pass.descriptor_sets[2]); + cmdbuf.Dispatch(groups_x, groups_y, 1); + + LsfgBarriers(cmdbuf) + .WriteToReadAll(temp2) + .WriteToRead(previous) + .WriteToRead(*flow_input) + .ReadToWrite(out_image) + .Build(); + passes[4].Bind(cmdbuf, pass.descriptor_sets[3]); + cmdbuf.Dispatch(groups_x, groups_y, 1); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_gamma.h b/src/video_core/renderer_vulkan/present/lsfg_gamma.h new file mode 100644 index 0000000000..848c380bfe --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_gamma.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr size_t LSFG_GAMMA_STAGES = 5; +constexpr size_t LSFG_GAMMA_TEMPS = 3; + +class LsfgGamma { +public: + LsfgGamma() = default; + LsfgGamma(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders, + LsfgResources& resources, vk::DescriptorPool& descriptor_pool, + LsfgImageHistory& inputs, LsfgImage& flow_input, LsfgImage* previous); + + void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot); + + [[nodiscard]] LsfgImage& Output() { + return out_image; + } + +private: + struct Generation { + std::array<VkDescriptorSet, LSFG_HISTORY_SLOTS> first_descriptor_sets{}; + std::array<VkDescriptorSet, LSFG_GAMMA_STAGES - 1> descriptor_sets{}; + }; + + LsfgImageHistory* inputs{}; + LsfgImage* flow_input{}; + LsfgImage* previous{}; + + std::array<LsfgPass, LSFG_GAMMA_STAGES> passes; + std::array<Generation, LSFG_GENERATION_SLOTS> generations{}; + vk::DescriptorSets owned_sets; + + std::array<LsfgImage, LSFG_GAMMA_TEMPS> temp1; + LsfgImagePair temp2; + LsfgImage out_image; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_generate.cpp b/src/video_core/renderer_vulkan/present/lsfg_generate.cpp new file mode 100644 index 0000000000..4473b59a2f --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_generate.cpp @@ -0,0 +1,130 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <vector> + +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_generate.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DISPATCH_TILE_SHIFT = 4; + +[[nodiscard]] u32 GroupCount(u32 size) { + return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT; +} + +VkImageMemoryBarrier MakeTargetBarrier(VkImage image, VkAccessFlags src_access, + VkAccessFlags dst_access, VkImageLayout old_layout) { + return VkImageMemoryBarrier{ + .sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER, + .pNext = nullptr, + .srcAccessMask = src_access, + .dstAccessMask = dst_access, + .oldLayout = old_layout, + .newLayout = VK_IMAGE_LAYOUT_GENERAL, + .srcQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED, + .image = image, + .subresourceRange{ + .aspectMask = VK_IMAGE_ASPECT_COLOR_BIT, + .baseMipLevel = 0, + .levelCount = 1, + .baseArrayLayer = 0, + .layerCount = 1, + }, + }; +} + +} // Anonymous namespace + +LsfgGenerate::LsfgGenerate(const Device& device, const LsfgShaders& shaders, + LsfgResources& resources, vk::DescriptorPool& descriptor_pool, + LsfgImagePair& frames_, LsfgImage& motion_, LsfgImage& detail1_, + LsfgImage& detail2_) + : frames{&frames_}, motion{&motion_}, detail1{&detail1_}, detail2{&detail2_} { + using namespace VideoCore::FrameGen::PerformanceShader; + + pass = LsfgPass(device, shaders, GENERATE, + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {2, VK_DESCRIPTOR_TYPE_SAMPLER}, + {5, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {1, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + + sampler = resources.GetSampler(); + edge_sampler = + resources.GetSampler(VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE, VK_COMPARE_OP_ALWAYS, false); + + const std::vector<VkDescriptorSetLayout> layouts( + LSFG_GENERATION_SLOTS * LSFG_MAX_TARGETS * 2, pass.SetLayout()); + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + size_t next = 0; + for (size_t slot = 0; slot < LSFG_GENERATION_SLOTS; ++slot) { + Generation& target = generations[slot]; + target.buffer = resources.GetBuffer(LsfgSlotTimestamp(slot)); + + for (auto& entry : target.targets) { + for (auto& set : entry.descriptor_sets) { + set = owned_sets[next++]; + } + } + } +} + +void LsfgGenerate::SetTarget(const Device& device, size_t slot, u32 target, VkImageView view) { + Target& entry = generations[slot].targets[target]; + if (entry.view == view) { + return; + } + entry.view = view; + + for (size_t i = 0; i < entry.descriptor_sets.size(); ++i) { + LsfgDescriptorWriter(entry.descriptor_sets[i]) + .AddUniformBuffer(generations[slot].buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampler(edge_sampler) + .AddSampledImage((*frames)[1 - i]) + .AddSampledImage((*frames)[i]) + .AddSampledImage(*motion) + .AddSampledImage(*detail1) + .AddSampledImage(*detail2) + .AddStorageView(view) + .Build(device); + } +} + +void LsfgGenerate::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot, u32 target, + VkImage image, VkExtent2D extent) { + const Target& entry = generations[slot].targets[target]; + + LsfgBarriers(cmdbuf) + .WriteToReadAll(*frames) + .WriteToRead(*motion) + .WriteToRead(*detail1) + .WriteToRead(*detail2) + .DiscardToWrite(image) + .Build(); + + pass.Bind(cmdbuf, entry.descriptor_sets[frame_count % entry.descriptor_sets.size()]); + cmdbuf.Dispatch(GroupCount(extent.width), GroupCount(extent.height), 1); + + const std::array after{MakeTargetBarrier( + image, VK_ACCESS_SHADER_WRITE_BIT, + VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT | VK_ACCESS_TRANSFER_READ_BIT, + VK_IMAGE_LAYOUT_GENERAL)}; + cmdbuf.PipelineBarrier(VK_PIPELINE_STAGE_COMPUTE_SHADER_BIT, + VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT | + VK_PIPELINE_STAGE_TRANSFER_BIT, + 0, {}, {}, after); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_generate.h b/src/video_core/renderer_vulkan/present/lsfg_generate.h new file mode 100644 index 0000000000..75cbd8d2e4 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_generate.h @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +class LsfgGenerate { +public: + LsfgGenerate() = default; + LsfgGenerate(const Device& device, const LsfgShaders& shaders, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImagePair& frames, LsfgImage& motion, + LsfgImage& detail1, LsfgImage& detail2); + + void SetTarget(const Device& device, size_t slot, u32 target, VkImageView view); + + void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count, size_t slot, u32 target, + VkImage image, VkExtent2D extent); + +private: + struct Target { + std::array<VkDescriptorSet, 2> descriptor_sets{}; + VkImageView view{}; + }; + + struct Generation { + std::array<Target, LSFG_MAX_TARGETS> targets{}; + VkBuffer buffer{}; + }; + + LsfgImagePair* frames{}; + LsfgImage* motion{}; + LsfgImage* detail1{}; + LsfgImage* detail2{}; + VkSampler sampler{}; + VkSampler edge_sampler{}; + + LsfgPass pass; + std::array<Generation, LSFG_GENERATION_SLOTS> generations{}; + vk::DescriptorSets owned_sets; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_mipmaps.cpp b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.cpp new file mode 100644 index 0000000000..3f31f58d68 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.cpp @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#include <algorithm> +#include <vector> + +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_mipmaps.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +namespace { + +constexpr u32 DISPATCH_TILE_SHIFT = 6; + +[[nodiscard]] u32 GroupCount(u32 size) { + return (size + (1u << DISPATCH_TILE_SHIFT) - 1) >> DISPATCH_TILE_SHIFT; +} + +} // Anonymous namespace + +LsfgMipmaps::LsfgMipmaps(const Device& device, MemoryAllocator& memory_allocator, + const LsfgShaders& shaders, LsfgResources& resources, + vk::DescriptorPool& descriptor_pool, LsfgImagePair& frames_, + f32 flow_scale) + : frames{&frames_} { + using namespace VideoCore::FrameGen::PerformanceShader; + + pass = LsfgPass(device, shaders, MIPMAPS, + {{1, VK_DESCRIPTOR_TYPE_UNIFORM_BUFFER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLER}, + {1, VK_DESCRIPTOR_TYPE_SAMPLED_IMAGE}, + {LSFG_MIP_LEVELS, VK_DESCRIPTOR_TYPE_STORAGE_IMAGE}}); + + const VkExtent2D input_extent = (*frames)[0].Extent(); + flow_extent = VkExtent2D{ + .width = std::max(1u, static_cast<u32>(static_cast<f32>(input_extent.width) * flow_scale)), + .height = std::max(1u, static_cast<u32>(static_cast<f32>(input_extent.height) * flow_scale)), + }; + + for (size_t i = 0; i < LSFG_MIP_LEVELS; ++i) { + const VkExtent2D level_extent{ + .width = flow_extent.width >> i, + .height = flow_extent.height >> i, + }; + out_images[i] = LsfgImage(device, memory_allocator, level_extent, LSFG_FLOW_FORMAT); + } + + const std::vector<VkDescriptorSetLayout> layouts(descriptor_sets.size(), pass.SetLayout()); + owned_sets = CreateWrappedDescriptorSets(descriptor_pool, layouts); + + const VkSampler sampler = resources.GetSampler(); + const VkBuffer buffer = resources.GetBuffer(); + + for (size_t i = 0; i < descriptor_sets.size(); ++i) { + descriptor_sets[i] = owned_sets[i]; + LsfgDescriptorWriter(descriptor_sets[i]) + .AddUniformBuffer(buffer, LsfgResources::BufferSize()) + .AddSampler(sampler) + .AddSampledImage((*frames)[i]) + .AddStorageImages(out_images) + .Build(device); + } +} + +void LsfgMipmaps::Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count) { + const size_t slot = frame_count % descriptor_sets.size(); + + LsfgBarriers(cmdbuf).WriteToRead((*frames)[slot]).ReadToWriteAll(out_images).Build(); + + pass.Bind(cmdbuf, descriptor_sets[slot]); + cmdbuf.Dispatch(GroupCount(flow_extent.width), GroupCount(flow_extent.height), 1); +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_mipmaps.h b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.h new file mode 100644 index 0000000000..7fa5b7b224 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_mipmaps.h @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <array> + +#include "common/common_types.h" +#include "video_core/renderer_vulkan/present/lsfg_common.h" + +namespace Vulkan { + +class Device; +class LsfgShaders; + +constexpr size_t LSFG_MIP_LEVELS = 7; + +class LsfgMipmaps { +public: + LsfgMipmaps() = default; + LsfgMipmaps(const Device& device, MemoryAllocator& memory_allocator, const LsfgShaders& shaders, + LsfgResources& resources, vk::DescriptorPool& descriptor_pool, + LsfgImagePair& frames, f32 flow_scale); + + void Dispatch(vk::CommandBuffer cmdbuf, u64 frame_count); + + [[nodiscard]] LsfgImage& Output(size_t level) { + return out_images[level]; + } + +private: + LsfgImagePair* frames{}; + + LsfgPass pass; + std::array<VkDescriptorSet, 2> descriptor_sets{}; + vk::DescriptorSets owned_sets; + + VkExtent2D flow_extent{}; + std::array<LsfgImage, LSFG_MIP_LEVELS> out_images; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_shaders.cpp b/src/video_core/renderer_vulkan/present/lsfg_shaders.cpp new file mode 100644 index 0000000000..aafe0d6419 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_shaders.cpp @@ -0,0 +1,37 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "common/settings.h" +#include "video_core/frame_gen/lossless_dll.h" +#include "video_core/renderer_vulkan/present/lsfg_shaders.h" +#include "video_core/renderer_vulkan/present/util.h" +#include "video_core/vulkan_common/vulkan_device.h" + +namespace Vulkan { + +LsfgShaders::LsfgShaders(const Device& device) { + if (!device.IsVulkanMemoryModelSupported() || !device.HasNullDescriptor()) { + return; + } + + const bool allow_fp16 = device.IsFloat16Supported(); + const bool prefer_fp16 = allow_fp16 && Settings::values.frame_gen_fp16.GetValue(); + + VideoCore::FrameGen::ShaderModules code; + if (VideoCore::FrameGen::LoadShaderModules(code, allow_fp16, prefer_fp16) != + VideoCore::FrameGen::LosslessStatus::Ok) { + return; + } + + for (const auto& [id, words] : code) { + modules.emplace(id, CreateWrappedShaderModule(device, words)); + } + valid = true; +} + +VkShaderModule LsfgShaders::Get(u32 shader_id) const { + const auto hit = modules.find(shader_id); + return hit == modules.end() ? VK_NULL_HANDLE : *hit->second; +} + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/lsfg_shaders.h b/src/video_core/renderer_vulkan/present/lsfg_shaders.h new file mode 100644 index 0000000000..cab37110d6 --- /dev/null +++ b/src/video_core/renderer_vulkan/present/lsfg_shaders.h @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include <map> + +#include "common/common_types.h" +#include "video_core/vulkan_common/vulkan_wrapper.h" + +namespace Vulkan { + +class Device; + +class LsfgShaders { +public: + explicit LsfgShaders(const Device& device); + + [[nodiscard]] bool IsValid() const { + return valid; + } + + [[nodiscard]] VkShaderModule Get(u32 shader_id) const; + +private: + std::map<u32, vk::ShaderModule> modules; + bool valid{}; +}; + +} // namespace Vulkan diff --git a/src/video_core/renderer_vulkan/present/util.cpp b/src/video_core/renderer_vulkan/present/util.cpp index 2b83902bc0..11250687d6 100644 --- a/src/video_core/renderer_vulkan/present/util.cpp +++ b/src/video_core/renderer_vulkan/present/util.cpp @@ -320,15 +320,16 @@ vk::DescriptorPool CreateWrappedDescriptorPool(const Device& device, size_t max_ }); } -vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout( - const Device& device, std::initializer_list<VkDescriptorType> types) { +vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout(const Device& device, + std::span<const VkDescriptorType> types, + VkShaderStageFlags stages) { std::vector<VkDescriptorSetLayoutBinding> bindings(types.size()); for (size_t i = 0; i < types.size(); i++) { bindings[i] = { .binding = static_cast<u32>(i), - .descriptorType = std::data(types)[i], + .descriptorType = types[i], .descriptorCount = 1, - .stageFlags = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT, + .stageFlags = stages, .pImmutableSamplers = nullptr, }; } @@ -342,6 +343,13 @@ vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout( }); } +vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout( + const Device& device, std::initializer_list<VkDescriptorType> types, + VkShaderStageFlags stages) { + return CreateWrappedDescriptorSetLayout( + device, std::span<const VkDescriptorType>{std::data(types), types.size()}, stages); +} + vk::DescriptorSets CreateWrappedDescriptorSets(vk::DescriptorPool& pool, vk::Span<VkDescriptorSetLayout> layouts) { return pool.Allocate(VkDescriptorSetAllocateInfo{ @@ -353,6 +361,28 @@ vk::DescriptorSets CreateWrappedDescriptorSets(vk::DescriptorPool& pool, }); } +vk::Pipeline CreateWrappedComputePipeline(const Device& device, vk::PipelineLayout& layout, + VkShaderModule shader) { + return device.GetLogical().CreateComputePipeline(VkComputePipelineCreateInfo{ + .sType = VK_STRUCTURE_TYPE_COMPUTE_PIPELINE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage = + { + .sType = VK_STRUCTURE_TYPE_PIPELINE_SHADER_STAGE_CREATE_INFO, + .pNext = nullptr, + .flags = 0, + .stage = VK_SHADER_STAGE_COMPUTE_BIT, + .module = shader, + .pName = "main", + .pSpecializationInfo = nullptr, + }, + .layout = *layout, + .basePipelineHandle = VK_NULL_HANDLE, + .basePipelineIndex = 0, + }); +} + vk::PipelineLayout CreateWrappedPipelineLayout(const Device& device, vk::DescriptorSetLayout& layout) { return device.GetLogical().CreatePipelineLayout(VkPipelineLayoutCreateInfo{ diff --git a/src/video_core/renderer_vulkan/present/util.h b/src/video_core/renderer_vulkan/present/util.h index 38cc6203c5..ecc263286d 100644 --- a/src/video_core/renderer_vulkan/present/util.h +++ b/src/video_core/renderer_vulkan/present/util.h @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: Copyright 2025 Eden Emulator Project +// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project // SPDX-License-Identifier: GPL-3.0-or-later // SPDX-FileCopyrightText: Copyright 2024 yuzu Emulator Project @@ -38,7 +38,11 @@ vk::DescriptorPool CreateWrappedDescriptorPool(const Device& device, size_t max_ std::initializer_list<VkDescriptorType> types = { VK_DESCRIPTOR_TYPE_COMBINED_IMAGE_SAMPLER}); vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout( - const Device& device, std::initializer_list<VkDescriptorType> types); + const Device& device, std::initializer_list<VkDescriptorType> types, + VkShaderStageFlags stages = VK_SHADER_STAGE_VERTEX_BIT | VK_SHADER_STAGE_FRAGMENT_BIT); +vk::DescriptorSetLayout CreateWrappedDescriptorSetLayout(const Device& device, + std::span<const VkDescriptorType> types, + VkShaderStageFlags stages); vk::DescriptorSets CreateWrappedDescriptorSets(vk::DescriptorPool& pool, vk::Span<VkDescriptorSetLayout> layouts); vk::PipelineLayout CreateWrappedPipelineLayout(const Device& device, @@ -46,6 +50,8 @@ vk::PipelineLayout CreateWrappedPipelineLayout(const Device& device, vk::Pipeline CreateWrappedPipeline(const Device& device, vk::RenderPass& renderpass, vk::PipelineLayout& layout, std::tuple<vk::ShaderModule&, vk::ShaderModule&> shaders); +vk::Pipeline CreateWrappedComputePipeline(const Device& device, vk::PipelineLayout& layout, + VkShaderModule shader); vk::Pipeline CreateWrappedPremultipliedBlendingPipeline( const Device& device, vk::RenderPass& renderpass, vk::PipelineLayout& layout, std::tuple<vk::ShaderModule&, vk::ShaderModule&> shaders); |
