feat(renderer): Add frame skipping and interpolation logic

- Further Implemented frame skipping functionality to improve performance on lower-end devices by skipping alternate frames.
- Added interpolation logic to create additional frames, resulting in smoother gameplay when frame interpolation is enabled.
- Introduced new settings for toggling frame skipping and interpolation in the renderer configuration.
This commit is contained in:
Phoenix 2024-09-12 15:35:39 +10:00
parent 9a34d40523
commit 308cc609c5
7 changed files with 137 additions and 91 deletions

View File

@ -29,7 +29,8 @@ enum class BooleanSetting(override val key: String) : AbstractBooleanSetting {
SHOW_THERMAL_OVERLAY("show_thermal_overlay"),
CORE_USE_MULTI_CORE("use_multi_core"),
SYNC_CORE_SPEED("sync_core_speed"),
FRAME_SKIPPING("frame_skipping");
FRAME_SKIPPING("frame_skipping"),
FRAME_INTERPOLATION("frame_interpolation");
override fun getBoolean(needsGlobal: Boolean): Boolean =
NativeConfig.getBoolean(key, needsGlobal)

View File

@ -177,6 +177,7 @@ private fun addPhoenixHacksSubmenu(sl: ArrayList<SettingsItem>) {
// Add settings using keys directly
add(BooleanSetting.FRAME_SKIPPING.key)
add(BooleanSetting.FRAME_INTERPOLATION.key)
add(BooleanSetting.CORE_USE_MULTI_CORE.key)
add(BooleanSetting.SYNC_CORE_SPEED.key)
add(IntSetting.RENDERER_SHADER_BACKEND.key)
@ -187,6 +188,25 @@ private fun addPhoenixHacksSubmenu(sl: ArrayList<SettingsItem>) {
}
}
private val useInterpolationSetting = object : AbstractBooleanSetting {
override val key = BooleanSetting.FRAME_INTERPOLATION.key
override fun getBoolean(needsGlobal: Boolean): Boolean {
return BooleanSetting.FRAME_INTERPOLATION.getBoolean(needsGlobal)
}
override fun setBoolean(value: Boolean) {
BooleanSetting.FRAME_INTERPOLATION.setBoolean(value)
}
override val defaultValue = BooleanSetting.FRAME_INTERPOLATION.defaultValue
override fun getValueAsString(needsGlobal: Boolean): String =
BooleanSetting.FRAME_INTERPOLATION.getValueAsString(needsGlobal)
override fun reset() = BooleanSetting.FRAME_INTERPOLATION.reset()
}
private val useSyncCoreSpeedSetting = object : AbstractBooleanSetting {
override val key = BooleanSetting.SYNC_CORE_SPEED.key
@ -226,34 +246,26 @@ private fun addPhoenixHacksSubmenu(sl: ArrayList<SettingsItem>) {
}
private val frameSkippingSetting = object : AbstractBooleanSetting {
override val key = "frame_skipping"
override val key = BooleanSetting.FRAME_SKIPPING.key
override fun getBoolean(needsGlobal: Boolean): Boolean {
return BooleanSetting.FRAME_SKIPPING.getBoolean(needsGlobal)
override fun getBoolean(needsGlobal: Boolean): Boolean {
return BooleanSetting.FRAME_SKIPPING.getBoolean(needsGlobal)
}
override fun setBoolean(value: Boolean) {
BooleanSetting.FRAME_SKIPPING.setBoolean(value)
}
override val defaultValue = BooleanSetting.FRAME_SKIPPING.defaultValue
override fun getValueAsString(needsGlobal: Boolean): String =
BooleanSetting.FRAME_SKIPPING.getValueAsString(needsGlobal)
override fun reset() = BooleanSetting.FRAME_SKIPPING.reset()
}
override fun setBoolean(value: Boolean) {
BooleanSetting.FRAME_SKIPPING.setBoolean(value)
NativeLibrary.setFrameSkipping(value) // Ensure this toggles the frame skipping in Vulkan renderer
}
override val defaultValue = BooleanSetting.FRAME_SKIPPING.defaultValue
override fun getValueAsString(needsGlobal: Boolean): String = getBoolean(needsGlobal).toString()
override fun reset() = setBoolean(defaultValue)
}
private fun addPhoenixHacksSettings(sl: ArrayList<SettingsItem>) {
private fun addPhoenixHacksSettings(sl: ArrayList<SettingsItem>) {
sl.apply {
// Add the multi-core setting to Phoenix Hacks submenu
add(
SwitchSetting(
frameSkippingSetting,
titleId = R.string.frame_skipping,
descriptionId = R.string.frame_skipping_description
)
)
add(
SwitchSetting(
useSyncCoreSpeedSetting,
@ -268,7 +280,20 @@ private fun addPhoenixHacksSettings(sl: ArrayList<SettingsItem>) {
descriptionId = R.string.use_multi_core_description
)
)
// Add the new settings
add(
SwitchSetting(
frameSkippingSetting,
titleId = R.string.frame_skipping,
descriptionId = R.string.frame_skipping_description
)
)
add(
SwitchSetting(
useInterpolationSetting, // The interpolation setting object you've created
titleId = R.string.frame_interpolation, // Use appropriate string resources for the title
descriptionId = R.string.frame_interpolation_description // Description resource for the interpolation setting
)
)
add(
SingleChoiceSetting(
IntSetting.RENDERER_SHADER_BACKEND,

View File

@ -390,9 +390,15 @@
<string name="sync_core_speed">Sync Core Speed</string>
<string name="sync_core_speed_description">Synchronizes the CPU core speed with the game\'s maximum rendering speed.</string>
<string name="frame_interpolation">Frame Interpolation</string>
<string name="frame_interpolation_description">Enable or disable frame interpolation for smoother transitions between frames.</string>
<string name="frame_skipping">Frame Skipping</string>
<string name="frame_skipping_description">Enables frame skipping to improve performance by skipping certain frames during rendering.</string>
<!-- Miscellaneous -->
<string name="preferences_phoenix_hacks">🐦‍🔥 Phoenix Hacks</string>
<string name="preferences_phoenix_hacks_description">Enable or adjust Phoenix-specific hacks</string>
<string name="preferences_phoenix_hacks">🐦‍🔥 Phoenix Tweaks</string>
<string name="preferences_phoenix_hacks_description">Enable or adjust Phoenix-specific tweaks</string>
<string name="slider_default">Default</string>
<string name="ini_saved">Saved settings</string>
<string name="gameid_saved">Saved settings for %1$s</string>

View File

@ -210,10 +210,12 @@ struct Values {
true,
true,
&use_speed_limit};
SwitchableSetting<bool> sync_core_speed{linkage, false, "sync_core_speed", Category::Core, Specialization::Default};
SwitchableSetting<bool> sync_core_speed{linkage, true, "sync_core_speed", Category::Core, Specialization::Default};
// Frame-Skipping (Experimental)
SwitchableSetting<bool> frame_skipping{linkage, false, "frame_skipping", Category::Core, Specialization::Default};
SwitchableSetting<bool> frame_skipping{linkage, false, "frame_skipping", Category::Renderer, Specialization::Default};
// Frame Interpolation (Experimental)
SwitchableSetting<bool> frame_interpolation{linkage, false, "frame_interpolation", Category::Renderer, Specialization::Default};
// Cpu
SwitchableSetting<CpuBackend, true> cpu_backend{linkage,

View File

@ -189,6 +189,10 @@ std::unique_ptr<TranslationMap> InitializeTranslations(QWidget* parent) {
tr("Enables frame skipping to improve performance in cases where the emulator cannot maintain the target frame rate. "
"Frame skipping will cause some frames to be skipped to maintain smoother gameplay at the cost of visual fluidity. "
"This is especially useful on lower-end hardware."));
INSERT(Settings, frame_interpolation, tr("Enable Frame Interpolation"),
tr("Enables frame interpolation to smooth out transitions between frames for a more fluid visual experience. "
"This can be useful when the emulator's frame rate is lower than the target frame rate, creating smoother movement at the cost of input lag or visual accuracy. "
"Recommended for higher-end hardware or when fluidity is prioritized over precision."));
// Renderer (Advanced Graphics)
INSERT(Settings, async_presentation, tr("Enable asynchronous presentation (Vulkan only)"),

View File

@ -124,6 +124,12 @@ RendererVulkan::RendererVulkan(Core::TelemetrySession& telemetry_session_,
rasterizer(render_window, gpu, device_memory, device, memory_allocator, state_tracker,
scheduler),
applet_frame() {
// Move frame skipping initialization here
frame_skipping_enabled = Settings::values.frame_skipping.GetValue();
// Initialize frame interpolation here
frame_interpolation_enabled = Settings::values.frame_interpolation.GetValue();
if (Settings::values.renderer_force_max_clock.GetValue() && device.ShouldBoostClocks()) {
turbo_mode.emplace(instance, dld);
scheduler.RegisterOnSubmit([this] { turbo_mode->QueueSubmitted(); });
@ -139,80 +145,83 @@ RendererVulkan::~RendererVulkan() {
void(device.GetLogical().WaitIdle());
}
void RendererVulkan::Composite(std::span<const Tegra::FramebufferConfig> framebuffers) {
if (frame_skip_interval > 0 && (frame_counter % (frame_skip_interval + 1)) == 0) {
frame_counter++;
is_frame_skipped = true; // Mark this frame as skipped
return; // Skip this frame
void RendererVulkan::InterpolateFrames(Frame* prev_frame, Frame* next_frame, Frame* interpolated_frame, float interpolation_factor) {
if (prev_frame && next_frame && interpolated_frame) {
// Implement further interpolation logic here, e.g., linear interpolation, spherical interpolation, etc.
for (size_t i = 0; i < interpolated_frame->data.size(); ++i) {
interpolated_frame->data[i] = (1.0f - interpolation_factor) * prev_frame->data[i] +
interpolation_factor * next_frame->data[i];
}
}
}
frame_counter++;
bool frame_skipping_enabled = false;
bool frame_interpolation_enabled = false;
void RendererVulkan::Composite(std::span<const Tegra::FramebufferConfig> framebuffers) {
static int frame_counter = 0; // Track frame count
static Frame* prev_frame = nullptr; // Track the previous frame
Frame* interpolated_frame = nullptr;
// Frame skipping logic
if (frame_skipping_enabled) {
// Skip rendering every other frame
if (++frame_counter % 2 != 0) {
return; // Skip this frame
}
}
if (framebuffers.empty()) {
return;
}
SCOPE_EXIT {
render_window.OnFrameDisplayed();
render_window.OnFrameDisplayed();
};
RenderAppletCaptureLayer(framebuffers);
// Check if frame interpolation is enabled
if (frame_interpolation_enabled && prev_frame != nullptr) {
// Allocate or retrieve a new frame for interpolation
interpolated_frame = present_manager.GetRenderFrame();
if (!render_window.IsShown()) {
return;
// Calculate interpolation factor (e.g., halfway between frames = 0.5)
float interpolation_factor = 0.5f;
// Interpolate between prev_frame and the current frame
InterpolateFrames(prev_frame, present_manager.GetRenderFrame(), interpolated_frame, interpolation_factor);
// Render the interpolated frame instead of the current frame
blit_swapchain.DrawToFrame(rasterizer, interpolated_frame, framebuffers,
render_window.GetFramebufferLayout(), swapchain.GetImageCount(),
swapchain.GetImageViewFormat());
scheduler.Flush(*interpolated_frame->render_ready);
present_manager.Present(interpolated_frame);
} else {
// Render the current frame as normal if interpolation is disabled
RenderAppletCaptureLayer(framebuffers);
if (!render_window.IsShown()) {
return;
}
RenderScreenshot(framebuffers);
Frame* frame = present_manager.GetRenderFrame();
blit_swapchain.DrawToFrame(rasterizer, frame, framebuffers,
render_window.GetFramebufferLayout(), swapchain.GetImageCount(),
swapchain.GetImageViewFormat());
scheduler.Flush(*frame->render_ready);
present_manager.Present(frame);
}
if (is_frame_skipped && previous_frame_buffer.has_value()) {
// Interpolate between the previous frame and the current frame
InterpolateFrame(previous_frame_buffer.value(), framebuffers);
is_frame_skipped = false;
}
// Capture current frame buffer for future interpolation
vk::Buffer current_frame_buffer = RenderToBuffer(framebuffers, render_window.GetFramebufferLayout(),
VK_FORMAT_B8G8R8A8_UNORM, layout.width * layout.height * 4);
previous_frame_buffer = current_frame_buffer;
RenderScreenshot(framebuffers);
Frame* frame = present_manager.GetRenderFrame();
blit_swapchain.DrawToFrame(rasterizer, frame, framebuffers,
render_window.GetFramebufferLayout(), swapchain.GetImageCount(),
swapchain.GetImageViewFormat());
scheduler.Flush(*frame->render_ready);
present_manager.Present(frame);
// Save the current frame as the previous frame for the next call
prev_frame = present_manager.GetRenderFrame();
gpu.RendererFrameEndNotify();
rasterizer.TickFrame();
}
void RendererVulkan::InterpolateFrame(vk::Buffer& previous_frame, std::span<const Tegra::FramebufferConfig> framebuffers) {
// Allocate a buffer for the interpolated frame
vk::Buffer interpolated_frame_buffer = RenderToBuffer(framebuffers, render_window.GetFramebufferLayout(),
VK_FORMAT_B8G8R8A8_UNORM, layout.width * layout.height * 4);
// Perform the interpolation here (simplified linear blending)
// In reality, you'd need to read from both frame buffers and blend the pixel values
scheduler.Record([&](vk::CommandBuffer cmdbuf) {
// For simplicity, assume a 50-50 blend between previous and current frames
// More complex interpolation can be implemented by reading pixel data and manually blending
// vkCmdCopyBuffer and other Vulkan operations to blend the previous and current frames
// Here you would implement your actual blending logic
BlitBuffer(cmdbuf, previous_frame, interpolated_frame_buffer); // Pseudo-code function for blending
});
scheduler.Finish(); // Ensure the interpolation finishes before presenting
// Present the interpolated frame
present_manager.PresentInterpolatedFrame(interpolated_frame_buffer);
}
void RendererVulkan::BlitBuffer(vk::CommandBuffer cmdbuf, vk::Buffer& src, vk::Buffer& dst) {
// This is a placeholder for Vulkan operations that blend two frame buffers.
// Vulkan commands would go here, such as vkCmdCopyBuffer, and shader operations
// that blend the contents of src and dst.
}
void RendererVulkan::Report() const {
using namespace Common::Literals;
const std::string vendor_name{device.GetVendorName()};

View File

@ -58,6 +58,8 @@ public:
return device.GetDriverName();
}
void InterpolateFrames(Frame* prev_frame, Frame* next_frame, Frame* interpolated_frame, float interpolation_factor);
private:
void Report() const;
@ -92,11 +94,8 @@ private:
Frame applet_frame;
std::optional<vk::Buffer> previous_frame_buffer; // Store previous frame buffer for interpolation
bool is_frame_skipped = false; // Flag to check if the previous frame was skipped
int frame_skip_interval = 0; // Number of frames to skip (0 means no skipping)
int frame_counter = 0; // Keeps track of the current frame number
bool frame_skipping_enabled;
bool frame_interpolation_enabled;
};
} // namespace Vulkan