Files
goldeneye-pc-port/port/fast3d/gfx_opengl.cpp
T
James Dansereau 4b710728bf v0.5.0 playtest fold-in: fog and near-plane clip, crash fixes, polish (D540-D564)
Deck/Windows playtest fixes: fog rework (D540) and RSP-style CPU near-plane
clip with per-vertex N64 fog (D543), with fog recomputed on near-plane-clipped vertices as the RSP does,
so the haze around the player stays steady (D553); colour-combiner clamp for banded walls
(D548); rocket and thrown-item stack overflows (D545, D549); menu
double-trigger mitigation (D541); Dam ending first-shot camera swivel
restored by removing an old port look-at workaround (D552); settings standardisation (D546), live
All unlocked (D547), F10 mouse (D544); static window title and the project
mark (the in-game crosshair shape, site gold) as window/taskbar icon and
favicon (D550). Reference frames re-based
on Windows, Linux and Steam Deck (A/B-confirmed); capture_p7.sh runs on
Windows. Opt-in update check, off by default (D551: one HTTPS request to
GitHub per launch when enabled; winhttp.dll loaded only then; the tag CI
job checks the tag against GE007_VERSION). Release notes, ROADMAP,
internals and security docs updated; tree-hygiene pass.

Refs: D540 D541 D543 D544 D545 D546 D547 D548 D549 D550 D551 D552 D553 D554 D555 D556 D557 D558 D559 D560 D561 D562 D563 D564
2026-10-06 18:35:44 -04:00

1554 lines
63 KiB
C++

