LSFG: run frame generation on our own device, and delete the old path

Completes the switch to the Eden port. GSLsfg keeps its entire public surface —
availability, status text, display FPS, the settings and OSD plumbing all
untouched — and only its internals change, so nothing above the renderer had to
move.

What actually changed on screen: the old implementation ran the interpolator on
a SECOND VkDevice and shared images as AHardwareBuffers, and because Android
offers no cross-device semaphore (Turnip rejects OPAQUE_FD export on AHB memory)
the only barrier available was a full device idle — twice per frame, every
frame. That is gone. Generation is now ordinary compute recorded into a command
buffer on the device we already have, and interpolated frames are written
STRAIGHT into an acquired swap chain image through a storage view, so the
intermediate copy is gone too.

The pacer comes with it, which is the fix for games that oscillate between 60
and 30fps on a 60Hz panel: the generation count now varies to hold the presented
rate near a target instead of blindly multiplying whatever the game produced.

★ ONE submit, N+1 semaphores. All the generation work goes into a single
command buffer, submitted once, waiting on the caller's render-finished
semaphore plus every acquire, and signalling one semaphore per present that
follows. The obvious alternative — a submit per generated frame — walks straight
back into the binary-semaphore bug this file was bitten by before, where the
real present and the first generated present both want to wait on the semaphore
that says the source has been read. A binary semaphore may be waited exactly
once.

★ The hook fires AFTER vkQueueSubmit, so FrameGen had to take its command
buffer as a parameter. It was written against GSDeviceVK::GetCurrentCommandBuffer(),
which at that point is in flight or already belongs to the next frame; recording
into it is undefined and the symptom would have been interpolation running a
frame late rather than anything resembling an error.

Layout bracketing is ours: the ported passes speak Eden's convention where a
presentable image lives in GENERAL, and PCSX2 hands them over in PRESENT_SRC_KHR
and needs them back in it.

The swap chain now requests VK_IMAGE_USAGE_STORAGE_BIT — but only when frame
generation is on AND both the surface and the chosen format allow it. Asking
unconditionally fails swap chain creation outright on drivers that do not, which
would take the whole renderer down for a feature that is switched off. The
format half is the easy one to miss: a surface can report STORAGE support while
the sRGB format picked for it has no STORAGE_IMAGE feature bit, and that only
shows up later as a validation error at image-view creation. Because usage is
fixed at creation, switching the feature on mid-session needs a renderer
restart; Initialize says so rather than failing silently.

DELETED: platforms/android/app/src/main/cpp/3rdparty/lsfg in full — the
lsfg-vk-android framegen library, the DXVK dxbc compiler, pe-parse, volk and its
759-symbol collision with VKLoader, the C ABI shim, the version script, the
separate .so and the dlopen that found it, and the -fexceptions carve-out they
needed. GSLsfg.cpp went from 1259 lines to 654. The ~130 MB configure-time fetch
goes with it.

build-play-aab.sh's guard was rewritten rather than dropped: it checked for a
file that can no longer exist either way, so it would have passed forever
without proving anything. It now looks inside the core for a symbol only the
ported implementation defines.

