GBA Video: Add special circlular window handling in OpenGL renderer

This commit is contained in:
Vicki Pfau 2024-05-27 02:57:11 -07:00
parent c7b5d10546
commit b7729c9e80
3 changed files with 139 additions and 2 deletions

View File

@ -32,6 +32,7 @@ Misc:
- GB Serialize: Add missing savestate support for MBC6 and NT (newer)
- GBA: Improve detection of valid ELF ROMs
- GBA Audio: Remove broken XQ audio pending rewrite
- GBA Video: Add special circlular window handling in OpenGL renderer
- Libretro: Add Super Game Boy Color support (closes mgba.io/i/3188)
- mGUI: Enable auto-softpatching (closes mgba.io/i/2899)
- mGUI: Persist fast forwarding after closing menu (fixes mgba.io/i/2414)

View File

@ -118,6 +118,8 @@ enum {
GBA_GL_WIN_FLAGS,
GBA_GL_WIN_WIN0,
GBA_GL_WIN_WIN1,
GBA_GL_WIN_CIRCLE0,
GBA_GL_WIN_CIRCLE1,
GBA_GL_FINALIZE_SCALE = 2,
GBA_GL_FINALIZE_LAYERS,

View File

@ -486,6 +486,8 @@ static const struct GBAVideoGLUniform _uniformsWindow[] = {
{ "flags", GBA_GL_WIN_FLAGS, },
{ "win0", GBA_GL_WIN_WIN0, },
{ "win1", GBA_GL_WIN_WIN1, },
{ "circle0", GBA_GL_WIN_CIRCLE0, },
{ "circle1", GBA_GL_WIN_CIRCLE1, },
{ 0 }
};
@ -496,6 +498,8 @@ static const char* const _renderWindow =
"uniform ivec3 flags;\n"
"uniform ivec4 win0[160];\n"
"uniform ivec4 win1[160];\n"
"uniform vec3 circle0;\n"
"uniform vec3 circle1;\n"
"OUT(0) out ivec4 window;\n"
"bool crop(vec4 windowParams) {\n"
@ -529,13 +533,20 @@ static const char* const _renderWindow =
" return vec4(mix(bottom.xy, top.xy, fract(texCoord.y)), top.zw);\n"
"}\n"
"bool test(vec3 circle, vec4 top, vec4 bottom) {\n"
" if (circle.z > 0) {\n"
" return distance(circle.xy, texCoord.xy) <= circle.z;\n"
" }\n"
" return crop(interpolate(top, bottom));\n"
"}\n"
"void main() {\n"
" ivec4 windowFlags = ivec4(flags.z, blend, 0);\n"
" int top = int(texCoord.y);\n"
" int bottom = max(top - 1, 0);\n"
" if ((dispcnt & 0x20) != 0 && crop(interpolate(vec4(win0[top]), vec4(win0[bottom])))) { \n"
" if ((dispcnt & 0x20) != 0 && test(circle0, vec4(win0[top]), vec4(win0[bottom]))) {\n"
" windowFlags.x = flags.x;\n"
" } else if ((dispcnt & 0x40) != 0 && crop(interpolate(vec4(win1[top]), vec4(win1[bottom])))) {\n"
" } else if ((dispcnt & 0x40) != 0 && test(circle1, vec4(win1[top]), vec4(win1[bottom]))) {\n"
" windowFlags.x = flags.y;\n"
" }\n"
" window = windowFlags;\n"
@ -1924,6 +1935,127 @@ void GBAVideoGLRendererDrawBackgroundMode5(struct GBAVideoGLRenderer* renderer,
glDrawBuffers(1, (GLenum[]) { GL_COLOR_ATTACHMENT0 });
}
static void _detectCircle(struct GBAVideoGLRenderer* renderer, int y, int window) {
int lastStart = 0;
int lastEnd = 0;
int startX = 0;
int endX = 0;
int firstY = -1;
float centerX;
float centerY = -1;
float radius = 0;
bool invalid = false;
int i;
for (i = renderer->firstY; i <= y; ++i) {
lastStart = startX;
lastEnd = endX;
startX = renderer->winNHistory[window][i * 4];
endX = renderer->winNHistory[window][i * 4 + 1];
int startY = renderer->winNHistory[window][i * 4 + 2];
int endY = renderer->winNHistory[window][i * 4 + 3];
if (startX == endX || i < startY || i >= endY) {
if (firstY >= 0) {
// The bottom edge of the circle
centerY = (firstY + i) / 2.f;
firstY = -1;
}
continue;
}
if (lastEnd - lastStart <= 0) {
continue;
}
// The previous segment was non-zero
if (startX >= GBA_VIDEO_HORIZONTAL_PIXELS) {
invalid = true;
break;
}
int startDiff = lastStart - startX;
int endDiff = endX - lastEnd;
// Make sure the slopes match, otherwise this isn't a circle
if (startDiff - endDiff < -1 || startDiff - endDiff > 1) {
invalid = true;
break;
}
if (startX < lastStart) {
centerX = (startX + endX) / 2.f;
if (radius > 0) {
// We found two separate shapes, which the interpolation can't handle
invalid = true;
break;
}
} else if (startX > lastStart && radius <= 0) {
radius = (lastEnd - lastStart) / 2.f;
}
if (firstY < 0 && i - 1 >= startY && i - 1 < endY) {
firstY = i - 1;
}
}
if (radius <= 0) {
invalid = true;
}
if (centerY < 0) {
invalid = true;
}
// Check validity
for (i = renderer->firstY; i <= y && !invalid; ++i) {
int startX = renderer->winNHistory[window][i * 4];
int endX = renderer->winNHistory[window][i * 4 + 1];
int startY = renderer->winNHistory[window][i * 4 + 2];
int endY = renderer->winNHistory[window][i * 4 + 3];
bool xActive = startX < endX;
bool yActive = i >= startY && i < endY;
if (xActive && yActive) {
// Real window would be active, make sure simulated window would too
if (centerY - i > radius) {
// y is above the radius
invalid = true;
break;
}
if (i - centerY > radius) {
// y is below the radius
invalid = true;
break;
}
float cosine = fabsf(i - centerY);
float sine = sqrtf(radius * radius - cosine * cosine);
if (fabsf(centerX - sine - startX) <= 1 && fabsf(centerX + sine - endX) <= 1) {
continue;
}
if (radius >= cosine + 1) {
sine = sqrtf(radius * radius - (cosine + 1) * (cosine + 1));
if (fabsf(centerX - sine - startX) <= 1 && fabsf(centerX + sine - endX) <= 1) {
continue;
}
}
// y is active on the wrong parts of the scanline
invalid = true;
} else if (centerY - i < radius && i - centerY < radius) {
// Real window would be inactive, make sure simulated window would too
invalid = true;
}
}
if (invalid) {
glUniform3f(renderer->windowShader.uniforms[GBA_GL_WIN_CIRCLE0 + window], 0, 0, 0);
} else {
glUniform3f(renderer->windowShader.uniforms[GBA_GL_WIN_CIRCLE0 + window], centerX, centerY, radius - 0.499);
}
}
void GBAVideoGLRendererDrawWindow(struct GBAVideoGLRenderer* renderer, int y) {
const struct GBAVideoGLShader* shader = &renderer->windowShader;
const GLuint* uniforms = shader->uniforms;
@ -1950,6 +2082,8 @@ void GBAVideoGLRendererDrawWindow(struct GBAVideoGLRenderer* renderer, int y) {
glUniform3i(uniforms[GBA_GL_WIN_FLAGS], renderer->winN[0].control, renderer->winN[1].control, renderer->winout);
glUniform4iv(uniforms[GBA_GL_WIN_WIN0], GBA_VIDEO_VERTICAL_PIXELS, renderer->winNHistory[0]);
glUniform4iv(uniforms[GBA_GL_WIN_WIN1], GBA_VIDEO_VERTICAL_PIXELS, renderer->winNHistory[1]);
_detectCircle(renderer, y, 0);
_detectCircle(renderer, y, 1);
glDrawArrays(GL_TRIANGLE_FAN, 0, 4);
break;
}