Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ set(SWEETLOADER_SOURCES
src/pl/cosmetics/network/CosmeticSyncJni.cpp
src/pl/render/CameraSource.cpp
src/pl/render/GlesProbe.cpp
src/pl/render/PresentFrameEffect.cpp
src/pl/render/RenderApi.cpp
src/pl/hooks/DobbyHookManager.cpp
src/pl/runtime/FeatureAvailability.cpp
Expand Down
23 changes: 23 additions & 0 deletions include/pl/render/GlesProbe.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,16 @@ struct GlesFunctions {
void *GenVertexArrays = nullptr;
void *BindVertexArray = nullptr;
void *EnableVertexAttribArray = nullptr;
void *DisableVertexAttribArray = nullptr;
void *VertexAttribPointer = nullptr;
void *UseProgram = nullptr;
void *GetUniformLocation = nullptr;
void *UniformMatrix4fv = nullptr;
void *Uniform4f = nullptr;
void *Uniform3f = nullptr;
void *Uniform1i = nullptr;
void *Uniform1f = nullptr;
void *Uniform2f = nullptr;
void *ActiveTexture = nullptr;
void *BindTexture = nullptr;
void *GenTextures = nullptr;
Expand All @@ -76,9 +79,29 @@ struct GlesFunctions {
void *DrawElements = nullptr;
void *DrawArrays = nullptr;
void *BlendFunc = nullptr;
void *BlendFuncSeparate = nullptr;
void *DeleteBuffers = nullptr;
void *DeleteVertexArrays = nullptr;
void *DeleteTextures = nullptr;
// Present-frame effect path (frame generation / motion blur): the shader program that samples the
// captured frame, and the framebuffer/texture copy used to keep it. Resolved here rather than in
// the effect so the probe stays the single GL symbol table.
void *CreateShader = nullptr;
void *ShaderSource = nullptr;
void *CompileShader = nullptr;
void *GetShaderiv = nullptr;
void *CreateProgram = nullptr;
void *AttachShader = nullptr;
void *LinkProgram = nullptr;
void *GetProgramiv = nullptr;
void *GetAttribLocation = nullptr;
void *GenFramebuffers = nullptr;
void *BindFramebuffer = nullptr;
void *FramebufferTexture2D = nullptr;
void *CopyTexImage2D = nullptr;
void *TexParameterf = nullptr;
void *ClearColor = nullptr;
void *Clear = nullptr;

/** @brief True when the core query path is available (nothing essential is null). */
[[nodiscard]] bool usable() const;
Expand Down
65 changes: 65 additions & 0 deletions include/pl/render/PresentFrameEffect.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
#pragma once

/**
* @file PresentFrameEffect.hpp
* @brief The present-frame effect seam: frame generation and motion blur, on the engine's present.
*
* <p>Both effects act on the frame the engine is about to present, so both live on the same seam:
* the {@code eglSwapBuffers} detour (`pl::runtime::InstallSwapFrameBoundary`). This header owns the
* plan the launcher sends, the pure blend/interpolation math, and the install state.
*
* <p><b>Honesty contract.</b> Neither effect raises the game's frame rate. Frame generation blends
* the previous frame into the current one so the presented image sits <em>between</em> two the game
* already drew; motion blur blends the previous frame in, which softens the image by design. Neither
* presents an extra frame: the seam emits one present per rendered frame. {@link ComputeBlendAlpha}
* and {@link ComputeInterpolationFactor} are pure and host-tested, so the numbers the seam uses are
* checkable off-device.
*
* <p><b>Fail-closed.</b> {@link InstallPresentFrameEffect} reports false when the GL path cannot be
* set up (no GLES table, no EGL context, or a build with no present hook). {@link
* IsPresentFrameEffectLive} then reports false and the launcher keeps both modules badged rather
* than toggling to no effect. Nothing here fabricates a frame.
*/

#include <cstdint>

#include "pl/Export.hpp"
#include "pl/render/PresentFrameMath.hpp"

namespace pl::render {

/**
* @brief Installs the present-frame effect seam, once. Safe to call repeatedly.
*
* <p>Fail-closed: returns false when the engine's present hook is not live, or the GL path the
* effects need cannot be resolved. A false result leaves {@link IsPresentFrameEffectLive} false, so
* the launcher keeps both modules badged.
*
* @return true when the seam is installed and can act on presented frames.
*/
PL_EXPORT bool InstallPresentFrameEffect();

/** @brief True once {@link InstallPresentFrameEffect} has installed a working seam. */
PL_EXPORT bool IsPresentFrameEffectLive();

/**
* @brief Stores the plan the effects apply on the next present.
*
* Safe to call from the launcher thread; the seam reads the plan on the render thread. Values are
* clamped so a stored option can never drive an out-of-range blend.
*/
PL_EXPORT void SetPresentFramePlan(const PresentFramePlan &plan);

/** @brief The stored plan, for diagnostics. */
PL_EXPORT PresentFramePlan GetPresentFramePlan();

/**
* @brief Runs the effects at a present boundary.
*
* Called from the {@code eglSwapBuffers} record stub, on the render thread, with the GL context
* current. A no-op when the seam is not live or no effect is enabled. It never blocks and never
* allocates on the steady path.
*/
PL_EXPORT void OnPresentBoundary();

} // namespace pl::render
117 changes: 117 additions & 0 deletions include/pl/render/PresentFrameMath.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
#pragma once

/**
* @file PresentFrameMath.hpp
* @brief The pure math behind the present-frame effect modules (frame generation, motion blur).
*
* <p>Kept apart from {@link PresentFrameEffect} on purpose: the effect file pulls in the GL probe
* and the logger (and therefore the Android shims), while these two functions are plain arithmetic
* with no dependencies. That is what lets the host test runner pin them directly, off-device, which
* is the same reason the other pure rules in this tree live in their own headers.
*
* <p><b>Honesty.</b> Neither function measures or promises a frame rate. Frame generation returns
* how far the blended image sits toward the previous real frame; motion blur returns how much of the
* previous frame to mix in. Both are clamped to [0, 1]. Neither inserts a frame: the seam presents
* one frame per rendered frame.
*/

#include <cstdint>

namespace pl::render {

/** @brief The plan the launcher sends from the module options. All values are clamped on receipt. */
struct PresentFramePlan {
/** Whether frame generation is enabled (the module switch and its own toggle). */
bool frameGenerationEnabled = false;
/** 0..1: how far the blended image is pushed toward the previous of the two real frames. */
float interpolationStrength = 0.f;
/**
* 2..3: reserved. The seam blends the previous frame into the current one and presents a single
* frame, so it does not act on this ceiling yet; it is kept for a future true frame-insertion
* seam. Stored and clamped so the option round-trips, but nothing reads it to emit extra presents.
*/
int maxFramesPerRendered = 2;
/** Bias the blend to reuse less of the previous frame near edges. */
bool reduceEdgeArtifacts = false;

/** Whether motion blur is enabled (the module switch and its own toggle). */
bool motionBlurEnabled = false;
/** 0..1: how much of the previous frame is blended in. */
float blurStrength = 0.f;
/** 0..1: the most of the frame time the blend may take. */
float blurBudget = 0.f;
/** Scale the blur by camera speed, so a still view stays sharp. */
bool weightByViewSpeed = false;
};

/**
* @brief The blend alpha for motion blur this frame.
*
* The alpha is the fraction of the previous frame mixed into the current one. It is the configured
* strength, scaled by how fast the view is moving when {@code weightByViewSpeed} is set, and it is
* additionally capped by the budget: a frame interval longer than the budget leaves no room for the
* blend, so the alpha falls proportionally rather than the effect overrunning the frame.
*
* @param strength configured blur strength, 0..1
* @param budget configured blend budget as a fraction of the frame time, 0..1
* @param frameIntervalMs the last measured frame interval, milliseconds (<= 0 means unknown)
* @param viewSpeed01 camera speed normalised to 0..1 (0 when unknown)
* @param weightByViewSpeed whether to scale by {@p viewSpeed01}
* @return the blend alpha in [0, 1]
*/
inline float ComputeBlendAlpha(float strength, float budget, float frameIntervalMs,
float viewSpeed01, bool weightByViewSpeed) {
if (strength <= 0.f) return 0.f;
float alpha = strength;
if (weightByViewSpeed) {
const float speed = viewSpeed01 < 0.f ? 0.f : (viewSpeed01 > 1.f ? 1.f : viewSpeed01);
alpha *= speed;
}
if (budget > 0.f && frameIntervalMs > 0.f) {
const float budgetMs = budget * frameIntervalMs;
if (budgetMs <= 0.f) return 0.f;
if (frameIntervalMs > budgetMs) alpha *= budgetMs / frameIntervalMs;
}
if (alpha < 0.f) return 0.f;
if (alpha > 1.f) return 1.f;
return alpha;
}

/**
* @brief The interpolation factor for frame generation this frame.
*
* The factor is where the blended frame sits between the previous real frame (0) and the current
* one (1): 0.5 is the midpoint. At full strength the factor is the midpoint; as strength falls it
* moves toward the current frame the game actually drew, which is the low-artefact end. With
* {@code reduceEdgeArtifacts} the factor is pulled further toward that real frame.
*
* <p><b>Honesty.</b> This factor is a position between two real frames, not a promise that a third
* frame is presented. The seam uses {@link FrameGenerationWeight} (its complement) as the blend
* weight: it mixes the previous frame into the current one, it does not insert a frame.
*
* @param strength configured interpolation strength, 0..1
* @param reduceEdgeArtifacts bias toward the current real frame near edges
* @return the interpolation factor in [0, 1]
*/
inline float ComputeInterpolationFactor(float strength, bool reduceEdgeArtifacts) {
const float s = strength < 0.f ? 0.f : (strength > 1.f ? 1.f : strength);
float factor = 0.5f + 0.5f * (1.f - s);
if (reduceEdgeArtifacts) {
factor = factor * 0.85f + 0.15f;
}
if (factor < 0.f) return 0.f;
if (factor > 1.f) return 1.f;
return factor;
}

/**
* @brief The blend weight frame generation contributes: its distance from the current real frame.
*
* This is what the seam actually blends. It is derived from {@link ComputeInterpolationFactor} so
* the two never drift, and it is the value the seam compares against the motion-blur alpha.
*/
inline float FrameGenerationWeight(float strength, bool reduceEdgeArtifacts) {
return 1.f - ComputeInterpolationFactor(strength, reduceEdgeArtifacts);
}

} // namespace pl::render
20 changes: 20 additions & 0 deletions src/pl/render/GlesProbe.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,16 @@ GlesFunctions ResolveGles() {
LOAD("glGenVertexArrays", GenVertexArrays);
LOAD("glBindVertexArray", BindVertexArray);
LOAD("glEnableVertexAttribArray", EnableVertexAttribArray);
LOAD("glDisableVertexAttribArray", DisableVertexAttribArray);
LOAD("glVertexAttribPointer", VertexAttribPointer);
LOAD("glUseProgram", UseProgram);
LOAD("glGetUniformLocation", GetUniformLocation);
LOAD("glUniformMatrix4fv", UniformMatrix4fv);
LOAD("glUniform4f", Uniform4f);
LOAD("glUniform3f", Uniform3f);
LOAD("glUniform1i", Uniform1i);
LOAD("glUniform1f", Uniform1f);
LOAD("glUniform2f", Uniform2f);
LOAD("glActiveTexture", ActiveTexture);
LOAD("glBindTexture", BindTexture);
LOAD("glGenTextures", GenTextures);
Expand All @@ -107,9 +110,26 @@ GlesFunctions ResolveGles() {
LOAD("glDrawElements", DrawElements);
LOAD("glDrawArrays", DrawArrays);
LOAD("glBlendFunc", BlendFunc);
LOAD("glBlendFuncSeparate", BlendFuncSeparate);
LOAD("glDeleteBuffers", DeleteBuffers);
LOAD("glDeleteVertexArrays", DeleteVertexArrays);
LOAD("glDeleteTextures", DeleteTextures);
LOAD("glCreateShader", CreateShader);
LOAD("glShaderSource", ShaderSource);
LOAD("glCompileShader", CompileShader);
LOAD("glGetShaderiv", GetShaderiv);
LOAD("glCreateProgram", CreateProgram);
LOAD("glAttachShader", AttachShader);
LOAD("glLinkProgram", LinkProgram);
LOAD("glGetProgramiv", GetProgramiv);
LOAD("glGetAttribLocation", GetAttribLocation);
LOAD("glGenFramebuffers", GenFramebuffers);
LOAD("glBindFramebuffer", BindFramebuffer);
LOAD("glFramebufferTexture2D", FramebufferTexture2D);
LOAD("glCopyTexImage2D", CopyTexImage2D);
LOAD("glTexParameterf", TexParameterf);
LOAD("glClearColor", ClearColor);
LOAD("glClear", Clear);
#undef LOAD

if (g_gles.usable()) {
Expand Down
Loading
Loading