mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daed55c427 | ||
|
|
43d26f3720 | ||
|
|
30f4d93733 | ||
|
|
1e9f8c70da | ||
|
|
e7ab163b54 | ||
|
|
e66ce8328a | ||
|
|
a378f38ba6 | ||
|
|
f2bce78ba1 | ||
|
|
f55a246f22 | ||
|
|
8759a648f7 | ||
|
|
5dc669bc7f | ||
|
|
11f960fde9 | ||
|
|
c537563d14 | ||
|
|
f8b4d63323 | ||
|
|
eda6870a47 | ||
|
|
b4a552d697 | ||
|
|
caaadbe266 | ||
|
|
b5f449ccb6 | ||
|
|
e05ea4d219 |
@@ -34,8 +34,8 @@ android {
|
||||
// agree -- an APK that installs below its core's target is a dlopen failure at boot.
|
||||
minSdk = (project.findProperty("armsx3.minSdk") as String?)?.toInt() ?: 33
|
||||
targetSdk = 37
|
||||
versionCode = 21
|
||||
versionName = "0.9.4.1"
|
||||
versionCode = 22
|
||||
versionName = "0.9.4.2"
|
||||
|
||||
// ARMSX2's UI reads these. STORAGE_ALL_FILES gates the all-files storage path in
|
||||
// onboarding; IN_APP_UPDATER gates the in-app GitHub-release updater.
|
||||
|
||||
@@ -360,16 +360,33 @@ void applyPresence(const std::string& serial, const std::string& title,
|
||||
// So: use the cover URL when we have one (the API accepts an https URL here,
|
||||
// no upload required), and otherwise send no assets rather than a key we
|
||||
// cannot guarantee exists.
|
||||
const bool has_art = !cover.empty();
|
||||
if (has_art) {
|
||||
discordpp::ActivityAssets assets{};
|
||||
// The app icon, by URL rather than by asset key, for the reason above: a key that is not in
|
||||
// the portal makes Discord reject the whole update, while an https URL always resolves. It is
|
||||
// the launcher icon out of this repository, so it cannot drift from the app it represents.
|
||||
static constexpr const char* kLogoUrl =
|
||||
"https://raw.githubusercontent.com/ARMSX2/ARMSX3/master/android/armsx3-ui/app/src/main/"
|
||||
"res/mipmap-xxxhdpi/ic_launcher.png";
|
||||
|
||||
discordpp::ActivityAssets assets{};
|
||||
|
||||
if (!cover.empty()) {
|
||||
// Cover art large, emulator small -- the shape ARMSX2 uses, and the one that reads as
|
||||
// "playing this, on this" rather than just naming a game.
|
||||
assets.SetLargeImage(cover);
|
||||
if (!title.empty()) {
|
||||
assets.SetLargeText(title);
|
||||
}
|
||||
activity.SetAssets(assets);
|
||||
assets.SetSmallImage(kLogoUrl);
|
||||
assets.SetSmallText("ARMSX3");
|
||||
} else {
|
||||
// No cover: the logo takes the large slot instead of sending no assets at all, which is
|
||||
// what used to happen and left the card blank.
|
||||
assets.SetLargeImage(kLogoUrl);
|
||||
assets.SetLargeText("ARMSX3");
|
||||
}
|
||||
|
||||
activity.SetAssets(assets);
|
||||
|
||||
LOGI("presence: title='%s' serial='%s' cover=%s ra=%s", title.c_str(), serial.c_str(),
|
||||
cover.empty() ? "none" : "yes", ra.empty() ? "none" : "yes");
|
||||
|
||||
|
||||
@@ -134,6 +134,8 @@ data class Ps3Settings(
|
||||
val frameGenPerformance: Boolean = true,
|
||||
// Optical-flow resolution as a percentage of full; lower is cheaper and blurrier in motion.
|
||||
val frameGenFlowScale: Int = 100,
|
||||
/** Hz to hold, or 0 for the fixed multiplier. Non-zero selects adaptive pacing. */
|
||||
val frameGenTargetRate: Int = 0,
|
||||
val writeColorBuffers: Boolean = false,
|
||||
val writeDepthBuffer: Boolean = false,
|
||||
val readColorBuffers: Boolean = false,
|
||||
@@ -1109,6 +1111,10 @@ data class Settings(
|
||||
put("PS3/Video", "Frame Generation", "enum", ps3.frameGeneration.toString())
|
||||
put("PS3/Video", "Frame Generation Performance Mode", "bool", ps3.frameGenPerformance.toString())
|
||||
put("PS3/Video", "Frame Generation Flow Scale", "int", ps3.frameGenFlowScale.toString())
|
||||
put("PS3/Video", "Frame Generation Target Rate", "int", ps3.frameGenTargetRate.toString())
|
||||
// Temporary: the value reaches the core as 0 whatever the UI is set to, and all six
|
||||
// plumbing sites read correctly. This says what the object being applied actually holds.
|
||||
android.util.Log.i("FRAMEGEN", "applyTo: targetRate=${ps3.frameGenTargetRate} mult=${ps3.frameGeneration}")
|
||||
put("PS3/Video", "Write Color Buffers", "bool", ps3.writeColorBuffers.toString())
|
||||
put("PS3/Video", "Write Depth Buffer", "bool", ps3.writeDepthBuffer.toString())
|
||||
put("PS3/Video", "Read Color Buffers", "bool", ps3.readColorBuffers.toString())
|
||||
@@ -2088,6 +2094,7 @@ data class Settings(
|
||||
put("ps3FrameGeneration", ps3.frameGeneration)
|
||||
put("ps3FrameGenPerformance", ps3.frameGenPerformance)
|
||||
put("ps3FrameGenFlowScale", ps3.frameGenFlowScale)
|
||||
put("ps3FrameGenTargetRate", ps3.frameGenTargetRate)
|
||||
put("ps3WriteColorBuffers", ps3.writeColorBuffers)
|
||||
put("ps3GpuTurbo", ps3.gpuTurbo)
|
||||
put("ps3SilenceAllLogs", ps3.silenceAllLogs)
|
||||
@@ -2442,6 +2449,7 @@ data class Settings(
|
||||
frameGeneration = json.optInt("ps3FrameGeneration", def.ps3.frameGeneration),
|
||||
frameGenPerformance = json.optBoolean("ps3FrameGenPerformance", def.ps3.frameGenPerformance),
|
||||
frameGenFlowScale = json.optInt("ps3FrameGenFlowScale", def.ps3.frameGenFlowScale),
|
||||
frameGenTargetRate = json.optInt("ps3FrameGenTargetRate", def.ps3.frameGenTargetRate),
|
||||
writeColorBuffers = json.optBoolean("ps3WriteColorBuffers", def.ps3.writeColorBuffers),
|
||||
gpuTurbo = json.optBoolean("ps3GpuTurbo", def.ps3.gpuTurbo),
|
||||
silenceAllLogs = json.optBoolean("ps3SilenceAllLogs", def.ps3.silenceAllLogs),
|
||||
@@ -2781,6 +2789,7 @@ data class Settings(
|
||||
if (current.ps3.frameGeneration != base.ps3.frameGeneration) j.put("ps3FrameGeneration", current.ps3.frameGeneration)
|
||||
if (current.ps3.frameGenPerformance != base.ps3.frameGenPerformance) j.put("ps3FrameGenPerformance", current.ps3.frameGenPerformance)
|
||||
if (current.ps3.frameGenFlowScale != base.ps3.frameGenFlowScale) j.put("ps3FrameGenFlowScale", current.ps3.frameGenFlowScale)
|
||||
if (current.ps3.frameGenTargetRate != base.ps3.frameGenTargetRate) j.put("ps3FrameGenTargetRate", current.ps3.frameGenTargetRate)
|
||||
if (current.ps3.writeColorBuffers != base.ps3.writeColorBuffers) j.put("ps3WriteColorBuffers", current.ps3.writeColorBuffers)
|
||||
if (current.ps3.gpuTurbo != base.ps3.gpuTurbo) j.put("ps3GpuTurbo", current.ps3.gpuTurbo)
|
||||
if (current.ps3.silenceAllLogs != base.ps3.silenceAllLogs) j.put("ps3SilenceAllLogs", current.ps3.silenceAllLogs)
|
||||
@@ -3096,6 +3105,7 @@ data class Settings(
|
||||
frameGeneration = if (overrides.has("ps3FrameGeneration")) overrides.getInt("ps3FrameGeneration") else base.ps3.frameGeneration,
|
||||
frameGenPerformance = if (overrides.has("ps3FrameGenPerformance")) overrides.getBoolean("ps3FrameGenPerformance") else base.ps3.frameGenPerformance,
|
||||
frameGenFlowScale = if (overrides.has("ps3FrameGenFlowScale")) overrides.getInt("ps3FrameGenFlowScale") else base.ps3.frameGenFlowScale,
|
||||
frameGenTargetRate = if (overrides.has("ps3FrameGenTargetRate")) overrides.getInt("ps3FrameGenTargetRate") else base.ps3.frameGenTargetRate,
|
||||
writeColorBuffers = if (overrides.has("ps3WriteColorBuffers")) overrides.getBoolean("ps3WriteColorBuffers") else base.ps3.writeColorBuffers,
|
||||
gpuTurbo = if (overrides.has("ps3GpuTurbo")) overrides.getBoolean("ps3GpuTurbo") else base.ps3.gpuTurbo,
|
||||
silenceAllLogs = if (overrides.has("ps3SilenceAllLogs")) overrides.getBoolean("ps3SilenceAllLogs") else base.ps3.silenceAllLogs,
|
||||
|
||||
@@ -1155,6 +1155,8 @@ val EN: Map<String, String> = mapOf(
|
||||
"perf.framegen.performance.label" to "Performance shaders",
|
||||
"perf.framegen.performance.description" to "Use Lossless Scaling's lighter 3.1p shaders instead of the full-quality 3.1 set. Cheaper to run and slightly softer in motion \u2014 on by default, because the quality set usually costs more than the frames it buys on a phone. Both come from the file you imported, so switching does not need another import.\n\nTakes effect when frame generation next starts: turn it off and on again, or restart the game.",
|
||||
"perf.framegen.flowScale.label" to "Motion detail",
|
||||
"perf.framegen.targetRate.label" to "Target refresh rate",
|
||||
"perf.framegen.targetRate.description" to "Generate as many frames as it takes to hold this rate, instead of a fixed multiplier. Steadies the picture when the game's own frame rate moves. Off uses the multiplier above.",
|
||||
"perf.framegen.flowScale.description" to "How finely motion is measured between frames, as a percentage of full resolution. Lower is faster and blurrier around moving edges. Drop this before dropping the multiplier if frame generation is costing more than it gives.\n\nTakes effect when frame generation next starts.",
|
||||
"perf.framegen.off" to "Off",
|
||||
"perf.framegen.x2" to "x2",
|
||||
|
||||
@@ -1059,6 +1059,54 @@ private fun PerformancePane(state: EmulationMenuUiState, viewModel: EmulationMen
|
||||
selected = settings.ps3.frameGeneration,
|
||||
onSelect = { v -> viewModel.updateSettings { it.copy(ps3 = it.ps3.copy(frameGeneration = v)) } },
|
||||
)
|
||||
|
||||
// The rest of frame generation, which until now only existed in the main settings
|
||||
// screen. Someone who opens this menu mid-game is here to change exactly these:
|
||||
// the multiplier alone cannot answer "it is generating, but the picture is unsteady"
|
||||
// (target rate) or "it is generating, but too expensive" (flow scale, performance).
|
||||
HorizontalOptions(
|
||||
title = str("perf.framegen.targetRate.label"),
|
||||
options = listOf(0 to str("perf.framegen.off"), 60 to "60 Hz", 90 to "90 Hz", 120 to "120 Hz"),
|
||||
selected = settings.ps3.frameGenTargetRate,
|
||||
onSelect = { v ->
|
||||
android.util.Log.i("FRAMEGEN", "pause menu: target rate chip -> $v")
|
||||
viewModel.updateSettings { it.copy(ps3 = it.ps3.copy(frameGenTargetRate = v)) }
|
||||
},
|
||||
)
|
||||
// Motion detail is continuous, so it gets a slider rather than three stops -- the
|
||||
// useful values are wherever the picture stops improving on a given game, not a set
|
||||
// someone picked in advance. 25 is the floor the core clamps to.
|
||||
Column(Modifier.fillMaxWidth().padding(horizontal = 4.dp, vertical = 6.dp)) {
|
||||
Row(
|
||||
Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
str("perf.framegen.flowScale.label"),
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
Text(
|
||||
"${settings.ps3.frameGenFlowScale}%",
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
Slider(
|
||||
value = settings.ps3.frameGenFlowScale.coerceIn(25, 100).toFloat(),
|
||||
onValueChange = { v ->
|
||||
viewModel.updateSettings {
|
||||
it.copy(ps3 = it.ps3.copy(frameGenFlowScale = Math.round(v).coerceIn(25, 100)))
|
||||
}
|
||||
},
|
||||
valueRange = 25f..100f,
|
||||
)
|
||||
}
|
||||
MenuSwitchRow(
|
||||
str("perf.framegen.performance.label"),
|
||||
settings.ps3.frameGenPerformance,
|
||||
) { v -> viewModel.updateSettings { it.copy(ps3 = it.ps3.copy(frameGenPerformance = v)) } }
|
||||
}
|
||||
Spacer(Modifier.height(10.dp))
|
||||
}
|
||||
|
||||
@@ -442,6 +442,19 @@ fun PerformanceTab(state: MutableState<Settings>) {
|
||||
onChange = { apply(s.copy(ps3 = s.ps3.copy(frameGenFlowScale = it))) },
|
||||
)
|
||||
SettingsDivider()
|
||||
SettingsDivider()
|
||||
SegmentedGridRow(
|
||||
label = str("perf.framegen.targetRate.label"),
|
||||
options = listOf(str("perf.framegen.off"), "60 Hz", "90 Hz", "120 Hz"),
|
||||
selectedIndex = when (s.ps3.frameGenTargetRate) { 60 -> 1; 90 -> 2; 120 -> 3; else -> 0 },
|
||||
columns = 4,
|
||||
description = str("perf.framegen.targetRate.description"),
|
||||
onChange = { idx ->
|
||||
val hz = when (idx) { 1 -> 60; 2 -> 90; 3 -> 120; else -> 0 }
|
||||
apply(s.copy(ps3 = s.ps3.copy(frameGenTargetRate = hz)))
|
||||
},
|
||||
)
|
||||
SettingsDivider()
|
||||
FrameGenShaderRow()
|
||||
}
|
||||
SettingsDivider()
|
||||
|
||||
@@ -621,6 +621,7 @@ object Rpcs3Bridge {
|
||||
"Frame Generation" -> Rpcs3Settings.setFrameGeneration(asInt(value))
|
||||
"Frame Generation Performance Mode" -> Rpcs3Settings.setFrameGenPerformance(asBool(value))
|
||||
"Frame Generation Flow Scale" -> Rpcs3Settings.setFrameGenFlowScale(asInt(value))
|
||||
"Frame Generation Target Rate" -> Rpcs3Settings.setFrameGenTargetRate(asInt(value))
|
||||
"Write Color Buffers" -> Rpcs3Settings.setWriteColorBuffers(asBool(value))
|
||||
"Write Depth Buffer" -> Rpcs3Settings.setWriteDepthBuffer(asBool(value))
|
||||
"Read Color Buffers" -> Rpcs3Settings.setReadColorBuffers(asBool(value))
|
||||
|
||||
@@ -493,6 +493,10 @@ object Rpcs3Settings {
|
||||
fun setFrameGenFlowScale(percent: Int) =
|
||||
setInt("$VIDEO@@Frame Generation Flow Scale", percent.coerceIn(25, 100))
|
||||
|
||||
/** Hz to hold, or 0 for the fixed multiplier. Bounds match what the core accepts. */
|
||||
fun setFrameGenTargetRate(hz: Int) =
|
||||
setInt("$VIDEO@@Frame Generation Target Rate", hz.coerceIn(0, 480))
|
||||
|
||||
fun setFrameGeneration(index: Int) =
|
||||
setEnum("$VIDEO@@Frame Generation", FRAME_GENERATION.getOrElse(index) { FRAME_GENERATION[0] })
|
||||
|
||||
|
||||
@@ -648,6 +648,25 @@ if(TARGET 3rdparty_vulkan)
|
||||
RSX/VK/vkutils/sampler.cpp
|
||||
RSX/VK/vkutils/shared.cpp
|
||||
RSX/VK/vkutils/unique_resource.cpp
|
||||
# Frame generation, ported from Eden (CamilleLaVey, eden PR #4263) via ARMSX2, and
|
||||
# relicensed GPL-2.0-or-later by its author for use here. Runs the passes on OUR device
|
||||
# in our own present path -- the earlier implementation lived behind a dlopen'd .so with
|
||||
# its own VkDevice, which forced every frame through an AHardwareBuffer round-trip.
|
||||
RSX/VK/FrameGen/FrameGen.cpp
|
||||
RSX/VK/FrameGen/FrameGenPacer.cpp
|
||||
RSX/VK/FrameGen/LosslessDll.cpp
|
||||
RSX/VK/FrameGen/LsfgAlpha.cpp
|
||||
RSX/VK/FrameGen/LsfgBeta.cpp
|
||||
RSX/VK/FrameGen/LsfgChain.cpp
|
||||
RSX/VK/FrameGen/LsfgCommon.cpp
|
||||
RSX/VK/FrameGen/LsfgDelta.cpp
|
||||
RSX/VK/FrameGen/LsfgGamma.cpp
|
||||
RSX/VK/FrameGen/LsfgGenerate.cpp
|
||||
RSX/VK/FrameGen/LsfgMipmaps.cpp
|
||||
RSX/VK/FrameGen/LsfgShaders.cpp
|
||||
RSX/VK/FrameGen/LsfgTranslate.cpp
|
||||
RSX/VK/FrameGen/LsfgUtil.cpp
|
||||
RSX/VK/FrameGen/LsfgVkCompat.cpp
|
||||
RSX/VK/VKAsyncScheduler.cpp
|
||||
RSX/VK/VKCommandStream.cpp
|
||||
RSX/VK/VKCommonDecompiler.cpp
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Camille LaVey
|
||||
// Relicensed to GPL-2.0-or-later for use in ARMSX3 by Camille LaVey, founder of the Eden
|
||||
// Emulator Project and author of this implementation (eden-emu PR #4263), who granted
|
||||
// permission for it to be used here. ARMSX3 derives from RPCS3, which is GPL-2.0-ONLY with no
|
||||
// "or later" clause, so the original GPL-3.0-or-later terms could not be carried across --
|
||||
// ARMSX2 is GPL-3.0 and ships it unchanged. Eden is unaffected: "or later" leaves its own use
|
||||
// exactly as it was.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-2.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 "../vkutils/device.h"
|
||||
#include "FrameGenConfig.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>(::vk::lsfg::flow_scale(), 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 (::vk::lsfg::flow_scale() < 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_, const ::vk::render_device* 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 || !::vk::lsfg::enabled()) {
|
||||
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 the renderer'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
|
||||
// the renderer's own submit: that submits the frame's command buffer, which is the
|
||||
// wrong thing to do halfway through a present.
|
||||
// PCSX2 called GSDeviceVK::WaitForGPUIdle() here. RPCS3 has no equivalent wrapper, and the
|
||||
// renderer's own teardown at VKGSRender.cpp:859 uses the plain Vulkan call, so use that.
|
||||
vkDeviceWaitIdle(static_cast<VkDevice>(*gs_device));
|
||||
}
|
||||
|
||||
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,104 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Camille LaVey
|
||||
// Relicensed to GPL-2.0-or-later for use in ARMSX3 by Camille LaVey, founder of the Eden
|
||||
// Emulator Project and author of this implementation (eden-emu PR #4263), who granted
|
||||
// permission for it to be used here. ARMSX3 derives from RPCS3, which is GPL-2.0-ONLY with no
|
||||
// "or later" clause, so the original GPL-3.0-or-later terms could not be carried across --
|
||||
// ARMSX2 is GPL-3.0 and ships it unchanged. Eden is unaffected: "or later" leaves its own use
|
||||
// exactly as it was.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-2.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 the render device and record into the 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
|
||||
|
||||
namespace vk { class render_device; }
|
||||
|
||||
#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, const ::vk::render_device* device);
|
||||
~FrameGen();
|
||||
|
||||
/// Feeds the frame about to be presented into the chain.
|
||||
///
|
||||
/// ★ The caller supplies [cmdbuf]. It is NOT taken from the renderer's current buffer:
|
||||
/// 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;
|
||||
const ::vk::render_device* 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
|
||||
@@ -0,0 +1,58 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// The settings the ported passes read.
|
||||
//
|
||||
// The Eden/ARMSX2 sources read a global `GSConfig` with fields named LsfgEnabled, LsfgMultiplier
|
||||
// and so on. RPCS3 keeps the same values in g_cfg under different names and different types --
|
||||
// a mode enum rather than a multiplier, most obviously -- so this is the one place the two
|
||||
// vocabularies meet. Keeping it in a single header means a future merge from Eden touches the
|
||||
// passes and not the wiring.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Emu/system_config.h"
|
||||
|
||||
namespace vk::lsfg
|
||||
{
|
||||
inline bool enabled()
|
||||
{
|
||||
return g_cfg.video.frame_generation != frame_generation_mode::off;
|
||||
}
|
||||
|
||||
/// How many frames to show per rendered one. PCSX2 stored this directly; RPCS3 stores a mode.
|
||||
inline u32 multiplier()
|
||||
{
|
||||
switch (g_cfg.video.frame_generation)
|
||||
{
|
||||
case frame_generation_mode::x3: return 3;
|
||||
case frame_generation_mode::x4: return 4;
|
||||
default: return 2;
|
||||
}
|
||||
}
|
||||
|
||||
/// 25..100. The optical flow runs at this fraction of full resolution.
|
||||
inline u32 flow_scale()
|
||||
{
|
||||
return g_cfg.video.frame_generation_flow_scale.get();
|
||||
}
|
||||
|
||||
/// Hz to hold, or 0 to use multiplier() unchanged. Non-zero selects adaptive pacing.
|
||||
inline u32 target_rate()
|
||||
{
|
||||
// 0 keeps the fixed multiplier.
|
||||
//
|
||||
// Forcing a 60Hz target here as a test made things WORSE, and the reason is worth
|
||||
// keeping: at ~36 real fps a 60Hz target wants 0.67 extra frames per frame, and since
|
||||
// only whole frames can be made the pacer produced 1, 0, 0, 0, 0. Sporadic generation is
|
||||
// less even than none, so the displayed rate barely moved and the judder got worse. A
|
||||
// target only helps when it is reachable in whole frames from the rate the game is
|
||||
// actually managing -- 120 against 36 wants two, 60 against 36 wants two thirds of one.
|
||||
return g_cfg.video.frame_generation_target_rate.get();
|
||||
}
|
||||
|
||||
/// The cheaper 3.1p shader family.
|
||||
inline bool performance_mode()
|
||||
{
|
||||
return g_cfg.video.frame_generation_performance.get();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Camille LaVey
|
||||
// Relicensed to GPL-2.0-or-later for use in ARMSX3 by Camille LaVey, founder of the Eden
|
||||
// Emulator Project and author of this implementation (eden-emu PR #4263), who granted
|
||||
// permission for it to be used here. ARMSX3 derives from RPCS3, which is GPL-2.0-ONLY with no
|
||||
// "or later" clause, so the original GPL-3.0-or-later terms could not be carried across --
|
||||
// ARMSX2 is GPL-3.0 and ships it unchanged. Eden is unaffected: "or later" leaves its own use
|
||||
// exactly as it was.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// Ported from Eden (eden-emu PR #4263). The pacing algorithm is unchanged — only the three
|
||||
// settings reads are remapped onto GSConfig via FrameGenSettings. Keeping it verbatim is
|
||||
// deliberate: this is subtle control logic and staying diffable against upstream is worth more
|
||||
// than restyling it to PCSX2's brace conventions.
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <utility>
|
||||
|
||||
#include "FrameGenPacer.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, FrameGenSettings::MaxGenerations());
|
||||
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 = FrameGenSettings::TargetRate();
|
||||
|
||||
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(FrameGenSettings::Generations(), 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
|
||||
@@ -0,0 +1,64 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Camille LaVey
|
||||
// Relicensed to GPL-2.0-or-later for use in ARMSX3 by Camille LaVey, founder of the Eden
|
||||
// Emulator Project and author of this implementation (eden-emu PR #4263), who granted
|
||||
// permission for it to be used here. ARMSX3 derives from RPCS3, which is GPL-2.0-ONLY with no
|
||||
// "or later" clause, so the original GPL-3.0-or-later terms could not be carried across --
|
||||
// ARMSX2 is GPL-3.0 and ships it unchanged. Eden is unaffected: "or later" leaves its own use
|
||||
// exactly as it was.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// Ported from Eden (eden-emu PR #4263), src/video_core/renderer_vulkan/present/frame_gen_pacer.h.
|
||||
// Logic unchanged; only the scalar aliases and settings accessors are ours. See FrameGenTypes.h.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "FrameGenTypes.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
|
||||
namespace Vulkan
|
||||
{
|
||||
struct FrameGenPlan
|
||||
{
|
||||
size_t generations{};
|
||||
bool warm{};
|
||||
};
|
||||
|
||||
/// Decides how many frames to interpolate for the frame about to be presented.
|
||||
///
|
||||
/// The naive alternative — always generate LsfgMultiplier-1 frames — is what makes frame
|
||||
/// generation feel worse than no frame generation on a game whose rendered rate moves. This
|
||||
/// watches the real cadence and adapts: it smooths the measured interval, ignores bursts,
|
||||
/// stabilises after a stall, and probes the generation count upward only while the output is
|
||||
/// short of target, backing off with an escalating delay when a probe makes things worse.
|
||||
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
|
||||
@@ -0,0 +1,84 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Camille LaVey
|
||||
// Relicensed to GPL-2.0-or-later for use in ARMSX3 by Camille LaVey, founder of the Eden
|
||||
// Emulator Project and author of this implementation (eden-emu PR #4263), who granted
|
||||
// permission for it to be used here. ARMSX3 derives from RPCS3, which is GPL-2.0-ONLY with no
|
||||
// "or later" clause, so the original GPL-3.0-or-later terms could not be carried across --
|
||||
// ARMSX2 is GPL-3.0 and ships it unchanged. Eden is unaffected: "or later" leaves its own use
|
||||
// exactly as it was.
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// Compatibility shim for code ported from Eden (eden-emu PR #4263).
|
||||
//
|
||||
// The port is deliberately kept close to the original so upstream fixes stay easy to follow.
|
||||
// Eden is a yuzu descendant and leans on two things PCSX2 does not have: an `f32` scalar alias,
|
||||
// and a `Settings::values.*` configuration object. Rather than rewriting every use of those
|
||||
// across ~3000 lines, they are provided here once.
|
||||
//
|
||||
// LICENSING: Eden is GPL-3.0-or-later and PCSX2 is GPL-3.0+, so this code may be combined.
|
||||
// Note this is NOT true of RPCS3, which is GPL-2.0-only — do not carry these files there.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "util/types.hpp"
|
||||
#include "FrameGenConfig.h"
|
||||
#include "Config.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include "FrameGenConfig.h"
|
||||
|
||||
// yuzu/Eden spell the float aliases this way; PCSX2 only defines the integer ones.
|
||||
using f32 = float;
|
||||
using f64 = double;
|
||||
|
||||
namespace VideoCore::FrameGen
|
||||
{
|
||||
/// Hard ceiling on interpolated frames per rendered frame. Matches LSFG_MAX_GENERATIONS in
|
||||
/// the pass code — the shader set only carries weights for three.
|
||||
inline constexpr size_t MAX_GENERATIONS = 3;
|
||||
} // namespace VideoCore::FrameGen
|
||||
|
||||
/// ★ __forceinline_odr, NOT __fi.
|
||||
///
|
||||
/// On GCC/Clang PCSX2's __forceinline expands to __attribute__((always_inline, unused)) with no
|
||||
/// `inline` keyword, so a free function marked __fi in a header gets EXTERNAL linkage and every
|
||||
/// translation unit that includes it emits its own copy — "duplicate symbol" at link, from a
|
||||
/// header that compiles perfectly in isolation. __fi is fine on member functions defined inside a
|
||||
/// class body, which are implicitly inline; that is what the rest of the renderer uses it for.
|
||||
/// RPCS3 spells this FORCE_INLINE (util/types.hpp); it expands to always_inline + inline,
|
||||
/// so these stay ODR-safe in a header exactly as FORCE_INLINE did.
|
||||
namespace Vulkan::FrameGenSettings
|
||||
{
|
||||
/// Interpolated frames per rendered frame, as the user configured it.
|
||||
///
|
||||
/// GSConfig stores the MULTIPLIER (2 = one interpolated frame, 3 = two, ...) because that is
|
||||
/// what the UI shows; the ported code counts GENERATIONS. The two differ by one, and mixing
|
||||
/// them up shows as "x2 looks like x3", so the conversion lives only here.
|
||||
FORCE_INLINE size_t Generations()
|
||||
{
|
||||
const u32 mult = std::max<u32>(::vk::lsfg::multiplier(), 2u);
|
||||
return std::min<size_t>(mult - 1u, VideoCore::FrameGen::MAX_GENERATIONS);
|
||||
}
|
||||
|
||||
/// Upper bound the pacer may probe up to.
|
||||
FORCE_INLINE size_t MaxGenerations()
|
||||
{
|
||||
return ::vk::lsfg::enabled() ? Generations() : 0;
|
||||
}
|
||||
|
||||
/// Target OUTPUT rate in Hz, or 0 to hold the multiplier fixed.
|
||||
///
|
||||
/// This is what makes the pacer adaptive rather than a blind multiplier, and it is the whole
|
||||
/// answer to games that oscillate between 60 and 30fps on a 60Hz panel: at a fixed x2 such a
|
||||
/// game presents 120 then 60, and the panel shows judder at every transition. Given a target
|
||||
/// the pacer instead varies the generation count to hold the output near it — two interpolated
|
||||
/// frames while the game runs at 30, one while it runs at 60.
|
||||
///
|
||||
/// Zero preserves the old fixed-multiplier behaviour exactly, so this is opt-in.
|
||||
FORCE_INLINE f32 TargetRate()
|
||||
{
|
||||
return static_cast<f32>(::vk::lsfg::target_rate());
|
||||
}
|
||||
} // namespace Vulkan::FrameGenSettings
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,93 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Camille LaVey
|
||||
// Relicensed to GPL-2.0-or-later for use in ARMSX3 by Camille LaVey, founder of the Eden
|
||||
// Emulator Project and author of this implementation (eden-emu PR #4263), who granted
|
||||
// permission for it to be used here. ARMSX3 derives from RPCS3, which is GPL-2.0-ONLY with no
|
||||
// "or later" clause, so the original GPL-3.0-or-later terms could not be carried across --
|
||||
// ARMSX2 is GPL-3.0 and ships it unchanged. Eden is unaffected: "or later" leaves its own use
|
||||
// exactly as it was.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// Ported from Eden (eden-emu PR #4263), src/video_core/frame_gen/lossless_dll.h.
|
||||
// Logic unchanged. The one substantive difference is the path type: Eden passes
|
||||
// std::filesystem::path, PCSX2 uses std::string throughout with its own FileSystem/Path
|
||||
// helpers, so every path here is a std::string. See FrameGenTypes.h.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "FrameGenTypes.h"
|
||||
|
||||
#include <array>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace VideoCore::FrameGen {
|
||||
|
||||
enum class LosslessStatus : u32 {
|
||||
Ok,
|
||||
NotInstalled,
|
||||
UnreadableFile,
|
||||
NotPortableExecutable,
|
||||
MissingShaders,
|
||||
TranslationFailed,
|
||||
CacheUnusable,
|
||||
};
|
||||
|
||||
using ShaderResources = std::map<u32, std::vector<u8>>;
|
||||
using ShaderModules = std::map<u32, std::vector<u32>>;
|
||||
|
||||
enum class ShaderVariant : u32 {
|
||||
NativeFp32 = 1,
|
||||
NativeFp16 = 2,
|
||||
};
|
||||
|
||||
namespace PerformanceShader {
|
||||
constexpr u32 MIPMAPS = 255;
|
||||
constexpr u32 GENERATE = 256;
|
||||
constexpr std::array<u32, 4> ALPHA{290, 291, 292, 293};
|
||||
constexpr std::array<u32, 5> BETA{298, 299, 300, 301, 302};
|
||||
constexpr std::array<u32, 5> GAMMA{280, 282, 283, 284, 285};
|
||||
constexpr std::array<u32, 10> DELTA{280, 286, 287, 288, 289, 281, 294, 295, 296, 297};
|
||||
|
||||
constexpr u32 NATIVE_FP16_OFFSET = 49;
|
||||
constexpr u32 NATIVE_FP32_OFFSET = 98;
|
||||
} // namespace PerformanceShader
|
||||
|
||||
[[nodiscard]] std::string GetLosslessDllPath();
|
||||
|
||||
[[nodiscard]] std::string GetShaderCachePath();
|
||||
|
||||
[[nodiscard]] LosslessStatus ReadShaderResources(const std::string& path,
|
||||
ShaderResources& out_resources);
|
||||
|
||||
[[nodiscard]] LosslessStatus ValidateLosslessDll(const std::string& path);
|
||||
|
||||
[[nodiscard]] LosslessStatus GetInstalledLosslessStatus();
|
||||
|
||||
/// Whether the fp16 family may be used, asked of the live render device.
|
||||
///
|
||||
/// THREE callers need the same answer -- the cache writer, LsfgShaders, and the availability
|
||||
/// probe -- and the cache header stores the flags and rejects an entry whose flags differ. Any
|
||||
/// two of them disagreeing means a cache that can never be read: it wrote fp16 and was probed
|
||||
/// with fp32, which reads to a user as "no shaders" immediately after a successful import.
|
||||
[[nodiscard]] bool Float16Allowed();
|
||||
|
||||
/// [allow_fp16]/[prefer_fp16] MUST match what LsfgShaders asks for: the cache header stores them
|
||||
/// and rejects an entry whose flags differ, so a mismatch means the cache written at import can
|
||||
/// never satisfy the read that follows it.
|
||||
[[nodiscard]] LosslessStatus BuildShaderCache(bool allow_fp16, bool prefer_fp16);
|
||||
|
||||
[[nodiscard]] LosslessStatus LoadShaderModules(ShaderModules& out_modules,
|
||||
bool allow_fp16 = false,
|
||||
bool prefer_fp16 = false);
|
||||
|
||||
/// Delete the on-disk SPIR-V cache, forcing a rebuild from the DLL next time.
|
||||
///
|
||||
/// Eden's equivalent also deleted the DLL; see the note in the .cpp for why that half is gone.
|
||||
void ClearShaderCache();
|
||||
|
||||
} // namespace VideoCore::FrameGen
|
||||
@@ -0,0 +1,152 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Camille LaVey
|
||||
// Relicensed to GPL-2.0-or-later for use in ARMSX3 by Camille LaVey, founder of the Eden
|
||||
// Emulator Project and author of this implementation (eden-emu PR #4263), who granted
|
||||
// permission for it to be used here. ARMSX3 derives from RPCS3, which is GPL-2.0-ONLY with no
|
||||
// "or later" clause, so the original GPL-3.0-or-later terms could not be carried across --
|
||||
// ARMSX2 is GPL-3.0 and ships it unchanged. Eden is unaffected: "or later" leaves its own use
|
||||
// exactly as it was.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// Ported from Eden (eden-emu PR #4263), src/video_core/renderer_vulkan/present/lsfg_alpha.cpp.
|
||||
// The pass is unchanged: the descriptor layouts, the tile shift and the barrier ordering are the
|
||||
// shader contract, not style, so this stays verbatim and diffable against upstream. Only the
|
||||
// includes are remapped onto the PCSX2 shim. See LsfgVkCompat.h.
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "LosslessDll.h"
|
||||
#include "LsfgAlpha.h"
|
||||
#include "LsfgShaders.h"
|
||||
#include "LsfgUtil.h"
|
||||
#include "LsfgVkCompat.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
|
||||
@@ -0,0 +1,73 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Camille LaVey
|
||||
// Relicensed to GPL-2.0-or-later for use in ARMSX3 by Camille LaVey, founder of the Eden
|
||||
// Emulator Project and author of this implementation (eden-emu PR #4263), who granted
|
||||
// permission for it to be used here. ARMSX3 derives from RPCS3, which is GPL-2.0-ONLY with no
|
||||
// "or later" clause, so the original GPL-3.0-or-later terms could not be carried across --
|
||||
// ARMSX2 is GPL-3.0 and ships it unchanged. Eden is unaffected: "or later" leaves its own use
|
||||
// exactly as it was.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// Ported from Eden (eden-emu PR #4263), src/video_core/renderer_vulkan/present/lsfg_alpha.h.
|
||||
// Descriptor layouts, dispatch maths and barrier ordering are verbatim; only the includes are
|
||||
// remapped onto the PCSX2 shim. See LsfgVkCompat.h.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
|
||||
#include "FrameGenTypes.h"
|
||||
#include "LsfgCommon.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
|
||||
@@ -0,0 +1,159 @@
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Eden Emulator Project
|
||||
// SPDX-FileCopyrightText: Copyright 2026 Camille LaVey
|
||||
// Relicensed to GPL-2.0-or-later for use in ARMSX3 by Camille LaVey, founder of the Eden
|
||||
// Emulator Project and author of this implementation (eden-emu PR #4263), who granted
|
||||
// permission for it to be used here. ARMSX3 derives from RPCS3, which is GPL-2.0-ONLY with no
|
||||
// "or later" clause, so the original GPL-3.0-or-later terms could not be carried across --
|
||||
// ARMSX2 is GPL-3.0 and ships it unchanged. Eden is unaffected: "or later" leaves its own use
|
||||
// exactly as it was.
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
// SPDX-FileCopyrightText: Copyright 2025 lsfg-vk
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
//
|
||||
// Ported from Eden (eden-emu PR #4263), src/video_core/renderer_vulkan/present/lsfg_beta.cpp.
|
||||
// The pass is unchanged: the descriptor layouts, the two tile shifts and the barrier ordering are
|
||||
// the shader contract, not style, so this stays verbatim and diffable against upstream. Only the
|
||||
// includes are remapped onto the PCSX2 shim. See LsfgVkCompat.h.
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "LosslessDll.h"
|
||||
#include "LsfgBeta.h"
|
||||
#include "LsfgShaders.h"
|
||||
#include "LsfgUtil.h"
|
||||
#include "LsfgVkCompat.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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user