mirror of
https://github.com/ARMSX2/ARMSX3.git
synced 2026-08-24 16:58:52 -07:00
Flurry: preserve the EGL back buffer so the trails can accumulate
The library background rendered as full-screen TV static with the smoke faintly visible behind it. Flurry draws its trails by fading the PREVIOUS frame rather than clearing, so it needs the back buffer to still hold what it drew last time. EGL defaults EGL_SWAP_BEHAVIOR to EGL_BUFFER_DESTROYED, which leaves the buffer undefined after every eglSwapBuffers, so every frame started from uninitialised GPU memory -- which is what reached the screen as static. Measured: a single fade darkened the buffer by 3.17%, but sixty consecutive fades only achieved 6.5% in total, where sixty compounding 3.17% fades should have taken a corner reading 123 down to 17. The fade was working every frame; its result was not surviving the swap. A 64x64 readback of that corner went from mean=128 ndiff=41 to mean=0 ndiff=0 with preservation on. Preservation must be requested on the config and on the surface, and is then queried back rather than assumed, since a driver may refuse it. Also set the surface alpha once and mask further alpha writes off. Flurry fades with GL_SRC_ALPHA/GL_ONE_MINUS_SRC_ALPHA, which writes destination alpha as well as colour and drags it toward the fade's own value -- harmless for a screensaver that owns the display, wrong for a surface the window compositor blends. RGB is untouched, so the trails are unaffected.
This commit is contained in:
@@ -217,14 +217,22 @@ GLuint MakeTexture(void)
|
||||
|
||||
/* Set the filtering. */
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_NEAREST);
|
||||
/* GL_LINEAR, not LINEAR_MIPMAP_NEAREST.
|
||||
*
|
||||
* A mipmapped min filter makes the texture INCOMPLETE unless the whole chain exists, and an
|
||||
* incomplete texture samples as opaque black -- which, through this shader's c *= texture,
|
||||
* multiplies the entire particle to nothing. glGenerateMipmap is also not guaranteed for
|
||||
* GL_LUMINANCE_ALPHA, which is filterable but not colour-renderable, so the chain may never
|
||||
* have been built. The sprite is a 256x256 blob drawn at roughly its own size; the mip chain
|
||||
* was never doing much for it. */
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
|
||||
/* gluBuild2DMipmaps upstream. There is no GLU on Android, and GLES2 builds the chain
|
||||
* itself -- the internal format has to be spelled out rather than given as a component
|
||||
* count, which is what the 2 meant in GL 1.x. */
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_LUMINANCE_ALPHA, 256, 256, 0,
|
||||
GL_LUMINANCE_ALPHA, GL_UNSIGNED_BYTE, bigTextureArray);
|
||||
glGenerateMipmap(GL_TEXTURE_2D);
|
||||
/* No glGenerateMipmap: see the min filter above. */
|
||||
glTexEnvf(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
|
||||
return theTexture;
|
||||
}
|
||||
|
||||
@@ -48,6 +48,8 @@ static const char sccsid[] = "@(#)flurry.c 4.07 97/11/24 xlockmore";
|
||||
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
#include <android/log.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include "flurry.h"
|
||||
|
||||
@@ -368,6 +370,7 @@ global_info_t *flurry_port_new(int preset)
|
||||
|
||||
global->first = 1;
|
||||
global->oldFrameTime = -1;
|
||||
global->port_clear_frames = 8;
|
||||
return global;
|
||||
}
|
||||
|
||||
@@ -409,6 +412,7 @@ global_info_t *flurry_port_new_custom(int streams, int colour, float thickness,
|
||||
|
||||
global->first = 1;
|
||||
global->oldFrameTime = -1;
|
||||
global->port_clear_frames = 8;
|
||||
return global;
|
||||
}
|
||||
|
||||
@@ -430,6 +434,7 @@ int flurry_port_draw(global_info_t *global)
|
||||
|
||||
if (!global) return 0;
|
||||
|
||||
|
||||
newFrameTime = currentTime();
|
||||
|
||||
if (global->oldFrameTime == -1) {
|
||||
@@ -450,6 +455,7 @@ int flurry_port_draw(global_info_t *global)
|
||||
global->oldFrameTime = newFrameTime;
|
||||
if (alpha > 0.2) alpha = 0.2;
|
||||
|
||||
|
||||
if (global->first) {
|
||||
global->texid = MakeTexture();
|
||||
global->first = 0;
|
||||
@@ -458,10 +464,27 @@ int flurry_port_draw(global_info_t *global)
|
||||
glEnable(GL_BLEND);
|
||||
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
/* The buffer starts undefined, so paint it black before the fade has anything coherent to
|
||||
* work on. A few frames covers whatever buffers the driver rotates through; after that the
|
||||
* fade below keeps the screen dark, and clearing would wipe the trails that are the effect.
|
||||
*
|
||||
* Alpha is set once here and then masked off for good. Flurry fades with GL_SRC_ALPHA/
|
||||
* GL_ONE_MINUS_SRC_ALPHA, which writes destination alpha as well as colour and drags it
|
||||
* toward the fade's own value -- harmless for a screensaver that owns the display, wrong for
|
||||
* a surface the window compositor blends. RGB is untouched, so the trails are unaffected. */
|
||||
if (global->port_clear_frames > 0) {
|
||||
global->port_clear_frames--;
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
|
||||
glClear(GL_COLOR_BUFFER_BIT);
|
||||
glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_FALSE);
|
||||
}
|
||||
|
||||
glColor4f(0.0, 0.0, 0.0, alpha);
|
||||
glRectd(0, 0, global->sys_glWidth, global->sys_glHeight);
|
||||
|
||||
brite = pow(deltaFrameTime, 0.75) * 10;
|
||||
|
||||
for (flurry = global->flurry; flurry; flurry = flurry->next) {
|
||||
GLRenderScene(global, flurry, brite * flurry->briteFactor);
|
||||
}
|
||||
|
||||
@@ -264,6 +264,13 @@ struct _global_info_t {
|
||||
/* system values */
|
||||
int optMode;
|
||||
|
||||
/* Port-only. Flurry never clears: it fades the screen by a few percent each frame, which
|
||||
* is how the trails exist. That assumes it owns a buffer that started black. Here it gets
|
||||
* a rotating set of swapchain buffers full of uninitialised GPU memory, and an 8% fade
|
||||
* cannot win against that -- it shows as static the smoke sits behind. Clear each buffer
|
||||
* once, then hand over to the fade. */
|
||||
int port_clear_frames;
|
||||
|
||||
float sys_glWidth;
|
||||
float sys_glHeight;
|
||||
|
||||
|
||||
@@ -1,5 +1,36 @@
|
||||
#include "gl_compat.h"
|
||||
|
||||
/*
|
||||
* Undo the redirection for this file.
|
||||
*
|
||||
* gl_compat.h rewrites glEnable/glDisable/glDrawArrays and friends into the fx_* entry points
|
||||
* so the Flurry sources need no edits -- but this file IMPLEMENTS those entry points, and it
|
||||
* has to be able to call the real GL underneath them. Without these undefs, fx_enable's own
|
||||
* glEnable(cap) expands to fx_enable(cap) and recurses forever. At -O2 that is a tail call and
|
||||
* becomes an infinite LOOP rather than a stack overflow, so it does not crash: the thread just
|
||||
* spins at 100% and nothing after it ever runs. The first glDisable in GLSetupRC was enough to
|
||||
* hang the whole renderer with no error anywhere.
|
||||
*/
|
||||
#undef glEnable
|
||||
#undef glDisable
|
||||
#undef glDrawArrays
|
||||
#undef glVertexPointer
|
||||
#undef glColorPointer
|
||||
#undef glTexCoordPointer
|
||||
#undef glEnableClientState
|
||||
#undef glDisableClientState
|
||||
#undef glColor4f
|
||||
#undef glColor3f
|
||||
#undef glRectd
|
||||
#undef glMatrixMode
|
||||
#undef glLoadIdentity
|
||||
#undef glOrtho
|
||||
#undef glAlphaFunc
|
||||
#undef glShadeModel
|
||||
#undef glTexEnvf
|
||||
#undef glDrawBuffer
|
||||
#undef glFinish
|
||||
|
||||
#include <android/log.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
@@ -191,13 +222,16 @@ void fx_enable(GLenum cap)
|
||||
{
|
||||
/* GLES2 has no alpha test or lighting; passing either would set GL_INVALID_ENUM and the
|
||||
* error would then be blamed on whatever ran next. */
|
||||
if (cap == GL_ALPHA_TEST || cap == GL_LIGHTING) return;
|
||||
/* GL_TEXTURE_2D as a capability is gone in GLES2 -- whether a sampler is used is decided by
|
||||
* the shader. Passing it sets GL_INVALID_ENUM, and the sticky error then gets blamed on
|
||||
* whatever ran next. */
|
||||
if (cap == GL_ALPHA_TEST || cap == GL_LIGHTING || cap == GL_TEXTURE_2D_ENABLE_COMPAT) return;
|
||||
glEnable(cap);
|
||||
}
|
||||
|
||||
void fx_disable(GLenum cap)
|
||||
{
|
||||
if (cap == GL_ALPHA_TEST || cap == GL_LIGHTING) return;
|
||||
if (cap == GL_ALPHA_TEST || cap == GL_LIGHTING || cap == GL_TEXTURE_2D_ENABLE_COMPAT) return;
|
||||
glDisable(cap);
|
||||
}
|
||||
|
||||
@@ -252,6 +286,7 @@ void fx_rect(float x0, float y0, float x1, float y1)
|
||||
|
||||
void fx_draw_arrays(GLenum mode, GLint first, GLsizei count)
|
||||
{
|
||||
|
||||
if (!fx.ready || count <= 0) return;
|
||||
|
||||
glUseProgram(fx.program);
|
||||
|
||||
@@ -46,6 +46,9 @@ extern "C" {
|
||||
#ifndef GL_LINEAR_MIPMAP_NEAREST
|
||||
#define GL_LINEAR_MIPMAP_NEAREST 0x2701
|
||||
#endif
|
||||
#ifndef GL_TEXTURE_2D_ENABLE_COMPAT
|
||||
#define GL_TEXTURE_2D_ENABLE_COMPAT 0x0DE1
|
||||
#endif
|
||||
|
||||
/* Lifetime. fx_init needs a current context; fx_lost forgets GL names without touching them,
|
||||
* for when the context has already gone away underneath us. */
|
||||
|
||||
@@ -31,9 +31,16 @@ class FlurryGlView(context: Context, private val preset: Int) :
|
||||
|
||||
init {
|
||||
surfaceTextureListener = this
|
||||
// Same reasoning as XmbGlView: if nothing ever renders, show what is behind rather than
|
||||
// a black rectangle.
|
||||
isOpaque = false
|
||||
|
||||
// Opaque, unlike XmbGlView.
|
||||
//
|
||||
// That view can afford isOpaque = false because, as its own comment says, it "draws an
|
||||
// opaque gradient every frame, fully covering that layer". Flurry does the opposite: it
|
||||
// never draws anything opaque, it fades the screen toward black at a few percent per
|
||||
// frame, which is what produces the trails. So the surface's alpha channel stays low,
|
||||
// and a non-opaque TextureView with low alpha composites whatever is behind it -- which
|
||||
// showed as full-screen static over the top of the smoke.
|
||||
isOpaque = true
|
||||
}
|
||||
|
||||
override fun onSurfaceTextureAvailable(st: SurfaceTexture, w: Int, h: Int) {
|
||||
@@ -53,6 +60,19 @@ class FlurryGlView(context: Context, private val preset: Int) :
|
||||
|
||||
override fun onSurfaceTextureUpdated(st: SurfaceTexture) {}
|
||||
|
||||
/**
|
||||
* Stop rendering without waiting for the surface callback.
|
||||
*
|
||||
* Toggling the setting recomposes the AndroidView, and the replacement view's thread can be
|
||||
* started before the old one's surface is destroyed -- two particle simulations at once,
|
||||
* which was visible as UI lag. AndroidView's onRelease calls this so teardown is tied to the
|
||||
* composable leaving rather than to a callback that arrives whenever it arrives.
|
||||
*/
|
||||
fun stop() {
|
||||
thread?.finish()
|
||||
thread = null
|
||||
}
|
||||
|
||||
private class RenderThread(
|
||||
private val surfaceTexture: SurfaceTexture,
|
||||
private var width: Int,
|
||||
@@ -110,15 +130,33 @@ class FlurryGlView(context: Context, private val preset: Int) :
|
||||
}
|
||||
|
||||
private fun initEgl(): Boolean {
|
||||
// Not exposed by EGL14.
|
||||
val EGL_SWAP_BEHAVIOR_PRESERVED_BIT = 0x0400
|
||||
|
||||
eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY)
|
||||
if (eglDisplay == EGL14.EGL_NO_DISPLAY) return false
|
||||
|
||||
val ver = IntArray(2)
|
||||
if (!EGL14.eglInitialize(eglDisplay, ver, 0, ver, 1)) return false
|
||||
|
||||
val attribs = intArrayOf(
|
||||
// Flurry draws its trails by fading the PREVIOUS frame rather than clearing, so it
|
||||
// needs the back buffer to still hold what it drew last time. EGL defaults
|
||||
// EGL_SWAP_BEHAVIOR to EGL_BUFFER_DESTROYED, which leaves the buffer undefined after
|
||||
// every swap -- so each frame started from uninitialised GPU memory and the fade had
|
||||
// nothing coherent to work on. That undefined memory is what showed up on screen as
|
||||
// TV static. Measured: one fade darkened the buffer 3.17%, but sixty consecutive
|
||||
// fades only managed 6.5% total, because the result never survived the swap.
|
||||
//
|
||||
// Preservation has to be asked for in the CONFIG as well as on the surface, and not
|
||||
// every driver offers it, so fall back to a plain window config if it is refused.
|
||||
fun attribs(preserved: Boolean) = intArrayOf(
|
||||
EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT,
|
||||
EGL14.EGL_SURFACE_TYPE,
|
||||
if (preserved) EGL14.EGL_WINDOW_BIT or EGL_SWAP_BEHAVIOR_PRESERVED_BIT
|
||||
else EGL14.EGL_WINDOW_BIT,
|
||||
EGL14.EGL_RED_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8, EGL14.EGL_BLUE_SIZE, 8,
|
||||
// A real alpha channel, which the renderer clears to 1 and then masks off, so
|
||||
// the compositor always sees a fully opaque surface.
|
||||
EGL14.EGL_ALPHA_SIZE, 8,
|
||||
// No depth buffer: Flurry is 2D additive smoke, and asking for one on a tiler
|
||||
// costs bandwidth for something never read.
|
||||
@@ -128,7 +166,13 @@ class FlurryGlView(context: Context, private val preset: Int) :
|
||||
|
||||
val cfg = arrayOfNulls<EGLConfig>(1)
|
||||
val num = IntArray(1)
|
||||
if (!EGL14.eglChooseConfig(eglDisplay, attribs, 0, cfg, 0, 1, num, 0) || num[0] == 0) return false
|
||||
var preserved = EGL14.eglChooseConfig(eglDisplay, attribs(true), 0, cfg, 0, 1, num, 0) &&
|
||||
num[0] > 0
|
||||
if (!preserved &&
|
||||
(!EGL14.eglChooseConfig(eglDisplay, attribs(false), 0, cfg, 0, 1, num, 0) || num[0] == 0)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
val ctxAttr = intArrayOf(EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
|
||||
eglContext = EGL14.eglCreateContext(eglDisplay, cfg[0], EGL14.EGL_NO_CONTEXT, ctxAttr, 0)
|
||||
@@ -139,7 +183,24 @@ class FlurryGlView(context: Context, private val preset: Int) :
|
||||
)
|
||||
if (eglSurface == EGL14.EGL_NO_SURFACE) return false
|
||||
|
||||
return EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)
|
||||
if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) return false
|
||||
|
||||
// Asking for the config is not enough; the surface has to be told as well. Query it
|
||||
// back rather than trusting the request -- a driver may quietly refuse.
|
||||
if (preserved) {
|
||||
EGL14.eglSurfaceAttrib(
|
||||
eglDisplay, eglSurface, EGL14.EGL_SWAP_BEHAVIOR, EGL14.EGL_BUFFER_PRESERVED,
|
||||
)
|
||||
val got = IntArray(1)
|
||||
preserved = EGL14.eglQuerySurface(
|
||||
eglDisplay, eglSurface, EGL14.EGL_SWAP_BEHAVIOR, got, 0,
|
||||
) && got[0] == EGL14.EGL_BUFFER_PRESERVED
|
||||
}
|
||||
android.util.Log.e(
|
||||
"Flurry",
|
||||
"swap behavior = " + (if (preserved) "PRESERVED" else "DESTROYED (trails will not accumulate)"),
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
private fun teardown() {
|
||||
|
||||
@@ -198,6 +198,7 @@ fun HomeScreen(
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onRelease = { it.stop() },
|
||||
)
|
||||
}
|
||||
} else if (LibraryBackground.animated2D.value) {
|
||||
|
||||
Reference in New Issue
Block a user