#include <stdint.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <map>
#include <unordered_map>
#include <vector>
#include <SDL.h>
#ifndef _LANGUAGE_C
#define _LANGUAGE_C
#endif
#include <PR/gbi.h>
#include "glad/glad.h"
#include "gfx_cc.h"
#include "gfx_rendering_api.h"
#include "gfx_pc.h"
using namespace std;
struct ShaderProgram {
GLuint opengl_program_id;
uint8_t num_inputs;
bool used_textures[SHADER_MAX_TEXTURES];
uint8_t num_floats;
GLint attrib_locations[16];
uint8_t attrib_sizes[16];
uint8_t num_attribs;
GLint frame_count_location;
GLint noise_scale_location;
GLint three_point_filter_locations[2];
};
struct Framebuffer {
uint32_t width, height;
bool has_depth_buffer;
uint32_t msaa_level;
bool invert_y;
GLuint fbo, clrbuf, clrbuf_msaa, rbo;
};
static std::map<pair<uint64_t, uint32_t>, struct ShaderProgram> shader_program_pool;
static GLuint opengl_vbo;
static GLuint opengl_vao;
static bool current_depth_mask;
static uint32_t frame_count;
static std::vector<Framebuffer> framebuffers;
static size_t current_framebuffer;
static float current_noise_scale;
static int current_anisotropy_level = 4; /* B1: sane default (0 was invalid for GL_TEXTURE_MAX_ANISOTROPY) */
static FilteringMode current_filter_mode = FILTER_LINEAR;
static MipmapFilteringMode current_mipmap_filter_mode = MIPMAP_LINEAR;
static bool current_textures_linear_filter[2] = {false, false};
static int gl_glsl_version = 130;
static char gl_glsl_version_str[16] = "130";
static GLenum gl_mirror_clamp = GL_MIRROR_CLAMP_TO_EDGE;
static bool gl_es = false;
extern int gfx_rdp_affine; /* D540: defined in gfx_pc.cpp */
extern int gfx_fog_vertex; /* D540: defined in gfx_pc.cpp */
static bool gl_core_profile = false;
static int gfx_opengl_get_max_texture_size() {
GLint max_texture_size;
glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_texture_size);
return max_texture_size;
}
static const char* gfx_opengl_get_name() {
return "OpenGL";
}
static struct GfxClipParameters gfx_opengl_get_clip_parameters(void) {
return { false, framebuffers[current_framebuffer].invert_y };
}
static void gfx_opengl_vertex_array_set_attribs(struct ShaderProgram* prg) {
size_t num_floats = prg->num_floats;
size_t pos = 0;
for (int i = 0; i < prg->num_attribs; i++) {
if (prg->attrib_locations[i] >= 0) {
glEnableVertexAttribArray(prg->attrib_locations[i]);
glVertexAttribPointer(prg->attrib_locations[i], prg->attrib_sizes[i], GL_FLOAT, GL_FALSE,
num_floats * sizeof(float), (void*)(pos * sizeof(float)));
}
pos += prg->attrib_sizes[i];
}
}
static void gfx_opengl_set_uniforms(struct ShaderProgram* prg) {
if (prg->frame_count_location >= 0) {
glUniform1i(prg->frame_count_location, frame_count);
}
if (prg->noise_scale_location >= 0) {
glUniform1f(prg->noise_scale_location, current_noise_scale);
}
if (prg->three_point_filter_locations[0] >= 0) {
glUniform1i(prg->three_point_filter_locations[0], current_textures_linear_filter[0]);
}
if (prg->three_point_filter_locations[1] >= 0) {
glUniform1i(prg->three_point_filter_locations[1], current_textures_linear_filter[1]);
}
}
static void gfx_opengl_unload_shader(struct ShaderProgram* old_prg) {
if (old_prg != NULL) {
for (int i = 0; i < old_prg->num_attribs; i++) {
if (old_prg->attrib_locations[i] >= 0) {
glDisableVertexAttribArray(old_prg->attrib_locations[i]);
}
}
}
}
static void gfx_opengl_load_shader(struct ShaderProgram* new_prg) {
// if (!new_prg) return;
glUseProgram(new_prg->opengl_program_id);
gfx_opengl_vertex_array_set_attribs(new_prg);
gfx_opengl_set_uniforms(new_prg);
}
static void append_str(char* buf, size_t* len, const char* str) {
while (*str != '\0') {
buf[(*len)++] = *str++;
}
}
static void append_line(char* buf, size_t* len, const char* str) {
while (*str != '\0') {
buf[(*len)++] = *str++;
}
buf[(*len)++] = '\n';
}
#define RAND_NOISE "((random(vec3(floor(gl_FragCoord.xy * noise_scale), float(frame_count))) + 1.0) / 2.0)"
static const char* shader_item_to_str(uint32_t item, bool with_alpha, bool only_alpha, bool inputs_have_alpha,
bool hint_single_element) {
if (!only_alpha) {
switch (item) {
case SHADER_0:
return with_alpha ? "vec4(0.0, 0.0, 0.0, 0.0)" : "vec3(0.0, 0.0, 0.0)";
case SHADER_1:
return with_alpha ? "vec4(1.0, 1.0, 1.0, 1.0)" : "vec3(1.0, 1.0, 1.0)";
case SHADER_INPUT_1:
return with_alpha || !inputs_have_alpha ? "cInput1" : "cInput1.rgb";
case SHADER_INPUT_2:
return with_alpha || !inputs_have_alpha ? "cInput2" : "cInput2.rgb";
case SHADER_INPUT_3:
return with_alpha || !inputs_have_alpha ? "cInput3" : "cInput3.rgb";
case SHADER_INPUT_4:
return with_alpha || !inputs_have_alpha ? "cInput4" : "cInput4.rgb";
case SHADER_TEXEL0:
return with_alpha ? "texVal0" : "texVal0.rgb";
case SHADER_TEXEL0A:
return hint_single_element ? "texVal0.a"
: (with_alpha ? "vec4(texVal0.a, texVal0.a, texVal0.a, texVal0.a)"
: "vec3(texVal0.a, texVal0.a, texVal0.a)");
case SHADER_TEXEL1A:
return hint_single_element ? "texVal1.a"
: (with_alpha ? "vec4(texVal1.a, texVal1.a, texVal1.a, texVal1.a)"
: "vec3(texVal1.a, texVal1.a, texVal1.a)");
case SHADER_TEXEL1:
return with_alpha ? "texVal1" : "texVal1.rgb";
case SHADER_COMBINED:
return with_alpha ? "texel" : "texel.rgb";
case SHADER_NOISE:
return with_alpha ? "vec4(" RAND_NOISE ", " RAND_NOISE ", " RAND_NOISE ", " RAND_NOISE ")"
: "vec3(" RAND_NOISE ", " RAND_NOISE ", " RAND_NOISE ")";
}
} else {
switch (item) {
case SHADER_0:
return "0.0";
case SHADER_1:
return "1.0";
case SHADER_INPUT_1:
return "cInput1.a";
case SHADER_INPUT_2:
return "cInput2.a";
case SHADER_INPUT_3:
return "cInput3.a";
case SHADER_INPUT_4:
return "cInput4.a";
case SHADER_TEXEL0:
return "texVal0.a";
case SHADER_TEXEL0A:
return "texVal0.a";
case SHADER_TEXEL1A:
return "texVal1.a";
case SHADER_TEXEL1:
return "texVal1.a";
case SHADER_COMBINED:
return "texel.a";
case SHADER_NOISE:
return RAND_NOISE;
}
}
return "";
}
#undef RAND_NOISE
static void append_formula(char* buf, size_t* len, uint8_t c[2][4], bool do_single, bool do_multiply, bool do_mix,
bool with_alpha, bool only_alpha, bool opt_alpha) {
if (do_single) {
append_str(buf, len, shader_item_to_str(c[only_alpha][3], with_alpha, only_alpha, opt_alpha, false));
} else if (do_multiply) {
append_str(buf, len, shader_item_to_str(c[only_alpha][0], with_alpha, only_alpha, opt_alpha, false));
append_str(buf, len, " * ");
append_str(buf, len, shader_item_to_str(c[only_alpha][2], with_alpha, only_alpha, opt_alpha, true));
} else if (do_mix) {
append_str(buf, len, "mix(");
append_str(buf, len, shader_item_to_str(c[only_alpha][1], with_alpha, only_alpha, opt_alpha, false));
append_str(buf, len, ", ");
append_str(buf, len, shader_item_to_str(c[only_alpha][0], with_alpha, only_alpha, opt_alpha, false));
append_str(buf, len, ", ");
append_str(buf, len, shader_item_to_str(c[only_alpha][2], with_alpha, only_alpha, opt_alpha, true));
append_str(buf, len, ")");
} else {
append_str(buf, len, "(");
append_str(buf, len, shader_item_to_str(c[only_alpha][0], with_alpha, only_alpha, opt_alpha, false));
append_str(buf, len, " - ");
append_str(buf, len, shader_item_to_str(c[only_alpha][1], with_alpha, only_alpha, opt_alpha, false));
append_str(buf, len, ") * ");
append_str(buf, len, shader_item_to_str(c[only_alpha][2], with_alpha, only_alpha, opt_alpha, true));
append_str(buf, len, " + ");
append_str(buf, len, shader_item_to_str(c[only_alpha][3], with_alpha, only_alpha, opt_alpha, false));
}
}
static struct ShaderProgram* gfx_opengl_create_and_load_new_shader(uint64_t shader_id0, uint32_t shader_id1) {
struct CCFeatures cc_features = { 0 };
gfx_cc_get_features(shader_id0, shader_id1, &cc_features);
char vs_buf[2048];
char fs_buf[8192];
size_t vs_len = 0;
size_t fs_len = 0;
size_t num_floats = 4;
// Vertex shader
vs_len += sprintf(vs_buf + vs_len, "#version %s\n", gl_glsl_version_str);
if (gl_es) {
append_line(vs_buf, &vs_len, "precision mediump float;");
}
if (gl_glsl_version >= 130) {
append_line(vs_buf, &vs_len, "#define INPUT in");
append_line(vs_buf, &vs_len, "#define OUTPUT out");
} else {
append_line(vs_buf, &vs_len, "#define INPUT attribute");
append_line(vs_buf, &vs_len, "#define OUTPUT varying");
}
append_line(vs_buf, &vs_len, "INPUT vec4 aVtxPos;");
for (int i = 0; i < 2; i++) {
if (cc_features.used_textures[i]) {
vs_len += sprintf(vs_buf + vs_len, "INPUT vec2 aTexCoord%d;\n", i);
vs_len += sprintf(vs_buf + vs_len, "OUTPUT vec2 vTexCoord%d;\n", i);
num_floats += 2;
for (int j = 0; j < 2; j++) {
if (cc_features.clamp[i][j]) {
vs_len += sprintf(vs_buf + vs_len, "INPUT float aTexClamp%s%d;\n", j == 0 ? "S" : "T", i);
vs_len += sprintf(vs_buf + vs_len, "OUTPUT float vTexClamp%s%d;\n", j == 0 ? "S" : "T", i);
num_floats += 1;
}
}
}
}
/* D540: RDP-style screen-affine shade colour (see gfx_opengl_init).
* D543: fog is the per-vertex RSP value by default, which the RDP also
* interpolates screen-affine (it rides in shade alpha), so it shares the
* shade qualifier. GE_FOGPIXEL=1 (D540 per-pixel) needs a perspective
* attribute: it carries fog * w, divided by w in the shader. */
const char *shadeq = gfx_rdp_affine ? "noperspective " : "";
const char *fogq = gfx_fog_vertex ? shadeq : "";
if (cc_features.opt_fog) {
append_line(vs_buf, &vs_len, "INPUT vec4 aFog;");
vs_len += sprintf(vs_buf + vs_len, "%sOUTPUT vec4 vFog;\n", fogq);
num_floats += 4;
}
if (cc_features.opt_grayscale) {
append_line(vs_buf, &vs_len, "INPUT vec4 aGrayscaleColor;");
append_line(vs_buf, &vs_len, "OUTPUT vec4 vGrayscaleColor;");
num_floats += 4;
}
for (int i = 0; i < cc_features.num_inputs; i++) {
vs_len += sprintf(vs_buf + vs_len, "INPUT vec%d aInput%d;\n", cc_features.opt_alpha ? 4 : 3, i + 1);
vs_len += sprintf(vs_buf + vs_len, "%sOUTPUT vec%d vInput%d;\n", shadeq, cc_features.opt_alpha ? 4 : 3, i + 1);
num_floats += cc_features.opt_alpha ? 4 : 3;
}
append_line(vs_buf, &vs_len, "void main() {");
for (int i = 0; i < 2; i++) {
if (cc_features.used_textures[i]) {
vs_len += sprintf(vs_buf + vs_len, " vTexCoord%d = aTexCoord%d;\n", i, i);
for (int j = 0; j < 2; j++) {
if (cc_features.clamp[i][j]) {
vs_len += sprintf(vs_buf + vs_len, " vTexClamp%s%d = aTexClamp%s%d;\n", j == 0 ? "S" : "T", i,
j == 0 ? "S" : "T", i);
}
}
}
}
if (cc_features.opt_fog) {
append_line(vs_buf, &vs_len, " vFog = aFog;");
}
if (cc_features.opt_grayscale) {
append_line(vs_buf, &vs_len, " vGrayscaleColor = aGrayscaleColor;");
}
for (int i = 0; i < cc_features.num_inputs; i++) {
vs_len += sprintf(vs_buf + vs_len, " vInput%d = aInput%d;\n", i + 1, i + 1);
}
append_line(vs_buf, &vs_len, " gl_Position = aVtxPos;");
if (!GLAD_GL_ARB_depth_clamp) {
// HACK: workaround for no GL_DEPTH_CLAMP
append_line(vs_buf, &vs_len, " gl_Position.z *= 0.3f;");
}
append_line(vs_buf, &vs_len, "}");
// Fragment shader
fs_len += sprintf(fs_buf + fs_len, "#version %s\n", gl_glsl_version_str);
if (gl_es) {
append_line(fs_buf, &fs_len, "precision mediump float;");
}
if (gl_glsl_version >= 130) {
append_line(fs_buf, &fs_len, "#define INPUT in");
append_line(fs_buf, &fs_len, "#define OUTPUT_COLOR outColor");
append_line(fs_buf, &fs_len, "#define SAMPLE_TEX(tex, uv) texture(tex, uv)");
} else {
append_line(fs_buf, &fs_len, "#define INPUT varying");
append_line(fs_buf, &fs_len, "#define OUTPUT_COLOR gl_FragColor");
append_line(fs_buf, &fs_len, "#define SAMPLE_TEX(tex, uv) texture2D(tex, uv)");
}
// Reference approach to color wrapping as per GLideN64
// Return wrapped value of x in interval [low, high)
append_line(fs_buf, &fs_len, "#define WRAP(x, low, high) mod((x)-(low), (high)-(low)) + (low)");
append_line(fs_buf, &fs_len, "#define TEX_OFFSET(tex, uv, texSize, off) SAMPLE_TEX(tex, uv - (off)/texSize)");
// append_line(fs_buf, &fs_len, "precision mediump float;");
for (int i = 0; i < 2; i++) {
if (cc_features.used_textures[i]) {
fs_len += sprintf(fs_buf + fs_len, "INPUT vec2 vTexCoord%d;\n", i);
for (int j = 0; j < 2; j++) {
if (cc_features.clamp[i][j]) {
fs_len += sprintf(fs_buf + fs_len, "INPUT float vTexClamp%s%d;\n", j == 0 ? "S" : "T", i);
}
}
}
}
if (cc_features.opt_fog) {
fs_len += sprintf(fs_buf + fs_len, "%sINPUT vec4 vFog;\n", fogq);
}
if (cc_features.opt_grayscale) {
append_line(fs_buf, &fs_len, "INPUT vec4 vGrayscaleColor;");
}
for (int i = 0; i < cc_features.num_inputs; i++) {
fs_len += sprintf(fs_buf + fs_len, "%sINPUT vec%d vInput%d;\n", shadeq, cc_features.opt_alpha ? 4 : 3, i + 1);
}
if (cc_features.used_textures[0]) {
append_line(fs_buf, &fs_len, "uniform sampler2D uTex0;");
if (current_filter_mode == FILTER_THREE_POINT)
append_line(fs_buf, &fs_len, "uniform int three_point_filter0;");
}
if (cc_features.used_textures[1]) {
append_line(fs_buf, &fs_len, "uniform sampler2D uTex1;");
if (current_filter_mode == FILTER_THREE_POINT)
append_line(fs_buf, &fs_len, "uniform int three_point_filter1;");
}
append_line(fs_buf, &fs_len, "uniform int frame_count;");
append_line(fs_buf, &fs_len, "uniform float noise_scale;");
append_line(fs_buf, &fs_len, "float random(in vec3 value) {");
append_line(fs_buf, &fs_len, " float random = dot(sin(value), vec3(12.9898, 78.233, 37.719));");
append_line(fs_buf, &fs_len, " return fract(sin(random) * 143758.5453);");
append_line(fs_buf, &fs_len, "}");
if (current_filter_mode == FILTER_THREE_POINT) {
append_line(fs_buf, &fs_len, "vec4 filter3point(in sampler2D tex, in vec2 texCoord, in vec2 texSize) {");
append_line(fs_buf, &fs_len, " vec2 offset = fract(texCoord*texSize - vec2(0.5));");
append_line(fs_buf, &fs_len, " offset -= step(1.0, offset.x + offset.y);");
append_line(fs_buf, &fs_len, " vec4 c0 = TEX_OFFSET(tex, texCoord, texSize, offset);");
append_line(fs_buf, &fs_len, " vec4 c1 = TEX_OFFSET(tex, texCoord, texSize, vec2(offset.x - sign(offset.x), offset.y));");
append_line(fs_buf, &fs_len, " vec4 c2 = TEX_OFFSET(tex, texCoord, texSize, vec2(offset.x, offset.y - sign(offset.y)));");
append_line(fs_buf, &fs_len, " return c0 + abs(offset.x)*(c1-c0) + abs(offset.y)*(c2-c0);");
append_line(fs_buf, &fs_len, "}");
}
if (cc_features.opt_blur) {
// blur filter, used for menu backgrounds
// used to be two for loops from 0 to 4, but apparently intel drivers crashed trying to unroll it
// used to have a const weight array, but apparently drivers for the GT620 don't like const array initializers
if (current_filter_mode == FILTER_THREE_POINT)
append_line(fs_buf, &fs_len, "lowp vec4 hookTexture2D(in sampler2D t, in vec2 uv, in vec2 texSize, in int three_point_filter) {");
else
append_line(fs_buf, &fs_len, "lowp vec4 hookTexture2D(in sampler2D t, in vec2 uv, in vec2 texSize) {");
append_line(fs_buf, &fs_len, " lowp vec4 cw = vec4(0.0);");
append_line(fs_buf, &fs_len, " for (int i = 0; i < 16; ++i) {");
append_line(fs_buf, &fs_len, " vec2 xy = vec2(float(i & 3), float(i >> 2));");
append_line(fs_buf, &fs_len, " lowp float w = 0.009947 - length(xy) * 0.001;");
append_line(fs_buf, &fs_len, " vec2 scaled_uv = uv + (vec2(-1.5) + xy) / texSize;");
if (current_filter_mode == FILTER_THREE_POINT)
append_line(fs_buf, &fs_len, " lowp vec4 tex = mix(SAMPLE_TEX(t, scaled_uv), filter3point(t, scaled_uv, texSize), three_point_filter);");
else
append_line(fs_buf, &fs_len, " lowp vec4 tex = SAMPLE_TEX(t, scaled_uv);");
append_line(fs_buf, &fs_len, " cw += vec4(tex.rgb * w, w);");
append_line(fs_buf, &fs_len, " }");
append_line(fs_buf, &fs_len, " return vec4(cw.rgb / cw.a, 1.0);");
append_line(fs_buf, &fs_len, "}");
} else {
if (current_filter_mode == FILTER_THREE_POINT) {
append_line(fs_buf, &fs_len, "vec4 hookTexture2D(in sampler2D tex, in vec2 uv, in vec2 texSize, in int three_point_filter) {");
append_line(fs_buf, &fs_len, " return mix(SAMPLE_TEX(tex, uv), filter3point(tex, uv, texSize), three_point_filter);");
append_line(fs_buf, &fs_len, "}");
} else {
append_line(fs_buf, &fs_len, "vec4 hookTexture2D(in sampler2D tex, in vec2 uv, in vec2 texSize) {");
append_line(fs_buf, &fs_len, " return SAMPLE_TEX(tex, uv);");
append_line(fs_buf, &fs_len, "}");
}
}
if (gl_glsl_version >= 130) {
append_line(fs_buf, &fs_len, "out vec4 outColor;");
}
append_line(fs_buf, &fs_len, "void main() {");
/* D548: combiner inputs are 8-bit on the N64 (shade, prim, env), so never
* above 1.0. Under the D540 noperspective shade, GPU clipping of a big
* near-camera triangle can extrapolate them past 1.0; the CC wrap
* emulation below then turned walls into black/white/yellow/blue bands
* (Aztec, near rocket fire). Clamp to the hardware range first. */
for (int i = 0; i < cc_features.num_inputs; i++) {
fs_len += sprintf(fs_buf + fs_len, " vec%d cInput%d = clamp(vInput%d, 0.0, 1.0);\n",
cc_features.opt_alpha ? 4 : 3, i + 1, i + 1);
}
for (int i = 0; i < 2; i++) {
if (cc_features.used_textures[i]) {
bool s = cc_features.clamp[i][0], t = cc_features.clamp[i][1];
fs_len += sprintf(fs_buf + fs_len, " vec2 texSize%d = vec2(textureSize(uTex%d, 0));\n", i, i);
if (!s && !t) {
fs_len += sprintf(fs_buf + fs_len, " vec2 vTexCoordAdj%d = vTexCoord%d;\n", i, i);
} else {
if (s && t) {
fs_len += sprintf(fs_buf + fs_len,
" vec2 vTexCoordAdj%d = clamp(vTexCoord%d, 0.5 / texSize%d, "
"vec2(vTexClampS%d, vTexClampT%d));\n",
i, i, i, i, i);
} else if (s) {
fs_len += sprintf(fs_buf + fs_len,
" vec2 vTexCoordAdj%d = vec2(clamp(vTexCoord%d.s, 0.5 / "
"texSize%d.s, vTexClampS%d), vTexCoord%d.t);\n",
i, i, i, i, i);
} else {
fs_len += sprintf(fs_buf + fs_len,
" vec2 vTexCoordAdj%d = vec2(vTexCoord%d.s, clamp(vTexCoord%d.t, "
"0.5 / texSize%d.t, vTexClampT%d));\n",
i, i, i, i, i);
}
}
if (current_filter_mode == FILTER_THREE_POINT)
fs_len += sprintf(fs_buf + fs_len, " vec4 texVal%d = hookTexture2D(uTex%d, vTexCoordAdj%d, texSize%d, three_point_filter%d);\n", i, i, i, i, i);
else
fs_len += sprintf(fs_buf + fs_len, " vec4 texVal%d = hookTexture2D(uTex%d, vTexCoordAdj%d, texSize%d);\n", i, i, i, i);
}
}
append_line(fs_buf, &fs_len, cc_features.opt_alpha ? " vec4 texel;" : " vec3 texel;");
for (int c = 0; c < (cc_features.opt_2cyc ? 2 : 1); c++) {
append_str(fs_buf, &fs_len, " texel = ");
if (!cc_features.color_alpha_same[c] && cc_features.opt_alpha) {
append_str(fs_buf, &fs_len, "vec4(");
append_formula(fs_buf, &fs_len, cc_features.c[c], cc_features.do_single[c][0],
cc_features.do_multiply[c][0], cc_features.do_mix[c][0], false, false, true);
append_str(fs_buf, &fs_len, ", ");
append_formula(fs_buf, &fs_len, cc_features.c[c], cc_features.do_single[c][1],
cc_features.do_multiply[c][1], cc_features.do_mix[c][1], true, true, true);
append_str(fs_buf, &fs_len, ")");
} else {
append_formula(fs_buf, &fs_len, cc_features.c[c], cc_features.do_single[c][0],
cc_features.do_multiply[c][0], cc_features.do_mix[c][0], cc_features.opt_alpha, false,
cc_features.opt_alpha);
}
append_line(fs_buf, &fs_len, ";");
if (c == 0) {
append_line(fs_buf, &fs_len, " texel = WRAP(texel, -1.01, 1.01);");
}
}
append_line(fs_buf, &fs_len, " texel = WRAP(texel, -0.51, 1.51);");
append_line(fs_buf, &fs_len, " texel = clamp(texel, 0.0, 1.0);");
// TODO discard if alpha is 0?
if (cc_features.opt_fog) {
/* D543 default: vFog.a is the per-vertex RSP value, used as-is
* (N64). GE_FOGPIXEL=1 (D540): vFog.a = fog * w, divided by w here =
* exact per-pixel fog from the pixel's own depth. */
append_line(fs_buf, &fs_len, gfx_fog_vertex ? " float fogA = clamp(vFog.a, 0.0, 1.0);"
: " float fogA = clamp(vFog.a * gl_FragCoord.w, 0.0, 1.0);");
/* D540: the D503 snap is OFF by default -- its hard 97% edge swept
* across the ground in tile-shaped chunks as the player walked
* (Deck playtest; maintainer-confirmed with it off). GE_FOGSNAP=1
* restores it. */
static int fogSnap = -1;
if (fogSnap < 0) { const char *e = getenv("GE_FOGSNAP"); fogSnap = (e && e[0] == '1'); }
if (fogSnap) append_line(fs_buf, &fs_len, " if (fogA >= 247.0 / 255.0) fogA = 1.0;");
if (cc_features.opt_alpha) {
append_line(fs_buf, &fs_len, " texel = vec4(mix(texel.rgb, vFog.rgb, fogA), texel.a);");
} else {
append_line(fs_buf, &fs_len, " texel = mix(texel, vFog.rgb, fogA);");
}
}
/* D266: CVG_X_ALPHA ("texedge") modelled on the RDP's actual semantics,
* keyed on the alphacompare field. The inherited line quantized to
* opaque/discard at a hardcoded 0.19: it kept texels the N64 discards and
* forced survivors fully opaque, defeating the ALPHA_CVG_SEL blend.
* GE's Surface 1 IA4 tree strip (D236/D265) rendered as a wall because of
* it. Now: AC_DITHER -> dithered discard; AC_THRESHOLD -> discard only
* fully-transparent; AC_NONE -> discard below 0x80 and let survivors keep
* their alpha for blending. (AC_DECAL never sets SHADER_OPT_TEXTURE_EDGE,
* see gfx_pc.cpp.) */
if (cc_features.opt_texture_edge && cc_features.opt_alpha) {
if (cc_features.opt_noise) {
append_line(fs_buf, &fs_len,
" if (texel.a < ((random(vec3(floor(gl_FragCoord.xy * noise_scale), float(frame_count))) + 1.0) / 2.0)) discard;");
} else if (cc_features.opt_alpha_threshold) {
append_line(fs_buf, &fs_len, " if (texel.a < 1.0 / 256.0) discard;");
} else {
append_line(fs_buf, &fs_len, " if (texel.a < 0.5) discard;");
}
}
if (cc_features.opt_alpha && cc_features.opt_noise && !cc_features.opt_texture_edge) {
append_line(fs_buf, &fs_len,
" texel.a *= floor(clamp(random(vec3(floor(gl_FragCoord.xy * noise_scale), float(frame_count))) + "
"texel.a, 0.0, 1.0));");
}
if (cc_features.opt_grayscale) {
append_line(fs_buf, &fs_len, " float intensity = (texel.r + texel.g + texel.b) / 3.0;");
append_line(fs_buf, &fs_len, " vec3 new_texel = vGrayscaleColor.rgb * intensity;");
append_line(fs_buf, &fs_len, " texel.rgb = mix(texel.rgb, new_texel, vGrayscaleColor.a);");
}
if (cc_features.opt_alpha) {
if (cc_features.opt_alpha_threshold) {
append_line(fs_buf, &fs_len, " if (texel.a < 8.0 / 256.0) discard;");
}
if (cc_features.opt_invisible) {
append_line(fs_buf, &fs_len, " texel.a = 0.0;");
}
append_line(fs_buf, &fs_len, " OUTPUT_COLOR = texel;");
} else {
append_line(fs_buf, &fs_len, " OUTPUT_COLOR = vec4(texel, 1.0);");
}
append_line(fs_buf, &fs_len, "}");
vs_buf[vs_len] = '\0';
fs_buf[fs_len] = '\0';
const GLchar* sources[2] = { vs_buf, fs_buf };
const GLint lengths[2] = { (GLint)vs_len, (GLint)fs_len };
GLint success;
GLuint vertex_shader = glCreateShader(GL_VERTEX_SHADER);
glShaderSource(vertex_shader, 1, &sources[0], &lengths[0]);
glCompileShader(vertex_shader);
glGetShaderiv(vertex_shader, GL_COMPILE_STATUS, &success);
if (!success) {
GLint max_length = 1024;
glGetShaderiv(vertex_shader, GL_INFO_LOG_LENGTH, &max_length);
char error_log[1024];
glGetShaderInfoLog(vertex_shader, max_length, &max_length, &error_log[0]);
sysLogPrintf(LOG_ERROR, "Failed to compile this vertex shader (ID %llx, %x):\n%s", shader_id0, shader_id1, vs_buf);
sysFatalError("Vertex shader compilation failed:\n%s", error_log);
}
GLuint fragment_shader = glCreateShader(GL_FRAGMENT_SHADER);
glShaderSource(fragment_shader, 1, &sources[1], &lengths[1]);
glCompileShader(fragment_shader);
glGetShaderiv(fragment_shader, GL_COMPILE_STATUS, &success);
if (!success) {
GLint max_length = 1024;
glGetShaderiv(fragment_shader, GL_INFO_LOG_LENGTH, &max_length);
char error_log[1024];
glGetShaderInfoLog(fragment_shader, max_length, &max_length, &error_log[0]);
sysLogPrintf(LOG_ERROR, "Failed to compile this fragment shader (ID %llx, %x):\n%s", shader_id0, shader_id1, fs_buf);
sysFatalError("Fragment shader compilation failed:\n%s", error_log);
}
GLuint shader_program = glCreateProgram();
glAttachShader(shader_program, vertex_shader);
glAttachShader(shader_program, fragment_shader);
glLinkProgram(shader_program);
glDetachShader(shader_program, vertex_shader);
glDetachShader(shader_program, fragment_shader);
glDeleteShader(vertex_shader);
glDeleteShader(fragment_shader);
size_t cnt = 0;
struct ShaderProgram* prg = &shader_program_pool[make_pair(shader_id0, shader_id1)];
prg->attrib_locations[cnt] = glGetAttribLocation(shader_program, "aVtxPos");
prg->attrib_sizes[cnt] = 4;
++cnt;
for (int i = 0; i < 2; i++) {
if (cc_features.used_textures[i]) {
char name[32];
sprintf(name, "aTexCoord%d", i);
prg->attrib_locations[cnt] = glGetAttribLocation(shader_program, name);
prg->attrib_sizes[cnt] = 2;
++cnt;
for (int j = 0; j < 2; j++) {
if (cc_features.clamp[i][j]) {
sprintf(name, "aTexClamp%s%d", j == 0 ? "S" : "T", i);
prg->attrib_locations[cnt] = glGetAttribLocation(shader_program, name);
prg->attrib_sizes[cnt] = 1;
++cnt;
}
}
}
}
if (cc_features.opt_fog) {
prg->attrib_locations[cnt] = glGetAttribLocation(shader_program, "aFog");
prg->attrib_sizes[cnt] = 4;
++cnt;
}
if (cc_features.opt_grayscale) {
prg->attrib_locations[cnt] = glGetAttribLocation(shader_program, "aGrayscaleColor");
prg->attrib_sizes[cnt] = 4;
++cnt;
}
for (int i = 0; i < cc_features.num_inputs; i++) {
char name[16];
sprintf(name, "aInput%d", i + 1);
prg->attrib_locations[cnt] = glGetAttribLocation(shader_program, name);
prg->attrib_sizes[cnt] = cc_features.opt_alpha ? 4 : 3;
++cnt;
}
prg->opengl_program_id = shader_program;
prg->num_inputs = cc_features.num_inputs;
prg->used_textures[0] = cc_features.used_textures[0];
prg->used_textures[1] = cc_features.used_textures[1];
prg->num_floats = num_floats;
prg->num_attribs = cnt;
glUseProgram(shader_program);
if (cc_features.used_textures[0]) {
GLint sampler_location = glGetUniformLocation(shader_program, "uTex0");
glUniform1i(sampler_location, 0);
}
if (cc_features.used_textures[1]) {
GLint sampler_location = glGetUniformLocation(shader_program, "uTex1");
glUniform1i(sampler_location, 1);
}
prg->frame_count_location = glGetUniformLocation(shader_program, "frame_count");
prg->noise_scale_location = glGetUniformLocation(shader_program, "noise_scale");
prg->three_point_filter_locations[0] = glGetUniformLocation(shader_program, "three_point_filter0");
prg->three_point_filter_locations[1] = glGetUniformLocation(shader_program, "three_point_filter1");
gfx_opengl_load_shader(prg);
return prg;
}
static struct ShaderProgram* gfx_opengl_lookup_shader(uint64_t shader_id0, uint32_t shader_id1) {
auto it = shader_program_pool.find(make_pair(shader_id0, shader_id1));
return it == shader_program_pool.end() ? nullptr : &it->second;
}
static void gfx_opengl_shader_get_info(struct ShaderProgram* prg, uint8_t* num_inputs, bool used_textures[2]) {
*num_inputs = prg->num_inputs;
used_textures[0] = prg->used_textures[0];
used_textures[1] = prg->used_textures[1];
}
static void gfx_opengl_clear_shaders(void) {
glUseProgram(0);
for (auto& pair : shader_program_pool) {
glDeleteProgram(pair.second.opengl_program_id);
}
shader_program_pool.clear();
}
static GLuint gfx_opengl_new_texture(void) {
GLuint ret;
glGenTextures(1, &ret);
return ret;
}
static void gfx_opengl_delete_texture(uint32_t texID) {
glDeleteTextures(1, &texID);
}
static void gfx_opengl_select_texture(int tile, GLuint texture_id, bool linear_filter) {
glActiveTexture(GL_TEXTURE0 + tile);
glBindTexture(GL_TEXTURE_2D, texture_id);
current_textures_linear_filter[tile] = linear_filter;
}
static void gfx_opengl_upload_texture(const uint8_t* rgba32_buf, uint32_t width, uint32_t height, bool gen_mipmaps) {
#ifdef PORT
/* GE_TEXDUMP: PPM-dump every uploaded texture (first N) for B2/D161 triage.
* D250: this runs on every texture upload -- cache the getenv() like the
* gfx_pc.cpp import_texture() sites (uncached getenv() measured at ~50%
* of the hot render thread's CPU time via a live profile). */
static int ge_texdump_gl = -1;
if (ge_texdump_gl < 0) ge_texdump_gl = getenv("GE_TEXDUMP") != NULL;
if (ge_texdump_gl) {
static int td = 0;
static int td_fire = 0;
/* D219: the 400-cap fills up during ordinary level load, long before
* a player-triggered explosion. Never suppress the 16x14 RGBA fire
* particle images (assets/oddtextures.c IMAGE_FIRE_N via
* globalDL_0x078..0x9a8) -- that's the one shape we actually need. */
const bool is_fire_shape = (width == 16 && height == 14);
if ((is_fire_shape || td < 400) && width && height && width < 4096 && height < 4096) {
char nm[128];
if (is_fire_shape) {
snprintf(nm, sizeof nm, "texdump/fire%03d_%ux%u_mip%d.ppm", td_fire++, width, height, (int)gen_mipmaps);
} else {
snprintf(nm, sizeof nm, "texdump/t%03d_%ux%u_mip%d.ppm", td++, width, height, (int)gen_mipmaps);
}
FILE* f = fopen(nm, "wb");
if (f) {
fprintf(f, "P6\n%u %u\n255\n", width, height);
for (uint32_t p = 0; p < width * height; p++)
fwrite(rgba32_buf + 4 * p, 1, 3, f);
fclose(f);
}
}
}
#endif
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba32_buf);
/* Trilinear option (playtest 2026-10-03): force mips for TRILINEAR too. */
if (gen_mipmaps || current_filter_mode == FILTER_THREE_POINT || current_filter_mode == FILTER_TRILINEAR) {
glGenerateMipmap(GL_TEXTURE_2D);
}
}
static uint32_t gfx_cm_to_opengl(uint32_t val) {
switch (val) {
case G_TX_NOMIRROR | G_TX_CLAMP:
return GL_CLAMP_TO_EDGE;
case G_TX_MIRROR | G_TX_WRAP:
return GL_MIRRORED_REPEAT;
case G_TX_MIRROR | G_TX_CLAMP:
return gl_mirror_clamp;
case G_TX_NOMIRROR | G_TX_WRAP:
return GL_REPEAT;
}
return 0;
}
static void gfx_opengl_set_sampler_parameters(int tile, bool linear_filter, uint32_t cms, uint32_t cmt, bool mipmaps) {
const GLint min_filters[4][3] = {
// MIPMAP_DISABLED MIPMAP_NEAREST MIPMAP_LINEAR
{ GL_NEAREST, GL_NEAREST_MIPMAP_NEAREST, GL_NEAREST_MIPMAP_LINEAR }, // FILTER_NONE
{ GL_LINEAR, GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR }, // FILTER_BILINEAR
{ GL_NEAREST, GL_LINEAR_MIPMAP_NEAREST, GL_LINEAR_MIPMAP_LINEAR }, // FILTER_THREE_POINT
{ GL_LINEAR, GL_LINEAR_MIPMAP_LINEAR, GL_LINEAR_MIPMAP_LINEAR }, // FILTER_TRILINEAR (Trilinear option, playtest 2026-10-03)
};
// B1: the shader does 3-point on magnification; for minification let the
// hardware do trilinear mips. THREE_POINT force-generates mips at upload
// (see gfx_opengl_upload_texture) so it can always sample them here -- this
// is what kills the grazing-angle shimmer (Depot roof, docs/BRIEF-B2).
/* Trilinear option (playtest 2026-10-03): TRILINEAR force-generates mips
* at upload too, so it can always sample them here. */
if (current_filter_mode == FILTER_THREE_POINT || current_filter_mode == FILTER_TRILINEAR) {
mipmaps = true;
}
mipmaps = mipmaps && (current_mipmap_filter_mode != MIPMAP_DISABLED);
const int mip_idx = mipmaps ? current_mipmap_filter_mode : 0;
const GLint min_filter = linear_filter ? min_filters[current_filter_mode][mip_idx] : GL_NEAREST;
/* Trilinear option (playtest 2026-10-03): TRILINEAR magnifies LINEAR too. */
const GLint max_filter = linear_filter && (current_filter_mode == FILTER_LINEAR || current_filter_mode == FILTER_TRILINEAR) ? GL_LINEAR : GL_NEAREST;
glActiveTexture(GL_TEXTURE0 + tile);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, min_filter);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, max_filter);
#ifdef PORT
/* D446: anisotropy is per-texture-object state. It was only ever SET
* (when mipmapped), never reset, so a GL texture id freed by
* gfx_texture_cache_delete_range and reused for a non-mipmapped texture
* (HUD / watch text glyphs) kept the previous owner's 4x -- llvmpipe then
* filters that glyph differently (+-1..2 per channel). Which id a texture
* inherits follows the unordered_map iteration order, i.e. the texture
* ADDRESSES, so the frame changed with the binary's data layout. Always
* write it: the level when mipmapped, 1 (off) otherwise. */
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, mipmaps ? (float)current_anisotropy_level : 1.0f);
#else
if (mipmaps) {
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY, current_anisotropy_level);
}
#endif
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, gfx_cm_to_opengl(cms));
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, gfx_cm_to_opengl(cmt));
}
static void gfx_opengl_set_depth_mode(bool depth_test, bool depth_update, bool depth_compare, bool depth_source_prim, uint16_t zmode) {
if (depth_test) {
glEnable(GL_DEPTH_TEST);
glDepthMask(depth_update ? GL_TRUE : GL_FALSE);
current_depth_mask = depth_update;
if (depth_compare) {
switch (zmode) {
case ZMODE_INTER:
glDepthFunc(GL_LEQUAL);
glDisable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(0, 0);
break;
case ZMODE_OPA:
case ZMODE_XLU:
if (depth_source_prim) {
glDepthFunc(GL_LEQUAL);
} else {
glDepthFunc(GL_LESS);
}
glDisable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(0, 0);
break;
case ZMODE_DEC:
glDepthFunc(GL_LEQUAL);
glEnable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(-2, -2);
break;
}
} else {
glDepthFunc(GL_ALWAYS);
glDisable(GL_POLYGON_OFFSET_FILL);
glPolygonOffset(0, 0);
}
} else {
glDisable(GL_DEPTH_TEST);
}
}
static void gfx_opengl_set_depth_range(float znear, float zfar) {
if (glDepthRangef) {
glDepthRangef(znear, zfar);
} else {
glDepthRange(znear, zfar);
}
}
static void gfx_opengl_set_viewport(int x, int y, int width, int height) {
glViewport(x, y, width, height);
}
static void gfx_opengl_set_scissor(int x, int y, int width, int height) {
glScissor(x, y, width, height);
}
static void gfx_opengl_set_use_alpha(bool use_alpha, bool modulate) {
if (use_alpha) {
glEnable(GL_BLEND);
} else {
glDisable(GL_BLEND);
}
if (modulate) {
glBlendFunc(GL_DST_COLOR, GL_ZERO);
} else {
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
}
}
static void gfx_opengl_draw_triangles(float buf_vbo[], size_t buf_vbo_len, size_t buf_vbo_num_tris) {
// printf("flushing %d tris\n", buf_vbo_num_tris);
glBufferData(GL_ARRAY_BUFFER, sizeof(float) * buf_vbo_len, buf_vbo, GL_STREAM_DRAW);
glDrawArrays(GL_TRIANGLES, 0, 3 * buf_vbo_num_tris);
}
typedef void (APIENTRY *DEBUGPROC)(GLenum source,
GLenum type,
GLuint id,
GLenum severity,
GLsizei length,
const GLchar *message,
const void *userParam);
static void APIENTRY gl_debug(GLenum source, GLenum type, GLuint id, GLenum severity, GLsizei length, const GLchar *msg, const void *p) {
sysLogPrintf(LOG_WARNING, "GL: (%05x) %s", id, msg);
}
static void gfx_opengl_enable_debug(void) {
if (GLAD_GL_KHR_debug) {
glEnable(GL_DEBUG_OUTPUT);
}
if (glDebugMessageControl != NULL) {
// enable everything except some specific spam messages
const GLuint disable[] = {
0x20061, /* "Framebuffer detailed info" */
0x20071 /* "Buffer detailed info" */
};
glDebugMessageControl(GL_DONT_CARE, GL_DONT_CARE, GL_DONT_CARE, 0, NULL, GL_TRUE);
glDebugMessageControl(GL_DEBUG_SOURCE_API, GL_DEBUG_TYPE_OTHER, GL_DONT_CARE, 2, disable, GL_FALSE);
}
if (glDebugMessageCallback != NULL) {
glDebugMessageCallback(gl_debug, NULL);
}
}
static bool gfx_opengl_supports_framebuffers(void) {
if (GLVersion.major > 2) {
// GL3.0+ supports everything we need, but we'll still check it for sanity
return (glad_glFramebufferRenderbuffer && glad_glBlitFramebuffer && glad_glRenderbufferStorageMultisample);
}
if (GLAD_GL_ARB_framebuffer_object) {
// some implementations might be missing these
return (glad_glBlitFramebuffer && glad_glRenderbufferStorageMultisample);
}
if (GLAD_GL_EXT_framebuffer_object && GLAD_GL_EXT_framebuffer_blit && GLAD_GL_EXT_framebuffer_multisample) {
// sanity check
return (glad_glFramebufferRenderbuffer && glad_glBlitFramebuffer && glad_glRenderbufferStorageMultisample);
}
// nothing
return false;
}
static bool gfx_opengl_supports_shaders(void) {
if (GLVersion.major > 2) {
// should support GLSL130+
return true;
}
// check supported GLSL version
const char *ver = (const char *)glGetString(GL_SHADING_LANGUAGE_VERSION);
if (ver) {
int maj = 0, min = 0;
sscanf(ver, "%d.%d", &maj, &min);
if (maj > 1 || (maj == 1 && min > 20)) {
// above 120, should be fine
return true;
}
}
// check for extension that adds textureSize
return GLAD_GL_EXT_gpu_shader4;
}
/* D482: GL_RENDERER as read at init, for video.c's low-end default check.
* A copy, never the driver's pointer; "" when the driver gave nothing. */
static char gl_renderer_str[256] = "";
extern "C" const char *gfx_opengl_renderer_string(void) {
return gl_renderer_str;
}
static void gfx_opengl_log_info(void) {
const char *version = (const char *)glGetString(GL_VERSION);
const char *vendor = (const char *)glGetString(GL_VENDOR);
const char *renderer = (const char *)glGetString(GL_RENDERER);
snprintf(gl_renderer_str, sizeof(gl_renderer_str), "%s", renderer ? renderer : "");
const char *glsl_version = (const char *)glGetString(GL_SHADING_LANGUAGE_VERSION);
sysLogPrintf(LOG_NOTE, "GL: version: %s", version ? version : "unknown");
sysLogPrintf(LOG_NOTE, "GL: vendor: %s", vendor ? vendor : "unknown");
sysLogPrintf(LOG_NOTE, "GL: renderer: %s", renderer ? renderer : "unknown");
sysLogPrintf(LOG_NOTE, "GL: GLSL version: %s", glsl_version ? glsl_version : "unknown");
sysLogPrintf(LOG_NOTE, "GL: ARB_framebuffer_object: %s", gfx_opengl_supports_framebuffers() ? "yes" : "no");
sysLogPrintf(LOG_NOTE, "GL: ARB_depth_clamp: %s", GLAD_GL_ARB_depth_clamp ? "yes" : "no");
sysLogPrintf(LOG_NOTE, "GL: ARB_texture_mirror_clamp_to_edge: %s", GLAD_GL_ARB_texture_mirror_clamp_to_edge ? "yes" : "no");
}
static void *gl_load_proc(const char *name) {
void *ret = SDL_GL_GetProcAddress(name);
if (ret) {
return ret;
}
// try with postfixes
static const char *post[] = { "ARB", "EXT" };
char tmp[256] = { 0 };
for (size_t i = 0; i < sizeof(post) / sizeof(*post); ++i) {
snprintf(tmp, sizeof(tmp), "%s%s", name, post[i]);
ret = SDL_GL_GetProcAddress(tmp);
if (ret) {
return ret;
}
}
sysLogPrintf(LOG_ERROR, "GL: could not find function: %s", name);
return NULL;
}
static void gfx_opengl_init_extensions(void) {
// patch up some extension values and pointers
if (!GLAD_GL_ARB_depth_clamp) {
if (GLAD_GL_EXT_depth_clamp || GLAD_GL_NV_depth_clamp) {
GLAD_GL_ARB_depth_clamp = 1;
} else if (!gl_es && GLVersion.major >= 3) {
// GL3.2+ should have depth_clamp as part of the spec, but some devices don't report it for some reason
GLAD_GL_ARB_depth_clamp = (GLVersion.major > 3 || (GLVersion.major == 3 && GLVersion.minor >= 2));
}
}
if (!GLAD_GL_ARB_texture_mirror_clamp_to_edge) {
GLAD_GL_ARB_texture_mirror_clamp_to_edge = GLAD_GL_EXT_texture_mirror_clamp_to_edge;
}
if (GLVersion.major < 3 && !GLAD_GL_ARB_framebuffer_object) {
if (GLAD_GL_EXT_framebuffer_object && GLAD_GL_EXT_framebuffer_blit && GLAD_GL_EXT_framebuffer_multisample) {
// because of the way glad works we'll have to copy the pointers over
glad_glGenFramebuffers = glad_glGenFramebuffersEXT;
glad_glGenRenderbuffers = glad_glGenRenderbuffersEXT;
glad_glDeleteFramebuffers = glad_glDeleteFramebuffersEXT;
glad_glDeleteRenderbuffers = glad_glDeleteRenderbuffersEXT;
glad_glBindFramebuffer = glad_glBindFramebufferEXT;
glad_glBindRenderbuffer = glad_glBindRenderbufferEXT;
glad_glFramebufferRenderbuffer = glad_glFramebufferRenderbufferEXT;
glad_glFramebufferTexture2D = glad_glFramebufferTexture2DEXT;
glad_glRenderbufferStorage = glad_glRenderbufferStorageEXT;
glad_glRenderbufferStorageMultisample = glad_glRenderbufferStorageMultisampleEXT;
glad_glBlitFramebuffer = glad_glBlitFramebufferEXT;
}
}
}
static void gfx_opengl_init(void) {
if (!gladLoadGLLoader(gl_load_proc) || glGetString == NULL || glEnable == NULL) {
sysFatalError("Could not load OpenGL.\nReported SDL error: %s", SDL_GetError());
}
// check if we're using ES or core, which have more limited feature sets
int val = 0;
SDL_GL_GetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, &val);
gl_core_profile = (val == SDL_GL_CONTEXT_PROFILE_CORE);
gl_es = (val == SDL_GL_CONTEXT_PROFILE_ES);
gfx_opengl_init_extensions();
{ /* D482: always capture GL_RENDERER (log_info below is --debug-gl only). */
const char *renderer = (const char *)glGetString(GL_RENDERER);
snprintf(gl_renderer_str, sizeof(gl_renderer_str), "%s", renderer ? renderer : "");
}
if (sysArgCheck("--debug-gl")) {
gfx_opengl_enable_debug();
// dump version info as early as possible
gfx_opengl_log_info();
}
if (GLVersion.major < 2 || (GLVersion.major == 2 && GLVersion.minor < 1)) {
const char *ver = (const char *)glGetString(GL_VERSION);
sysFatalError("Could not load OpenGL 2.1.\nReported version: %d.%d (%s)",
GLVersion.major, GLVersion.minor, ver ? ver : "unknown");
}
if (!gfx_opengl_supports_shaders()) {
sysLogPrintf(LOG_WARNING, "GL: GLSL 1.30 may be unsupported");
// maybe replace this with sysFatalError, though the GLSL compiler will cause that later anyway
}
if (!gfx_framebuffers_enabled) {
sysLogPrintf(LOG_WARNING, "GL: framebuffer effects disabled by user");
} else if (!gfx_opengl_supports_framebuffers()) {
sysLogPrintf(LOG_WARNING, "GL: GL_ARB_framebuffer_object unsupported, framebuffer effects disabled");
gfx_framebuffers_enabled = false;
}
if ((GLVersion.major < 4 || GLVersion.minor < 4) && !GLAD_GL_ARB_texture_mirror_clamp_to_edge) {
// GL_MIRROR_CLAMP_TO_EDGE unsupported
gl_mirror_clamp = GL_MIRRORED_REPEAT;
}
// determine GLSL version
if (gl_es) {
// ES has its own numbering scheme, but it should support 300 even on 3.1 and 3.2
gl_glsl_version = 300;
snprintf(gl_glsl_version_str, sizeof(gl_glsl_version_str), "%d es", gl_glsl_version);
} else if (!gl_core_profile) {
// in compat profile we can just request the lowest possible
gl_glsl_version = 130;
snprintf(gl_glsl_version_str, sizeof(gl_glsl_version_str), "%d", gl_glsl_version);
} else {
// otherwise we have to pick a specific version
if (GLVersion.major == 3 && GLVersion.minor == 2) {
// 3.2core is the earliest core version and it follows the old numbering scheme
gl_glsl_version = 150;
} else {
// 3.3+ follow the new numbering scheme
gl_glsl_version = GLVersion.major * 100 + GLVersion.minor * 10;
}
snprintf(gl_glsl_version_str, sizeof(gl_glsl_version_str), "%d core", gl_glsl_version);
}
sysLogPrintf(LOG_NOTE, "GL: using GLSL version %s", gl_glsl_version_str);
/* D540: the RDP interpolates shade colour affinely in screen space; the
* GPU default is perspective-correct, which squeezed the sky's horizon
* fade into a thin band. noperspective needs desktop GLSL 1.30+ (not
* GLSL ES). GE_SHADEPERSP=1 restores perspective-correct shade (A/B).
* Fog (D543): per-vertex RSP fog by default, as the N64. Its old
* light/dark cycling at the player's feet came from GPU near-clipping of
* behind-camera corners; gfx_sp_tri1 now near-clips on the CPU like the
* RSP. GE_FOGPIXEL=1 = the D540 exact per-pixel fog (A/B). */
{
const char *sp = getenv("GE_SHADEPERSP"), *fp = getenv("GE_FOGPIXEL");
gfx_rdp_affine = (!gl_es && gl_glsl_version >= 130 && !(sp && sp[0] == '1')) ? 1 : 0;
gfx_fog_vertex = (fp && fp[0] == '1') ? 0 : 1; /* D543: per-vertex RSP fog by default */
}
sysLogPrintf(LOG_NOTE, "GL: shade %s, fog %s", gfx_rdp_affine ? "affine (RDP)" : "perspective",
gfx_fog_vertex ? "per-vertex" : "per-pixel");
glGenBuffers(1, &opengl_vbo);
glBindBuffer(GL_ARRAY_BUFFER, opengl_vbo);
if (gl_core_profile || gl_es) {
// warn user that funny things can happen
sysLogPrintf(LOG_WARNING, "GL: using core profile or ES, watch out for errors");
// core/es will explode if we don't use a VAO for our VBO
glGenVertexArrays(1, &opengl_vao);
glBindVertexArray(opengl_vao);
}
if (GLAD_GL_ARB_depth_clamp) {
glEnable(GL_DEPTH_CLAMP);
}
glDepthFunc(GL_LEQUAL);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
framebuffers.resize(1); // for the default screen buffer
}
static void gfx_opengl_on_resize(void) {
}
/* #92 perf probe (GE_PERFSTAT=1): GPU time per frame via GL_TIME_ELAPSED,
* double-buffered so reading never stalls the pipeline. Skipped when the
* driver lacks timer queries. Tells a low-end reporter's log whether frames
* are GPU-bound (render/MSAA/filtering cost) or CPU-bound. */
static int s_gpuq_on = -1;
static GLuint s_gpuq[2];
static int s_gpuq_idx = 0, s_gpuq_valid[2] = { 0, 0 };
static double s_gpuq_ms = 0.0;
static int s_gpuq_n = 0, s_gpuq_frames = 0;
static void gfx_opengl_start_frame(void) {
frame_count++;
if (s_gpuq_on < 0) {
s_gpuq_on = getenv("GE_PERFSTAT") != NULL && glGenQueries != NULL &&
glGetQueryObjectui64v != NULL && glBeginQuery != NULL;
if (s_gpuq_on) {
glGenQueries(2, s_gpuq);
fprintf(stderr, "PERFSTAT gpu timer on: %s | %s\n",
(const char *)glGetString(GL_RENDERER), (const char *)glGetString(GL_VERSION));
}
}
if (s_gpuq_on == 1) {
if (s_gpuq_valid[s_gpuq_idx]) {
GLuint avail = 0;
glGetQueryObjectuiv(s_gpuq[s_gpuq_idx], GL_QUERY_RESULT_AVAILABLE, &avail);
if (avail) {
GLuint64 ns = 0;
glGetQueryObjectui64v(s_gpuq[s_gpuq_idx], GL_QUERY_RESULT, &ns);
s_gpuq_ms += (double)ns / 1.0e6;
s_gpuq_n++;
}
}
glBeginQuery(GL_TIME_ELAPSED, s_gpuq[s_gpuq_idx]);
}
}
static void gfx_opengl_end_frame(void) {
glFlush();
if (s_gpuq_on == 1) {
glEndQuery(GL_TIME_ELAPSED);
s_gpuq_valid[s_gpuq_idx] = 1;
s_gpuq_idx ^= 1;
if (++s_gpuq_frames % 300 == 0 && s_gpuq_n > 0) {
fprintf(stderr, "PERFSTAT gpu=%.2fms/frame (avg over %d frames)\n", s_gpuq_ms / s_gpuq_n, s_gpuq_n);
s_gpuq_ms = 0.0;
s_gpuq_n = 0;
}
}
}
/* Frame capture (dev tool, env GE_PCDUMP) — see gfx_opengl.h. TEMP D70: read
* the default framebuffer (the window back buffer; the scene renders there
* directly at 1:1 window size) and write a P6 PPM. */
extern "C" bool gfx_opengl_pcdump_enabled(void) {
static int cached = -1;
if (cached < 0) cached = getenv("GE_PCDUMP") != NULL;
return cached != 0;
}
extern "C" bool gfx_opengl_dump_bound_fbo(uint32_t width, uint32_t height, const char* path) {
size_t n = (size_t)width * height * 3;
uint8_t* pixels = (uint8_t*)malloc(n);
if (!pixels) return false;
glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
glReadBuffer(GL_BACK);
glPixelStorei(GL_PACK_ALIGNMENT, 1);
glFinish();
glReadPixels(0, 0, (GLsizei)width, (GLsizei)height, GL_RGB, GL_UNSIGNED_BYTE, pixels);
{
GLenum e = glGetError();
if (e) sysLogPrintf(LOG_WARNING, "dump_bound_fbo: glReadPixels err 0x%x (%ux%u)", (unsigned)e, width, height);
}
FILE* f = fopen(path, "wb");
if (!f) { free(pixels); return false; }
fprintf(f, "P6\n%u %u\n255\n", width, height);
/* D168: glReadPixels() returns rows bottom-to-top (GL framebuffer origin is
* bottom-left); PPM P6 is top-row-first. Emit rows in reverse so the capture
* is the right way up. Previously written unreversed -> every GE_PCDUMP was
* vertically flipped (the source of the bogus D114/D116 "text X-mirror"). */
{
size_t stride = (size_t)width * 3;
for (uint32_t row = 0; row < height; row++)
fwrite(pixels + (size_t)(height - 1 - row) * stride, 1, stride, f);
}
fclose(f);
free(pixels);
return true;
}
static void gfx_opengl_finish_render(void) {
}
static int gfx_opengl_create_framebuffer() {
size_t i = framebuffers.size();
framebuffers.resize(i + 1);
GLuint clrbuf;
glGenTextures(1, &clrbuf);
glBindTexture(GL_TEXTURE_2D, clrbuf);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, 1, 1, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glBindTexture(GL_TEXTURE_2D, 0);
framebuffers[i].clrbuf = clrbuf;
if (!gfx_framebuffers_enabled) {
return i;
}
GLuint clrbuf_msaa;
glGenRenderbuffers(1, &clrbuf_msaa);
framebuffers[i].clrbuf_msaa = clrbuf_msaa;
GLuint rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 1, 1);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
framebuffers[i].rbo = rbo;
GLuint fbo;
glGenFramebuffers(1, &fbo);
framebuffers[i].fbo = fbo;
return i;
}
static void gfx_opengl_update_framebuffer_parameters(int fb_id, uint32_t width, uint32_t height, uint32_t msaa_level,
bool opengl_invert_y, bool render_target, bool has_depth_buffer,
bool can_extract_depth) {
Framebuffer& fb = framebuffers[fb_id];
width = max(width, 1U);
height = max(height, 1U);
/* macOS GL 4.1 core: asking for more samples than GL_MAX_SAMPLES (e.g. 8x)
* makes the multisample renderbuffers incomplete and the screen goes dark
* (glReadPixels 0x506). Clamp to what the driver reports. */
if (msaa_level > 1) {
static GLint maxSamples = -1;
if (maxSamples < 0) {
glGetIntegerv(GL_MAX_SAMPLES, &maxSamples);
if (maxSamples < 1) maxSamples = 1;
}
if ((GLint)msaa_level > maxSamples) {
msaa_level = (uint32_t)maxSamples;
}
}
if (gfx_framebuffers_enabled) {
glBindFramebuffer(GL_FRAMEBUFFER, fb.fbo);
if (fb_id != 0) {
if (fb.width != width || fb.height != height || fb.msaa_level != msaa_level) {
if (msaa_level <= 1) {
glBindTexture(GL_TEXTURE_2D, fb.clrbuf);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glBindTexture(GL_TEXTURE_2D, 0);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, fb.clrbuf, 0);
} else {
glBindRenderbuffer(GL_RENDERBUFFER, fb.clrbuf_msaa);
glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa_level, GL_RGB8, width, height);
glBindRenderbuffer(GL_RENDERBUFFER, 0);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, fb.clrbuf_msaa);
}
}
if (has_depth_buffer &&
(fb.width != width || fb.height != height || fb.msaa_level != msaa_level || !fb.has_depth_buffer)) {
glBindRenderbuffer(GL_RENDERBUFFER, fb.rbo);
if (msaa_level <= 1) {
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
} else {
glRenderbufferStorageMultisample(GL_RENDERBUFFER, msaa_level, GL_DEPTH24_STENCIL8, width, height);
}
glBindRenderbuffer(GL_RENDERBUFFER, 0);
}
if (!fb.has_depth_buffer && has_depth_buffer) {
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, fb.rbo);
} else if (fb.has_depth_buffer && !has_depth_buffer) {
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, 0);
}
}
} else {
has_depth_buffer = false;
}
fb.width = width;
fb.height = height;
fb.has_depth_buffer = has_depth_buffer;
fb.msaa_level = msaa_level;
fb.invert_y = opengl_invert_y;
}
bool gfx_opengl_start_draw_to_framebuffer(int fb_id, float noise_scale) {
if (gfx_framebuffers_enabled && fb_id < (int)framebuffers.size()) {
Framebuffer& fb = framebuffers[fb_id];
if (noise_scale != 0.0f) {
current_noise_scale = 1.0f / noise_scale;
}
/* fb_id 0 is the on-screen target: bind the window's default
* framebuffer (GL name 0), not framebuffers[0].fbo (a generated but
* attachment-less FBO -> incomplete -> nothing renders on a strict
* GL implementation like Mesa/llvmpipe). */
glBindFramebuffer(GL_FRAMEBUFFER, fb_id == 0 ? 0 : fb.fbo);
current_framebuffer = fb_id;
return true;
} else {
return false;
}
}
void gfx_opengl_clear_framebuffer(bool clear_color, bool clear_depth) {
glDisable(GL_SCISSOR_TEST);
GLbitfield mask = 0;
if (clear_color) {
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
mask |= GL_COLOR_BUFFER_BIT;
}
if (clear_depth) {
glDepthMask(GL_TRUE);
mask |= GL_DEPTH_BUFFER_BIT;
}
glClear(mask);
if (clear_depth) {
glDepthMask(current_depth_mask ? GL_TRUE : GL_FALSE);
}
glEnable(GL_SCISSOR_TEST);
}
void gfx_opengl_resolve_msaa_color_buffer(int fb_id_target, int fb_id_source) {
if (!gfx_framebuffers_enabled) {
return;
}
Framebuffer& fb_dst = framebuffers[fb_id_target];
Framebuffer& fb_src = framebuffers[fb_id_source];
glDisable(GL_SCISSOR_TEST);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fb_dst.fbo);
glBindFramebuffer(GL_READ_FRAMEBUFFER, fb_src.fbo);
glBlitFramebuffer(0, 0, fb_src.width, fb_src.height, 0, 0, fb_dst.width, fb_dst.height, GL_COLOR_BUFFER_BIT,
GL_NEAREST);
/* current_framebuffer is an INDEX into framebuffers[], not a GL name; fb 0 is the window's default framebuffer (name 0). */
glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer == 0 ? 0 : framebuffers[current_framebuffer].fbo);
glEnable(GL_SCISSOR_TEST);
}
void* gfx_opengl_get_framebuffer_texture_id(int fb_id) {
return (void*)(uintptr_t)framebuffers[fb_id].clrbuf;
}
void gfx_opengl_select_texture_fb(int fb_id) {
// glDisable(GL_DEPTH_TEST);
glActiveTexture(GL_TEXTURE0 + 0);
glBindTexture(GL_TEXTURE_2D, framebuffers[fb_id].clrbuf);
current_textures_linear_filter[0] = true;
}
void gfx_opengl_copy_framebuffer(int fb_dst, int fb_src, int left, int top, bool flip_y, bool use_back) {
if (!gfx_framebuffers_enabled || fb_dst >= (int)framebuffers.size() || fb_src >= (int)framebuffers.size()) {
return;
}
const Framebuffer& src = framebuffers[fb_src];
const Framebuffer& dst = framebuffers[fb_dst];
int srcX0, srcY0, srcX1, srcY1;
int dstX0, dstY0, dstX1, dstY1;
dstX0 = dstY0 = 0;
dstX1 = dst.width;
dstY1 = dst.height;
if (left >= 0 && top >= 0) {
// unscaled rect copy
srcX0 = left;
srcY0 = top;
srcX1 = left + dst.width;
srcY1 = top + dst.height;
} else {
// scaled full copy
srcX0 = 0;
srcY0 = 0;
srcX1 = src.width;
srcY1 = src.height;
}
glDisable(GL_SCISSOR_TEST);
glBindFramebuffer(GL_READ_FRAMEBUFFER, src.fbo);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, dst.fbo);
if (flip_y) {
// flip the dst rect to mirror the image vertically
std::swap(dstY0, dstY1);
}
if (fb_src == 0) {
// GLES does not support GL_FRONT here
glReadBuffer((use_back || gl_es) ? GL_BACK : GL_FRONT);
} else {
glReadBuffer(GL_COLOR_ATTACHMENT0);
}
glBlitFramebuffer(srcX0, srcY0, srcX1, srcY1, dstX0, dstY0, dstX1, dstY1, GL_COLOR_BUFFER_BIT, GL_NEAREST);
glBindFramebuffer(GL_FRAMEBUFFER, current_framebuffer == 0 ? 0 : framebuffers[current_framebuffer].fbo);
glReadBuffer(GL_BACK);
glEnable(GL_SCISSOR_TEST);
}
void gfx_opengl_set_texture_filter(FilteringMode mode) {
current_filter_mode = mode;
}
FilteringMode gfx_opengl_get_texture_filter(void) {
return current_filter_mode;
}
void gfx_opengl_set_mipmap_filter(MipmapFilteringMode mode) {
current_mipmap_filter_mode = mode;
}
static int gfx_opengl_get_max_anisotropy_level() {
GLfloat max_aniso_level;
glGetFloatv(GL_MAX_TEXTURE_MAX_ANISOTROPY, &max_aniso_level);
return (int)max_aniso_level;
}
static void gfx_opengl_set_anisotropy_level(int level) {
current_anisotropy_level = level;
}
struct GfxRenderingAPI gfx_opengl_api = {
gfx_opengl_get_name,
gfx_opengl_get_max_texture_size,
gfx_opengl_get_clip_parameters,
gfx_opengl_unload_shader,
gfx_opengl_load_shader,
gfx_opengl_create_and_load_new_shader,
gfx_opengl_lookup_shader,
gfx_opengl_shader_get_info,
gfx_opengl_clear_shaders,
gfx_opengl_new_texture,
gfx_opengl_select_texture,
gfx_opengl_upload_texture,
gfx_opengl_set_sampler_parameters,
gfx_opengl_set_depth_mode,
gfx_opengl_set_depth_range,
gfx_opengl_set_viewport,
gfx_opengl_set_scissor,
gfx_opengl_set_use_alpha,
gfx_opengl_draw_triangles,
gfx_opengl_init,
gfx_opengl_on_resize,
gfx_opengl_start_frame,
gfx_opengl_end_frame,
gfx_opengl_finish_render,
gfx_opengl_create_framebuffer,
gfx_opengl_update_framebuffer_parameters,
gfx_opengl_start_draw_to_framebuffer,
gfx_opengl_copy_framebuffer,
gfx_opengl_clear_framebuffer,
gfx_opengl_resolve_msaa_color_buffer,
gfx_opengl_get_framebuffer_texture_id,
gfx_opengl_select_texture_fb,
gfx_opengl_delete_texture,
gfx_opengl_set_texture_filter,
gfx_opengl_get_texture_filter,
gfx_opengl_set_mipmap_filter,
gfx_opengl_set_anisotropy_level,
gfx_opengl_get_max_anisotropy_level
};