Verified: all 18 affected translation units compile without errors, with
ARMSX2_HAS_LSFG on AND off (the play flavour still compiles the feature out
entirely). Not yet run on hardware.
This commit is contained in:
jpolo1224
2026-08-21 01:14:30 -04:00
parent 5e1d979b4e
commit 0bfefd4b69
15 changed files with 831 additions and 1376 deletions
+53
View File
@@ -707,6 +707,59 @@ if(USE_VULKAN)
COMPILE_OPTIONS "-fexceptions"
SKIP_PRECOMPILE_HEADERS ON)
# Frame generation ported from Eden (eden-emu PR #4263).
#
# Gated on ARMSX2_ENABLE_LSFG rather than on ARMSX2_HAVE_LSFG, and the difference matters:
# ARMSX2_ENABLE_LSFG arrives as a -D on the CMake command line from the Android gradle
# flavour, so it is in the cache before any subdirectory is processed. ARMSX2_HAVE_LSFG is
# set by 3rdparty/lsfg, which is added AFTER this file — testing it here would silently
# evaluate false and drop every source below, producing a build that succeeds with the
# feature missing. Desktop builds never define either, so they skip this entirely.
#
# Unlike GSLsfg.cpp above, none of these need -fexceptions. That carve-out exists because the
# lsfg-vk-android library reports failure by throwing; the Eden port has zero throw sites and
# returns status enums instead, so it builds under the emulator's normal -fno-exceptions.
if(ARMSX2_ENABLE_LSFG)
list(APPEND pcsx2GSSources
GS/Renderers/Vulkan/FrameGen/FrameGen.cpp
GS/Renderers/Vulkan/FrameGen/FrameGenPacer.cpp
GS/Renderers/Vulkan/FrameGen/LosslessDll.cpp
GS/Renderers/Vulkan/FrameGen/LsfgAlpha.cpp
GS/Renderers/Vulkan/FrameGen/LsfgBeta.cpp
GS/Renderers/Vulkan/FrameGen/LsfgChain.cpp
GS/Renderers/Vulkan/FrameGen/LsfgCommon.cpp
GS/Renderers/Vulkan/FrameGen/LsfgDelta.cpp
GS/Renderers/Vulkan/FrameGen/LsfgGamma.cpp
GS/Renderers/Vulkan/FrameGen/LsfgGenerate.cpp
GS/Renderers/Vulkan/FrameGen/LsfgMipmaps.cpp
GS/Renderers/Vulkan/FrameGen/LsfgShaders.cpp
GS/Renderers/Vulkan/FrameGen/LsfgTranslate.cpp
GS/Renderers/Vulkan/FrameGen/LsfgUtil.cpp
GS/Renderers/Vulkan/FrameGen/LsfgVkCompat.cpp
)
list(APPEND pcsx2GSHeaders
GS/Renderers/Vulkan/FrameGen/FrameGen.h
GS/Renderers/Vulkan/FrameGen/FrameGenPacer.h
GS/Renderers/Vulkan/FrameGen/FrameGenTypes.h
GS/Renderers/Vulkan/FrameGen/LosslessDll.h
GS/Renderers/Vulkan/FrameGen/LsfgAlpha.h
GS/Renderers/Vulkan/FrameGen/LsfgBeta.h
GS/Renderers/Vulkan/FrameGen/LsfgChain.h
GS/Renderers/Vulkan/FrameGen/LsfgCommon.h
GS/Renderers/Vulkan/FrameGen/LsfgDelta.h
GS/Renderers/Vulkan/FrameGen/LsfgGamma.h
GS/Renderers/Vulkan/FrameGen/LsfgGenerate.h
GS/Renderers/Vulkan/FrameGen/LsfgMipmaps.h
GS/Renderers/Vulkan/FrameGen/LsfgShaders.h
GS/Renderers/Vulkan/FrameGen/LsfgTranslate.h
GS/Renderers/Vulkan/FrameGen/LsfgUtil.h
GS/Renderers/Vulkan/FrameGen/LsfgVkCompat.h
)
# The ported sources include each other by bare filename, as they did upstream.
target_include_directories(PCSX2_FLAGS INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/GS/Renderers/Vulkan/FrameGen")
endif()
# VKLibretro.cpp needs the libretro Vulkan HW-render interface header.
# Link it PRIVATE to PCSX2 (not the shared PCSX2_FLAGS) so the header path
# doesn't leak onto every target that consumes PCSX2_FLAGS.
@@ -0,0 +1,284 @@
// 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
//
// Ported from Eden (eden-emu PR #4263), src/video_core/renderer_vulkan/present/frame_gen.cpp.
// The orchestration is verbatim; see FrameGen.h for the three structural differences and for why
// the debug image dump is gone. Only the PORT-marked spots below deviate.
#include <algorithm>
#include <array>
#include <cmath>
#include "FrameGen.h"
#include "LsfgChain.h"
#include "LsfgCommon.h"
#include "LsfgShaders.h"
#include "LsfgVkCompat.h"
#include "GS/Renderers/Vulkan/GSDeviceVK.h"
namespace Vulkan {
namespace {
constexpr u64 LSFG_REQUIRED_FRAMES = 2;
constexpr u32 LSFG_RECURRENCE_FRAMES = 2;
[[nodiscard]] f32 ManualFlowScale() {
// The clamp is not Eden's: their setting type enforces 25..100 on the way in, ours is a plain
// u8 that a hand-edited INI can hold anything in, and LsfgResources feeds this straight into
// 1.0f / flow_scale. GSLsfg.cpp clamps the same way for the same reason.
return static_cast<f32>(std::clamp<u8>(GSConfig.LsfgFlowScale, 25, 100)) / 100.0f;
}
[[nodiscard]] f32 ConfiguredFlowScale(VkExtent2D guest_extent, VkExtent2D presented_extent) {
// PORT: Eden gates the automatic path on frame_gen_flow_scale_auto, a toggle that defaults on
// and that PCSX2 has no equivalent for. 100% — our default, and the top of the 25..100 range
// the UI offers — reads as "do not reduce the flow resolution", and the automatic result is
// clamped to 1.0 anyway, so treating it as Eden's auto mode preserves both projects' default
// behaviour. Any explicit value below 100 pins the scale exactly where the user put it.
if (GSConfig.LsfgFlowScale < 100) {
return ManualFlowScale();
}
if (guest_extent.width == 0 || presented_extent.width == 0) {
return 1.0f;
}
// PORT: Eden scales by resolution_info.up_factor because its guest_extent is the console's own
// resolution. Ours is the size the game was really rendered at, upscale already applied.
const f32 rendered_width = static_cast<f32>(guest_extent.width);
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);
}
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);
// PORT: yuzu's vk::Span takes a lone VkImageCopy; the shim's std::span needs a range.
const std::array regions{MakeCopyRegion(extent)};
cmdbuf.CopyImage(source, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, destination.Handle(),
VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, regions);
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_, GSDeviceVK* device_)
: memory_allocator{memory_allocator_}, gs_device{device_} {}
FrameGen::~FrameGen() {
if (chain) {
// ★ Device idle before teardown — see WaitForIdle. The chain is about to be destroyed by
// the member destructor, and its images and pipelines go with it immediately.
WaitForIdle();
}
}
void FrameGen::Process(const Device& device, vk::CommandBuffer cmdbuf, VkImage image,
VkImageView storage_view, VkExtent2D extent, VkFormat format,
VkExtent2D guest_extent) {
generated = false;
if (unavailable || !GSConfig.LsfgEnabled) {
if (chain) {
// ★ Device idle before teardown — see WaitForIdle. Stands in for Eden's
// scheduler.Finish() ahead of the same chain.reset().
WaitForIdle();
chain.reset();
}
warm_streak = 0;
return;
}
if (storage_view == VK_NULL_HANDLE) {
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 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;
// PORT: Eden asks the scheduler for an outside-render-pass context and defers the recording
// into a callback. Here the caller is already outside any render pass and hands us the buffer
// to record into — see the note on Process() in the header for why it must not be the frame's.
CopyPresentedFrame(cmdbuf, image, chain->Input(count), extent);
if (warm) {
chain->DispatchShared(cmdbuf, count);
}
}
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, vk::CommandBuffer cmdbuf, VkImage image,
VkImageView storage_view, size_t generation) {
const u32 target = TargetIndex(storage_view);
chain->SetTarget(device, last_generations, generation, target, storage_view);
// PORT: Eden reads the destination Frame's own size. Everything we generate into is a
// presented image, so it is the extent the chain was built for.
const VkExtent2D extent = built_extent;
// PORT: recorded into the caller's buffer, as in Process.
chain->DispatchGeneration(cmdbuf, last_count, last_generations, generation, target, image,
extent);
}
void FrameGen::Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale) {
// ★ Device idle before teardown — see WaitForIdle. This is Eden's scheduler.Finish().
WaitForIdle();
chain.reset();
// PORT: the slot table keys on VkImageView handles, and a rebuild is exactly when the
// presented images are recreated. Dropping the stale handles keeps a recycled one from
// matching an entry that belongs to a view that no longer exists.
targets.fill(VK_NULL_HANDLE);
target_count = 0;
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::WaitForIdle() {
// ★ LOAD-BEARING, and the reason every teardown path calls it first.
//
// LsfgVkCompat's wrappers call vkDestroy* the moment they go out of scope instead of routing
// through GSDeviceVK's deferred-destruction queue. Releasing the chain while a submitted
// command buffer still references its images, pipelines or descriptor pool is therefore a
// use-after-free, and one that surfaces as a random GPU fault rather than as an obvious bug.
// Eden gets the guarantee from Scheduler::Finish(); ours has to be explicit.
//
// vkDeviceWaitIdle covers everything submitted, which is sufficient here because every site
// that calls this runs before that frame's chain work is recorded, and the previous frame's
// command buffer was submitted by the present that ended it. Note it is deliberately not
// GSDeviceVK::ExecuteCommandBuffer: that submits the frame's command buffer, which is the
// wrong thing to do halfway through a present.
gs_device->WaitForGPUIdle();
}
u32 FrameGen::TargetIndex(VkImageView view) {
for (size_t i = 0; i < targets.size(); ++i) {
if (targets[i] == view) {
return static_cast<u32>(i);
}
}
// Unseen view: claim the next slot. Wrapping is not expected — a present path draws from a
// handful of images and there are LSFG_MAX_TARGETS of these — and costs only a descriptor
// rewrite if it ever happens.
const u32 index = static_cast<u32>(target_count++ % targets.size());
targets[index] = view;
return index;
}
} // namespace Vulkan
@@ -0,0 +1,95 @@
// 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
//
// Ported from Eden (eden-emu PR #4263), src/video_core/renderer_vulkan/present/frame_gen.h.
// The orchestration is unchanged — warm-up counting, the rebuild conditions, the flow-scale
// derivation and the pacer plumbing are all Eden's. What differs is structural:
//
// * Eden's methods take a `Frame*` out of PresentManager's pool. PCSX2 has no such pool, so the
// three fields they read (image, storage view, extent) arrive as explicit parameters instead.
// * Eden records through a `Scheduler`. We hold GSDeviceVK and record into its current command
// buffer directly.
// * Eden's debug image dump — `DumpDebugImages` plus the WritePortablePixmap / WriteGrayscalePgm
// / WriteRaw / WriteColorPpm writers and the `dumped` flag — is not carried over. It needs
// <filesystem>, which the GS backend does not pull in, and a buffer readback the compat shim
// does not implement.
//
// See FrameGenTypes.h and LsfgVkCompat.h.
#pragma once
#include <array>
#include <optional>
#include "FrameGenPacer.h"
#include "FrameGenTypes.h"
#include "LsfgChain.h"
#include "LsfgShaders.h"
#include "LsfgVkCompat.h"
namespace Vulkan {
class FrameGen {
public:
explicit FrameGen(MemoryAllocator& memory_allocator, GSDeviceVK* device);
~FrameGen();
/// Feeds the frame about to be presented into the chain.
///
/// ★ The caller supplies [cmdbuf]. It is NOT taken from GSDeviceVK::GetCurrentCommandBuffer():
/// frame generation runs from the present hook, which fires AFTER the frame's command buffer
/// has been submitted, so that buffer is either in flight or already belongs to the next
/// frame. Recording into it is undefined, and the visible symptom would be interpolation
/// running a frame late rather than anything that looks like an error. GSLsfg owns its own
/// one-shot buffers and passes them in, which is also how Eden's scheduler supplies one.
///
/// `image` / `storage_view` / `extent` describe that presented image; `guest_extent` is the
/// size the game was actually rendered at, upscale included — the flow-scale heuristic
/// compares the two. Must be called outside a render pass.
void Process(const Device& device, vk::CommandBuffer cmdbuf, VkImage image,
VkImageView storage_view, VkExtent2D extent, VkFormat format,
VkExtent2D guest_extent);
[[nodiscard]] size_t WantedGenerations(size_t capacity);
[[nodiscard]] size_t GeneratedFrameCount() const;
/// Writes interpolated frame `generation` into `image`. Only valid while
/// GeneratedFrameCount() is non-zero, and for `generation` below it.
void GenerateInto(const Device& device, vk::CommandBuffer cmdbuf, VkImage image,
VkImageView storage_view, size_t generation);
private:
void Rebuild(const Device& device, VkExtent2D extent, VkFormat format, f32 flow_scale);
void WaitForIdle();
[[nodiscard]] u32 TargetIndex(VkImageView view);
MemoryAllocator& memory_allocator;
GSDeviceVK* gs_device;
std::optional<LsfgShaders> shaders;
std::optional<LsfgChain> chain;
FrameGenPacer pacer;
FrameGenPlan plan{};
/// PORT: stands in for Eden's `Frame::index`. LsfgGenerate keys its descriptor sets by target
/// slot and only rewrites them when the view in that slot changes, so a destination image
/// needs a *stable* index — Eden gets one from its frame pool, we recover it by remembering
/// which view we handed to which slot.
std::array<VkImageView, LSFG_MAX_TARGETS> targets{};
size_t target_count{};
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{};
};
} // namespace Vulkan
@@ -255,26 +255,20 @@ namespace Vulkan
m_logical = vk::LogicalDevice(dev->GetDevice());
// Both features are queried straight from the driver rather than trusted from PCSX2's
// own feature struct: the LSFG shaders need them specifically, and PCSX2 does not enable
// or track either for its own rendering, so there is nothing cached to read.
// Value-init then assign, rather than a braced initialiser: these structs carry more
// members than the two being set, and naming only the first leaves the rest to
// -Wmissing-field-initializers. Same idiom as the rest of the Vulkan backend.
VkPhysicalDeviceRobustness2FeaturesEXT robustness = {};
robustness.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT;
VkPhysicalDeviceVulkanMemoryModelFeatures memory_model = {};
memory_model.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_MEMORY_MODEL_FEATURES;
memory_model.pNext = &robustness;
VkPhysicalDeviceFeatures2 features = {};
features.sType = VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FEATURES_2;
features.pNext = &memory_model;
vkGetPhysicalDeviceFeatures2(dev->GetPhysicalDevice(), &features);
m_vulkan_memory_model = (memory_model.vulkanMemoryModel == VK_TRUE);
m_null_descriptor = (robustness.nullDescriptor == VK_TRUE);
// ★ Read what the LOGICAL device actually ENABLED, not what the physical device reports.
//
// This originally called vkGetPhysicalDeviceFeatures2 and believed the answer, which is
// wrong in the direction that hurts: a driver can report both features as supported while
// GSDeviceVK never requested either at vkCreateDevice, and then declaring the Vulkan
// memory model in a shader module, or relying on a null descriptor, is invalid usage.
// On Adreno that surfaces as device-lost mid-frame rather than as a clean failure, so
// the check would have looked like it passed right up until it took the renderer down.
//
// GSDeviceVK already probes both against the driver and clears the flag when a feature is
// advertised but absent, so these are true only when the feature was really enabled.
const GSDeviceVK::OptionalExtensions& ext = dev->GetOptionalExtensions();
m_vulkan_memory_model = ext.vk_khr_vulkan_memory_model;
m_null_descriptor = ext.vk_ext_robustness2_null_descriptor;
}
// --- MemoryAllocator -------------------------------------------------------------------
+34
View File
@@ -529,6 +529,10 @@ bool GSDeviceVK::SelectDeviceExtensions(ExtensionList* extension_list, bool enab
#endif
m_optional_extensions.vk_ext_fragment_shader_interlock = SupportsExtension(VK_EXT_FRAGMENT_SHADER_INTERLOCK_EXTENSION_NAME, false);
// LSFG frame generation only. PCSX2 targets Vulkan 1.1, where neither is core — the memory
// model is 1.2 and nullDescriptor never became core at all — so both come in as extensions.
m_optional_extensions.vk_khr_vulkan_memory_model = SupportsExtension(VK_KHR_VULKAN_MEMORY_MODEL_EXTENSION_NAME, false);
m_optional_extensions.vk_ext_robustness2_null_descriptor = SupportsExtension(VK_EXT_ROBUSTNESS_2_EXTENSION_NAME, false);
return true;
}
@@ -718,6 +722,10 @@ bool GSDeviceVK::CreateDevice(VkSurfaceKHR surface, bool enable_validation_layer
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR};
VkPhysicalDeviceFragmentShaderInterlockFeaturesEXT fragment_shader_interlock_ext_feature = {
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT};
VkPhysicalDeviceVulkanMemoryModelFeatures vulkan_memory_model_feature = {
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_MEMORY_MODEL_FEATURES};
VkPhysicalDeviceRobustness2FeaturesEXT robustness2_feature = {
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
// An advertised EXTENSION does not guarantee its FEATURE bit, and asking for a feature the
// driver does not have fails vkCreateDevice outright with VK_ERROR_FEATURE_NOT_PRESENT —
@@ -743,6 +751,10 @@ bool GSDeviceVK::CreateDevice(VkSurfaceKHR surface, bool enable_validation_layer
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_SWAPCHAIN_MAINTENANCE_1_FEATURES_KHR};
VkPhysicalDeviceFragmentShaderInterlockFeaturesEXT probe_fsi = {
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_FRAGMENT_SHADER_INTERLOCK_FEATURES_EXT};
VkPhysicalDeviceVulkanMemoryModelFeatures probe_vmm = {
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_VULKAN_MEMORY_MODEL_FEATURES};
VkPhysicalDeviceRobustness2FeaturesEXT probe_r2 = {
VK_STRUCTURE_TYPE_PHYSICAL_DEVICE_ROBUSTNESS_2_FEATURES_EXT};
// Only chain what we would actually enable: querying a struct whose extension is absent is
// not something the spec promises anything about.
@@ -759,6 +771,10 @@ bool GSDeviceVK::CreateDevice(VkSurfaceKHR surface, bool enable_validation_layer
Vulkan::AddPointerToChain(&probe, &probe_sm1);
if (m_optional_extensions.vk_ext_fragment_shader_interlock)
Vulkan::AddPointerToChain(&probe, &probe_fsi);
if (m_optional_extensions.vk_khr_vulkan_memory_model)
Vulkan::AddPointerToChain(&probe, &probe_vmm);
if (m_optional_extensions.vk_ext_robustness2_null_descriptor)
Vulkan::AddPointerToChain(&probe, &probe_r2);
vkGetPhysicalDeviceFeatures2(m_physical_device, &probe);
// Returns the flag rather than taking it by reference: m_optional_extensions members are
@@ -792,6 +808,13 @@ bool GSDeviceVK::CreateDevice(VkSurfaceKHR surface, bool enable_validation_layer
m_optional_extensions.vk_ext_fragment_shader_interlock = keep("VK_EXT_fragment_shader_interlock",
m_optional_extensions.vk_ext_fragment_shader_interlock,
probe_fsi.fragmentShaderPixelInterlock == VK_TRUE);
m_optional_extensions.vk_khr_vulkan_memory_model = keep("VK_KHR_vulkan_memory_model",
m_optional_extensions.vk_khr_vulkan_memory_model, probe_vmm.vulkanMemoryModel == VK_TRUE);
// The FEATURE we want is nullDescriptor specifically. VK_EXT_robustness2 also carries
// robustBufferAccess2/robustImageAccess2, which cost performance and which nothing here
// needs — they are deliberately left VK_FALSE below.
m_optional_extensions.vk_ext_robustness2_null_descriptor = keep("VK_EXT_robustness2 (nullDescriptor)",
m_optional_extensions.vk_ext_robustness2_null_descriptor, probe_r2.nullDescriptor == VK_TRUE);
// Depth ROAA is an optional sub-feature: a driver can offer the extension and colour
// access yet not depth.
@@ -832,6 +855,17 @@ bool GSDeviceVK::CreateDevice(VkSurfaceKHR surface, bool enable_validation_layer
fragment_shader_interlock_ext_feature.fragmentShaderPixelInterlock = VK_TRUE;
Vulkan::AddPointerToChain(&device_info, &fragment_shader_interlock_ext_feature);
}
if (m_optional_extensions.vk_khr_vulkan_memory_model)
{
vulkan_memory_model_feature.vulkanMemoryModel = VK_TRUE;
Vulkan::AddPointerToChain(&device_info, &vulkan_memory_model_feature);
}
if (m_optional_extensions.vk_ext_robustness2_null_descriptor)
{
// nullDescriptor ONLY — see the note by the probe above.
robustness2_feature.nullDescriptor = VK_TRUE;
Vulkan::AddPointerToChain(&device_info, &robustness2_feature);
}
VkResult res = vkCreateDevice(m_physical_device, &device_info, nullptr, &m_device);
if (res != VK_SUCCESS)
+5
View File
@@ -49,6 +49,11 @@ public:
bool vk_khr_shader_non_semantic_info : 1;
bool vk_ext_attachment_feedback_loop_layout : 1;
bool vk_ext_fragment_shader_interlock : 1;
/// Both are required by the LSFG frame-generation shaders and by NOTHING else in the
/// renderer. They are requested anyway whenever the driver really has them, because the
/// alternative is recreating the device when frame generation is switched on.
bool vk_khr_vulkan_memory_model : 1; ///< shaders declare the Vulkan memory model
bool vk_ext_robustness2_null_descriptor : 1; ///< nullDescriptor only; not the robust-access bits
};
// Global state accessors
File diff suppressed because it is too large Load Diff
+27
View File
@@ -700,6 +700,33 @@ bool VKSwapChain::CreateSwapChain()
return false;
}
// Frame generation writes interpolated frames straight into a swap chain image through a
// storage view, so those images need STORAGE usage. Requested only when frame generation is
// actually on, and only when BOTH the surface and the chosen format allow it — asking
// unconditionally would fail swap chain creation outright on drivers that do not, taking the
// whole renderer down for a feature that is off.
//
// The format check is the one that is easy to forget: a surface can report STORAGE in
// supportedUsageFlags while the sRGB format selected for it has no STORAGE_IMAGE feature bit,
// and the mismatch only shows up as a validation error at image-view creation.
m_storage_usage_available = false;
if (GSConfig.LsfgEnabled && (surface_capabilities.supportedUsageFlags & VK_IMAGE_USAGE_STORAGE_BIT) != 0)
{
VkFormatProperties fp = {};
vkGetPhysicalDeviceFormatProperties(GSDeviceVK::GetInstance()->GetPhysicalDevice(),
surface_format->format, &fp);
if ((fp.optimalTilingFeatures & VK_FORMAT_FEATURE_STORAGE_IMAGE_BIT) != 0)
{
image_usage |= VK_IMAGE_USAGE_STORAGE_BIT;
m_storage_usage_available = true;
}
else
{
Console.Warning("Vulkan: swap chain format has no STORAGE_IMAGE feature; "
"frame generation will report itself unavailable.");
}
}
// Store the old/current swap chain when recreating for resize
// Old swap chain is destroyed regardless of whether the create call succeeds
VkSwapchainKHR old_swap_chain;
+7
View File
@@ -94,6 +94,12 @@ public:
}
VkFormat GetTextureFormat() const;
/// True when the swap chain images carry VK_IMAGE_USAGE_STORAGE_BIT, which frame generation
/// requires in order to write an interpolated frame directly into one. False whenever frame
/// generation was off at creation time, so switching it on needs a swap chain recreate — see
/// GSLsfg::Initialize.
__fi bool IsStorageUsageAvailable() const { return m_storage_usage_available; }
VkResult AcquireNextImage();
void ReleaseCurrentImage();
void ResetImageAcquireResult();
@@ -128,6 +134,7 @@ private:
VkSwapchainKHR m_swap_chain = VK_NULL_HANDLE;
std::vector<std::unique_ptr<GSTextureVK>> m_images;
bool m_storage_usage_available = false;
std::array<ImageSemaphores, NUM_SEMAPHORES> m_semaphores = {};
u32 m_current_image = 0;
@@ -63,7 +63,7 @@ import java.io.File
*
* [BuildConfig.LSFG] is false in the Play build and the whole section compiles out of it
* the native side is not there either (ARMSX2_ENABLE_LSFG is off, so GSLsfg answers
* NOT_COMPILED_IN), and build-play-aab.sh fails the build if libarmsx2_lsfg.so ever
* NOT_COMPILED_IN), and build-play-aab.sh fails the build if the ported frame-generation code ever
* appears in the bundle.
*/
@@ -1,180 +0,0 @@
# LSFG Lossless Scaling frame generation, via lsfg-vk-android.
#
# Fetched at configure time from a pinned commit rather than vendored, for the same reason
# librashader is: the source tree plus its four submodules is ~130 MB, most of it test data
# and a Rust UI we never build.
#
# WHAT WE TAKE AND WHAT WE DO NOT
#
# Only the MIT-licensed `lsfg-vk-android` repository, and inside it only the framegen static
# library. Its sibling `LSFG-Android-Application` carries a "No Play Store, No Commercial Use"
# licence and is deliberately NOT fetched nor is it needed. That app exists to capture the
# screen with MediaProjection and composite over another process, because Android 12+ forbids
# injecting code into a non-debuggable app. ARMSX2 owns its own swapchain, so it drives the
# library's AHardwareBuffer entry points directly: no screen capture, no overlay window, no
# accessibility service.
#
# The `liblsfg-vk.so` Vulkan-layer target from upstream is also skipped. It fails to link
# against the NDK anyway (vkGetAndroidHardwareBufferPropertiesANDROID is an extension entry
# point, not a plain export, so it must come from vkGetDeviceProcAddr) and a layer is the
# wrong shape for an in-process caller. We link the static framegen lib and resolve Vulkan
# through the loader the GS backend already has.
#
# NOTHING PROPRIETARY IS FETCHED OR SHIPPED. The frame-generation shaders are not in this
# repository or upstream's; they are read at runtime out of the user's own legitimately
# purchased Lossless.dll, which they supply through the Storage Access Framework. This build
# only produces the code that can read one.
#
# OPTIONAL, like librashader and the Discord SDK: ARMSX2_ENABLE_LSFG defaults OFF and the play
# flavour never sets it, so the feature compiles out entirely rather than failing to configure.
if(NOT ARMSX2_ENABLE_LSFG)
message(STATUS "LSFG: disabled (ARMSX2_ENABLE_LSFG is off) — frame generation compiles out")
set(ARMSX2_HAVE_LSFG OFF PARENT_SCOPE)
return()
endif()
if(NOT ANDROID)
message(STATUS "LSFG: skipped — the AHardwareBuffer path is Android-only")
set(ARMSX2_HAVE_LSFG OFF PARENT_SCOPE)
return()
endif()
# Pinned to a COMMIT, which is what the paragraph below actually asks for.
#
# This said `release` a branch while the comment claimed it was pinned, so every configure
# re-resolved it and a remote push could silently change what the core links. That is the exact
# failure the comment was written to prevent, and it went unnoticed because a branch name reads
# like a tag at a glance. This library reaches into Vulkan device internals and ships inside our
# APK; it does not get to move on its own.
#
# 3e89e54 "feat: enable shaderFloat16 support in logical device creation" is the tip of `release`
# that ARMSX2 has actually been built and verified against. Bump it deliberately, and re-verify.
set(LSFG_PIN "3e89e5439a98f55d5acb003d20039426ab24e69c"
CACHE STRING "lsfg-vk-android commit to build against")
include(FetchContent)
FetchContent_Declare(lsfg_src
GIT_REPOSITORY https://github.com/FrankBarretta/lsfg-vk-android.git
GIT_TAG ${LSFG_PIN}
# GIT_SHALLOW cannot fetch an arbitrary commit a shallow clone only carries branch tips,
# so it works for a ref name and fails for the SHA above.
GIT_SHALLOW FALSE
GIT_SUBMODULES_RECURSE TRUE
# Populate only; its top-level CMakeLists builds the layer .so we do not want, and adding
# it wholesale would drag the Rust UI target in with it.
SOURCE_SUBDIR framegen
)
FetchContent_MakeAvailable(lsfg_src)
if(NOT EXISTS "${lsfg_src_SOURCE_DIR}/framegen/CMakeLists.txt")
message(WARNING "LSFG: fetch produced no framegen/ — compiling the feature out")
set(ARMSX2_HAVE_LSFG OFF PARENT_SCOPE)
return()
endif()
# --- the shader path -------------------------------------------------------------------
# framegen does not read Lossless.dll. Its initialize() takes a loader callback and expects
# SPIR-V back, so the caller owns the whole chain: pull the RCDATA resources out of the PE,
# then translate each one from DXBC to SPIR-V. Upstream does that in its layer .so, which we
# do not build so we assemble the same two pieces here and drive them from GSLsfg.cpp.
#
# pe-parse reads the resource directory out of the user's DLL
# dxbc DXVK's DXBC->SPIR-V compiler, which does the actual translation
# trans.cpp upstream's thin wrapper over dxbc that also rewrites descriptor bindings
#
# extract.cpp is deliberately NOT taken: it hunts for Steam install paths and pulls in
# upstream's toml11 config system, neither of which means anything on Android. Our path
# comes from the SAF picker, so GSLsfg.cpp does that ~30 lines of pe-parse itself.
foreach(dep dxbc pe-parse/pe-parser-library volk)
if(NOT EXISTS "${lsfg_src_SOURCE_DIR}/thirdparty/${dep}/CMakeLists.txt")
message(WARNING "LSFG: thirdparty/${dep} missing (submodules not fetched?) — compiling out")
set(ARMSX2_HAVE_LSFG OFF PARENT_SCOPE)
return()
endif()
endforeach()
if(NOT TARGET lsfg-vk-framegen)
message(WARNING "LSFG: lsfg-vk-framegen target absent after fetch — compiling the feature out")
set(ARMSX2_HAVE_LSFG OFF PARENT_SCOPE)
return()
endif()
add_subdirectory("${lsfg_src_SOURCE_DIR}/thirdparty/dxbc"
"${CMAKE_CURRENT_BINARY_DIR}/dxbc" EXCLUDE_FROM_ALL)
add_subdirectory("${lsfg_src_SOURCE_DIR}/thirdparty/pe-parse/pe-parser-library"
"${CMAKE_CURRENT_BINARY_DIR}/pe-parse" EXCLUDE_FROM_ALL)
# framegen links volk PUBLIC. It is NOT in the fetch's SOURCE_SUBDIR, so it has to be added
# by hand or the framegen compile fails on a missing <volk.h>.
add_subdirectory("${lsfg_src_SOURCE_DIR}/thirdparty/volk"
"${CMAKE_CURRENT_BINARY_DIR}/volk" EXCLUDE_FROM_ALL)
# Upstream's translator, compiled on its own so we inherit their binding-rewrite fixes
# instead of forking a copy of it into our tree.
add_library(lsfg-shader-translate STATIC "${lsfg_src_SOURCE_DIR}/src/extract/trans.cpp")
set_target_properties(lsfg-shader-translate PROPERTIES
CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON POSITION_INDEPENDENT_CODE ON)
target_include_directories(lsfg-shader-translate PUBLIC "${lsfg_src_SOURCE_DIR}/include")
# trans.cpp includes <thirdparty/spirv.hpp>, which lives under dxbc's include/spirv.
target_link_libraries(lsfg-shader-translate PUBLIC dxbc)
# VK_USE_PLATFORM_ANDROID_KHR is REQUIRED, not cosmetic: without it the Vulkan headers hide
# VkAndroidHardwareBufferFormatPropertiesANDROID and friends, and framegen/src/core/image.cpp
# fails with three "unknown type name" errors. Upstream's own Gradle defines it, which is why
# a bare CMake configure of that repo does not build.
#
# PRIVATE, not PUBLIC: framegen's own sources need it, but its public header does not, and
# PUBLIC leaks -DVK_USE_PLATFORM_ANDROID_KHR onto every consumer where it collides with
# VKLoader.h's own valueless #define and emits a macro-redefined warning in each of the
# hundreds of GS translation units.
target_compile_definitions(lsfg-vk-framegen PRIVATE VK_USE_PLATFORM_ANDROID_KHR)
# volk needs it too, and for a reason that is not obvious: volk generates its PFN_* globals
# from the same platform macros, so without this it never defines
# vkGetAndroidHardwareBufferPropertiesANDROID while framegen compiled WITH the macro
# references it. The result is a single undefined symbol at link, pointing at framegen rather
# than at the volk target that actually dropped it.
target_compile_definitions(volk PUBLIC VK_USE_PLATFORM_ANDROID_KHR)
# The interpolation runs on its own VkDevice, so it needs the AHB import/export entry points
# from libvulkan plus libandroid for AHardwareBuffer itself.
target_link_libraries(lsfg-vk-framegen PUBLIC vulkan android)
# ARMSX2's own build turns exceptions OFF globally (BuildParameters.cmake, add_compile_options
# at top level, inherited by every subdirectory added after it). Every one of these libraries
# reports failure by throwing 62 throw sites in framegen, 53 in dxbc so they do not merely
# warn without this, they fail to compile. Re-enabled per target rather than by editing the
# global flag, so nothing else in the emulator gains exception handling as a side effect.
foreach(lsfg_target lsfg-vk-framegen dxbc lsfg-shader-translate)
target_compile_options(${lsfg_target} PRIVATE -fexceptions)
endforeach()
# --- the isolation boundary -------------------------------------------------------------
#
# framegen goes into its OWN shared object, never into libemucore. volk (which framegen links)
# defines 759 globals named vkCreateImage, vkQueueSubmit and so on precisely the names
# PCSX2's VKLoader.cpp defines. In one library that is a duplicate-symbol error at best; at
# worst the linker merges them and framegen's volkLoadDevice() call, made against ITS OWN
# VkDevice, silently repoints every entry point the GS renderer uses. A separate .so gives
# volk its own copies and is the only arrangement where the two loaders can coexist.
#
# The interface is C, for the second half of the same problem: ANDROID_STL=c++_static means
# each .so carries its own libc++, so an std::vector crossing the boundary would be two
# unrelated types sharing a name. See armsx2_lsfg_shim.h.
add_library(armsx2_lsfg SHARED "${CMAKE_CURRENT_SOURCE_DIR}/armsx2_lsfg_shim.cpp")
set_target_properties(armsx2_lsfg PROPERTIES
CXX_STANDARD 20 CXX_STANDARD_REQUIRED ON)
target_compile_options(armsx2_lsfg PRIVATE -fexceptions)
target_compile_definitions(armsx2_lsfg PRIVATE VK_USE_PLATFORM_ANDROID_KHR)
target_link_libraries(armsx2_lsfg PRIVATE lsfg-vk-framegen)
# Everything is hidden except the handful of armsx2_lsfg_* entry points, which are marked
# visible in the shim itself. In particular framegen's own LSFG_3_1 symbols stay internal, so
# nothing can reach past the boundary by accident.
target_link_options(armsx2_lsfg PRIVATE -Wl,--exclude-libs,ALL)
set(ARMSX2_HAVE_LSFG ON PARENT_SCOPE)
# Only the shim header and the shader chain reach libemucore. framegen's headers deliberately
# do NOT: nothing in the core should be able to call it directly.
set(ARMSX2_LSFG_INCLUDE
"${CMAKE_CURRENT_SOURCE_DIR}"
"${lsfg_src_SOURCE_DIR}/include"
PARENT_SCOPE)
set(ARMSX2_LSFG_LIBRARIES lsfg-shader-translate pe-parse PARENT_SCOPE)
message(STATUS "LSFG: enabled — framegen isolated in libarmsx2_lsfg.so, core links the shader chain only")
@@ -1,183 +0,0 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
// Implementation of the C ABI declared in armsx2_lsfg_shim.h. Compiled ONLY into
// libarmsx2_lsfg.so, alongside framegen and its private copy of volk. See the header for why
// this boundary exists at all.
#include "armsx2_lsfg_shim.h"
#include "lsfg_3_1.hpp"
#include "lsfg_3_1p.hpp"
#include <exception>
#include <string>
#include <vector>
#define LSFG_EXPORT extern "C" __attribute__((visibility("default")))
namespace
{
// Thread-local so a failure on the GS thread cannot be overwritten by one elsewhere, and so
// the returned pointer stays valid without any locking on the reader's side.
thread_local std::string t_last_error;
// Which shader family initialise() picked, fixed until finalize(). LSFG_3_1 and LSFG_3_1P
// keep entirely separate device state and context tables, so a context created by one cannot
// be presented or destroyed through the other — which is why every entry point below has to
// dispatch on this rather than only the one that chose it.
bool g_performance = false;
void ClearError() { t_last_error.clear(); }
void SetError(const char* what) { t_last_error = what ? what : "unknown error"; }
} // namespace
LSFG_EXPORT uint32_t armsx2_lsfg_abi_version(void)
{
return ARMSX2_LSFG_ABI_VERSION;
}
LSFG_EXPORT const char* armsx2_lsfg_last_error(void)
{
return t_last_error.c_str();
}
LSFG_EXPORT int armsx2_lsfg_initialize(uint64_t device_uuid, int is_hdr, float flow_scale,
uint32_t generation_count, int performance, armsx2_lsfg_shader_fn loader, void* user)
{
ClearError();
if (!loader)
{
SetError("no shader loader supplied");
return -1;
}
// The std::function wrapper is constructed HERE, on this side of the boundary, so the
// std::vector it returns is allocated and freed by the same libc++ that framegen uses.
// Building it on the caller's side would hand framegen a vector from a different
// allocator — the exact hazard this shim exists to prevent.
const auto bridge = [loader, user](const std::string& name) -> std::vector<uint8_t> {
const uint8_t* data = nullptr;
uint32_t size = 0;
if (loader(user, name.c_str(), &data, &size) != 0 || !data || size == 0)
throw std::runtime_error("shader '" + name + "' could not be loaded");
return std::vector<uint8_t>(data, data + size);
};
// Recorded BEFORE the call, so a throw out of initialise cannot leave the family flag and
// whatever partial state framegen kept disagreeing about which library owns the teardown.
g_performance = (performance != 0);
try
{
if (g_performance)
LSFG_3_1P::initialize(device_uuid, is_hdr != 0, flow_scale, generation_count, bridge);
else
LSFG_3_1::initialize(device_uuid, is_hdr != 0, flow_scale, generation_count, bridge);
return 0;
}
catch (const std::exception& ex)
{
SetError(ex.what());
return -1;
}
catch (...)
{
SetError("unknown exception during initialise");
return -1;
}
}
LSFG_EXPORT int32_t armsx2_lsfg_create_context(AHardwareBuffer* in0, AHardwareBuffer* in1,
AHardwareBuffer* const* outs, uint32_t out_count, uint32_t width, uint32_t height, uint32_t format)
{
ClearError();
try
{
std::vector<AHardwareBuffer*> out_vec(outs, outs + out_count);
const VkExtent2D extent = {width, height};
if (g_performance)
return LSFG_3_1P::createContextFromAHB(in0, in1, out_vec, extent, static_cast<VkFormat>(format));
return LSFG_3_1::createContextFromAHB(in0, in1, out_vec, extent, static_cast<VkFormat>(format));
}
catch (const std::exception& ex)
{
SetError(ex.what());
return -1;
}
catch (...)
{
SetError("unknown exception creating the context");
return -1;
}
}
LSFG_EXPORT int armsx2_lsfg_present(int32_t context)
{
ClearError();
try
{
// No semaphores: on Android framegen has no exportable cross-device semaphore (Turnip
// rejects OPAQUE_FD on AHB-imported memory), so the caller brackets this with idle waits
// instead. Passing -1 and an empty list is upstream's own Android path.
if (g_performance)
LSFG_3_1P::presentContext(context, -1, {});
else
LSFG_3_1::presentContext(context, -1, {});
return 0;
}
catch (const std::exception& ex)
{
SetError(ex.what());
return -1;
}
catch (...)
{
SetError("unknown exception during generation");
return -1;
}
}
LSFG_EXPORT void armsx2_lsfg_wait_idle(void)
{
try
{
if (g_performance)
LSFG_3_1P::waitIdle();
else
LSFG_3_1::waitIdle();
}
catch (...)
{
// A failed idle leaves nothing for the caller to do differently, and this is on the
// teardown path where throwing would be worse than continuing.
}
}
LSFG_EXPORT void armsx2_lsfg_delete_context(int32_t context)
{
try
{
if (g_performance)
LSFG_3_1P::deleteContext(context);
else
LSFG_3_1::deleteContext(context);
}
catch (...)
{
}
}
LSFG_EXPORT void armsx2_lsfg_finalize(void)
{
try
{
if (g_performance)
LSFG_3_1P::finalize();
else
LSFG_3_1::finalize();
}
catch (...)
{
}
}
@@ -1,86 +0,0 @@
// SPDX-FileCopyrightText: 2002-2026 PCSX2 Dev Team
// SPDX-License-Identifier: GPL-3.0+
#pragma once
// C ABI for libarmsx2_lsfg.so — the frame-generation backend, isolated in its own shared
// object and reached by dlopen/dlsym from GSLsfg.cpp.
//
// WHY A SEPARATE .so, AND WHY C
//
// LSFG's framegen links volk, which DEFINES 759 globals named vkCreateImage, vkQueueSubmit,
// and so on — exactly the names PCSX2's own VKLoader.cpp defines. Linking them into one
// library is a duplicate-symbol error at best. At worst the linker merges them and
// framegen's volkLoadDevice() call, which it makes against ITS OWN VkDevice, silently
// repoints every entry point the GS renderer uses. Every subsequent vkCmdDraw would go to
// the wrong device, and it would present as a driver crash with nothing pointing back here.
// A separate shared object gives volk its own copy of those globals, which is the only
// arrangement where both loaders can coexist.
//
// The interface is C because the app builds with ANDROID_STL=c++_static: each .so carries
// its own libc++, so an std::vector or std::function crossing this boundary would be two
// unrelated types that happen to share a name. Nothing here is richer than a pointer, an
// integer, or a function pointer.
//
// Errors come back as codes, never exceptions. framegen throws (LSFG::vulkan_error) and so
// does the DXBC translator; the shim catches everything and stores the message for
// armsx2_lsfg_last_error(). An exception must never reach the unwinder in the caller's
// shared object.
#include <stdint.h>
#ifdef __cplusplus
extern "C" {
#endif
struct AHardwareBuffer;
#define ARMSX2_LSFG_ABI_VERSION 2
/// Fill `out_data`/`out_size` with SPIR-V for the shader framegen asked for by `name`.
/// The buffer must stay valid until the next call to this callback or until initialise
/// returns, whichever comes first — framegen copies it into a VkShaderModule immediately.
/// Return 0 on success, non-zero to fail the initialise.
typedef int (*armsx2_lsfg_shader_fn)(
void* user, const char* name, const uint8_t** out_data, uint32_t* out_size);
/// ABI version this .so was built against; mismatch means do not use it.
typedef uint32_t (*pfn_armsx2_lsfg_abi_version)(void);
/// Bring the library up. `generation_count` is the number of frames to interpolate between
/// each pair of real ones (multiplier - 1). Returns 0 on success.
///
/// `flow_scale` is a DIVISOR applied to the input extent to size the optical-flow pyramid:
/// framegen computes `flowExtent = inputExtent / flow_scale`, so 1.0 is full resolution and
/// larger values are cheaper. Upstream's own layer reaches this by passing `1.0 / conf.flowScale`
/// from a [0.25, 1.0] fraction, which is why the number arriving here is >= 1.0.
///
/// `performance` selects LSFG 3.1p, a lighter shader family, in place of 3.1. It is fixed for
/// the lifetime of the library: the two keep entirely separate device state and context tables,
/// so a context created by one cannot be presented or destroyed through the other.
typedef int (*pfn_armsx2_lsfg_initialize)(uint64_t device_uuid, int is_hdr, float flow_scale,
uint32_t generation_count, int performance, armsx2_lsfg_shader_fn loader, void* user);
/// Create the interpolation context over caller-owned AHardwareBuffers. `format` is a
/// VkFormat. Returns the context id, or -1 on failure.
typedef int32_t (*pfn_armsx2_lsfg_create_context)(struct AHardwareBuffer* in0,
struct AHardwareBuffer* in1, struct AHardwareBuffer* const* outs, uint32_t out_count,
uint32_t width, uint32_t height, uint32_t format);
/// Run generation for one frame. Returns 0 on success.
typedef int (*pfn_armsx2_lsfg_present)(int32_t context);
/// Block until the library's own device is idle. On Android this is the only barrier that
/// exists between its device and ours — there is no exportable cross-device semaphore.
typedef void (*pfn_armsx2_lsfg_wait_idle)(void);
typedef void (*pfn_armsx2_lsfg_delete_context)(int32_t context);
typedef void (*pfn_armsx2_lsfg_finalize)(void);
/// Message from the most recent failure on this thread, or "" if there was none. Valid until
/// the next failing call on the same thread.
typedef const char* (*pfn_armsx2_lsfg_last_error)(void);
#ifdef __cplusplus
} // extern "C"
#endif
@@ -76,11 +76,15 @@ add_subdirectory(3rdparty/adrenotools)
# compiles out, so a missing Rust toolchain can never break the build.
add_subdirectory(3rdparty/librashader)
# LSFG: Lossless Scaling frame generation (github flavour only see the subdir CMakeLists
# for the licence split and why we take only the MIT framegen library). OPTIONAL in exactly
# the same way: ARMSX2_ENABLE_LSFG defaults OFF, and only the github flavour turns it on, so
# a play build never fetches it and the feature compiles out.
add_subdirectory(3rdparty/lsfg)
# LSFG: Lossless Scaling frame generation, github flavour only ARMSX2_ENABLE_LSFG defaults
# OFF and only the github flavour turns it on, so a play build compiles the feature out.
#
# There is no 3rdparty/lsfg subdirectory any more. The implementation was replaced by the
# port of Eden's (eden-emu PR #4263), which runs as ordinary compute on the emulator's own
# device and needs NONE of what the old one dragged in: no lsfg-vk-android framegen library,
# no DXVK dxbc compiler, no pe-parse, no volk, and so no separate .so to dlopen. The whole
# subtree and its ~130 MB fetch are gone; the ported sources live in
# pcsx2/GS/Renderers/Vulkan/FrameGen and build straight into the core.
# Discord Social SDK (rich presence + friends). Staged by hand rather than consumed as the
# vendor .aar: that .aar's manifest declares RECORD_AUDIO and four foreground-service
@@ -199,13 +203,10 @@ endif()
# compile into the separate PCSX2_LTO target. A define placed on PCSX2 would never reach it,
# the code behind #ifdef ARMSX2_HAS_LSFG would vanish, and the build would succeed anyway
# shipping the toggle with nothing behind it. Verify with `strings` on the core, not the log.
if(ARMSX2_HAVE_LSFG)
target_link_libraries(PCSX2_FLAGS INTERFACE ${ARMSX2_LSFG_LIBRARIES})
target_include_directories(PCSX2_FLAGS INTERFACE ${ARMSX2_LSFG_INCLUDE})
if(ARMSX2_ENABLE_LSFG)
# Only the define is needed now the sources are part of the core, so there is nothing to
# link and nothing to add_dependencies on. Gated on ARMSX2_ENABLE_LSFG rather than the old
# ARMSX2_HAVE_LSFG, which was set by the subdirectory that no longer exists; leaving that
# test in place would silently drop the define and ship the toggle with nothing behind it.
target_compile_definitions(PCSX2_FLAGS INTERFACE ARMSX2_HAS_LSFG=1)
target_link_libraries(${ARMSX2_EMUCORE_LIBRARY_NAME} PRIVATE ${ARMSX2_LSFG_LIBRARIES})
# libarmsx2_lsfg.so is dlopen'd, not linked, so nothing here would otherwise make the
# build produce it and a missing .so is invisible until frame generation is switched on
# at runtime. State the dependency so it is built and packaged with every core.
add_dependencies(${ARMSX2_EMUCORE_LIBRARY_NAME} armsx2_lsfg)
endif()
+8 -3
View File
@@ -88,9 +88,14 @@ echo "-- REQUEST_INSTALL_PACKAGES must be ABSENT (self-updating violates Play po
if unzip -p "$OUTPUT_AAB" base/manifest/AndroidManifest.xml | strings | grep -q "REQUEST_INSTALL_PACKAGES"; then
echo " !! FATAL: REQUEST_INSTALL_PACKAGES present in play AAB (in-app updater leaked into the Play build)" >&2; exit 1
else echo " absent OK"; fi
echo "-- libarmsx2_lsfg.so must be ABSENT (frame generation is github-flavour only) --"
if unzip -l "$OUTPUT_AAB" | grep -q "libarmsx2_lsfg.so"; then
echo " !! FATAL: libarmsx2_lsfg.so present in play AAB (LSFG leaked into the Play build)" >&2; exit 1
echo "-- no frame-generation code in the core (github flavour only) --"
# There is no separate libarmsx2_lsfg.so any more: frame generation is compiled into the core
# itself, gated on ARMSX2_ENABLE_LSFG which the play flavour sets to OFF. So the check moved
# from "is that file packaged" — which can no longer be true either way, and would therefore
# pass forever without proving anything — to looking inside the core for a symbol only the
# ported implementation defines.
if unzip -p "$OUTPUT_AAB" 'base/lib/arm64-v8a/libemucore_*.so' 2>/dev/null | LC_ALL=C grep -aq "LsfgChain"; then
echo " !! FATAL: frame-generation code present in the play core (ARMSX2_ENABLE_LSFG leaked ON)" >&2; exit 1
else echo " absent OK"; fi
echo "-- no frame-generation text at all (the Play build has no LSFG whatsoever) --"
# The strongest of these checks, and the one someone looking would actually notice. Every