mirror of
https://github.com/RetroPortingToolKit/psxrecomp
synced 2026-10-26 02:11:36 -04:00
Test: render thread on vs off on a real OpenGL context
gl_render_thread_test runs a scripted stream through the facade twice per scale (1x, 4x): every value the guest reads back, native VRAM, the frame at internal resolution and the native-wide surface must match with the render thread off and on. The stream has mask set/check, a mask-checked upload (per-pixel readback sync point), back-to-back uploads of one rect with a catch-up between gpu.c's writes and its commit, ring wraps, readback frames, presents, and widescreen tags that change per primitive and are poisoned after each call. A slow render-thread present keeps the replay a frame behind. It fails for each of: reading tags live, sharing the upload source with the guest array, dropping the stream state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -211,6 +211,19 @@ if(BUILD_TESTING)
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-scale-results")
|
||||
set_tests_properties(gl_scale_invariance_test PROPERTIES
|
||||
LABELS "gpu;opengl;hardware" TIMEOUT 300 SKIP_RETURN_CODE 77)
|
||||
# Render thread ([video] render_thread) on vs off: the values the
|
||||
# guest reads back, native VRAM, the frame at S and the native-wide
|
||||
# surface are identical for a scripted stream with readbacks, mask
|
||||
# bits, uploads, ring wraps and per-primitive widescreen tags.
|
||||
add_test(NAME gl_render_thread_test
|
||||
COMMAND "${Python3_EXECUTABLE}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/tests/run_gl_render_thread.py"
|
||||
--fixture "${CMAKE_CURRENT_SOURCE_DIR}/tests/test_gl_render_thread.c"
|
||||
--cc "${CMAKE_C_COMPILER}"
|
||||
--sdl-include "${_gl_scale_sdl_inc}"
|
||||
--sdl-library "$<TARGET_FILE:SDL3::SDL3>"
|
||||
--output "${CMAKE_CURRENT_BINARY_DIR}/gl-render-thread-results")
|
||||
set_tests_properties(gl_render_thread_test PROPERTIES LABELS "gpu;opengl;hardware" TIMEOUT 300)
|
||||
unset(_gl_scale_sdl_inc)
|
||||
else()
|
||||
message(STATUS "gl_scale_invariance_test: not registered (needs macOS, Linux or MinGW, Python 3 and SDL3)")
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
"""Build and run test_gl_render_thread.c on a hidden real OpenGL context.
|
||||
|
||||
Each scale runs twice, render thread off and on ([video] render_thread). The
|
||||
values the scripted guest read back (rb), the native VRAM (digest), the frame
|
||||
at internal resolution (hires) and the native-wide surface (wide) must be the
|
||||
same both ways, and the threaded run must actually have recorded and replayed
|
||||
the stream (its own checks). A third run per scale with one frame in flight
|
||||
(PSX_TEST_RT_FRAMES is not needed: the fixture takes the bound as argv) is not
|
||||
required; the in-flight bound itself is covered by render_thread_test.
|
||||
|
||||
macOS/Linux: pass the SDL3 include directory and static library and a C
|
||||
compiler, as for run_gl_scale_invariance.py. Evidence (commands, output) is
|
||||
written to receipt.json under --output.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import pathlib
|
||||
import platform
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
MAC_FRAMEWORKS = ["Cocoa", "OpenGL", "IOKit", "CoreVideo", "CoreAudio", "AudioToolbox",
|
||||
"Carbon", "ForceFeedback", "GameController", "Metal", "QuartzCore",
|
||||
"CoreMedia", "AVFoundation", "Foundation", "CoreHaptics",
|
||||
"UniformTypeIdentifiers"]
|
||||
KEYS = ("rb", "digest", "hires", "wide")
|
||||
|
||||
|
||||
def parse(stdout):
|
||||
out = {}
|
||||
for k in KEYS:
|
||||
m = re.search(r"^" + k + r"=([0-9a-f]{16})$", stdout, re.M)
|
||||
out[k] = m[1] if m else None
|
||||
m = re.search(r"^checks=(\d+) failures=(\d+)$", stdout, re.M)
|
||||
out["failures"] = int(m[2]) if m else None
|
||||
return out
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser(description=__doc__)
|
||||
ap.add_argument("--cc", default="cc")
|
||||
ap.add_argument("--sdl-include", required=True)
|
||||
ap.add_argument("--sdl-library", required=True)
|
||||
ap.add_argument("--output", required=True)
|
||||
ap.add_argument("--fixture", type=pathlib.Path)
|
||||
ap.add_argument("--scales", default="1,4")
|
||||
ap.add_argument("--frames", default="80")
|
||||
args = ap.parse_args()
|
||||
sdl_includes = [str(pathlib.Path(d).resolve()) for d in args.sdl_include.split(";") if d]
|
||||
args.sdl_library = str(pathlib.Path(args.sdl_library).resolve())
|
||||
framework = pathlib.Path(__file__).resolve().parents[2]
|
||||
fixture = args.fixture or framework / "runtime/tests/test_gl_render_thread.c"
|
||||
out_root = pathlib.Path(args.output).resolve()
|
||||
out_root.mkdir(parents=True, exist_ok=True)
|
||||
dest = pathlib.Path(tempfile.mkdtemp(prefix="rth-", dir=out_root))
|
||||
print("Evidence directory:", dest)
|
||||
receipt = []
|
||||
|
||||
def run(command):
|
||||
command = [str(c) for c in command]
|
||||
r = subprocess.run(command, cwd=dest, capture_output=True, text=True,
|
||||
encoding="utf-8", errors="replace")
|
||||
receipt.append({"cmd": command, "exit": r.returncode,
|
||||
"stdout": r.stdout, "stderr": r.stderr[-4000:]})
|
||||
(dest / "receipt.json").write_text(json.dumps(receipt, indent=2), encoding="utf-8")
|
||||
return r
|
||||
|
||||
includes = ["-I", framework / "runtime/include", "-I", framework / "runtime/src"]
|
||||
for d in sdl_includes:
|
||||
includes += ["-I", d]
|
||||
src = framework / "runtime/src"
|
||||
# The fixture #includes gpu_gl_renderer.c and drives it through the real
|
||||
# facade (gpu_render.c) and render thread (render_thread.c).
|
||||
sources = [("probe", fixture), ("sw", src / "gpu_sw_renderer.c"),
|
||||
("fi", src / "frame_interpolation.c"), ("rp", src / "render_pass_plan.c"),
|
||||
("xr", src / "psx_openxr.c"), ("rth", src / "render_thread.c"),
|
||||
("facade", src / "gpu_render.c")]
|
||||
sections = [] if platform.system() == "Darwin" else ["-ffunction-sections", "-fdata-sections"]
|
||||
objs = []
|
||||
for name, path in sources:
|
||||
o = dest / (name + ".o")
|
||||
r = run([args.cc, "-std=gnu11", "-O1", "-DPSX_SDL3=1", "-DPSX_NO_DEBUG_TOOLS=1",
|
||||
"-DGL_SILENCE_DEPRECATION=1", "-w", *sections, *includes, "-c", path, "-o", o])
|
||||
if r.returncode:
|
||||
print(r.stderr[-3000:])
|
||||
return 2
|
||||
objs.append(o)
|
||||
link = [args.cc, *objs, args.sdl_library, "-o", dest / "probe"]
|
||||
if platform.system() == "Darwin":
|
||||
for f in MAC_FRAMEWORKS:
|
||||
link += ["-framework", f]
|
||||
link += ["-liconv", "-lm", "-lpthread", "-Wl,-dead_strip"]
|
||||
else:
|
||||
link += ["-lGL", "-lm", "-ldl", "-lpthread", "-Wl,--gc-sections"]
|
||||
r = run(link)
|
||||
if r.returncode:
|
||||
print(r.stderr[-3000:])
|
||||
return 2
|
||||
|
||||
ok = True
|
||||
for s in [int(v) for v in args.scales.split(",") if v]:
|
||||
runs = {}
|
||||
for threaded in (0, 1):
|
||||
r = run([dest / "probe", s, threaded, args.frames])
|
||||
p = parse(r.stdout)
|
||||
tail = r.stdout.strip().splitlines()[-6:]
|
||||
print(f"scale {s} render_thread={threaded}: exit={r.returncode}", tail,
|
||||
r.stderr.strip()[-800:])
|
||||
if r.returncode or p["failures"] != 0 or any(p[k] is None for k in KEYS):
|
||||
ok = False
|
||||
runs[threaded] = p
|
||||
for k in KEYS:
|
||||
if runs[0][k] != runs[1][k]:
|
||||
print(f"FAIL scale {s}: {k} differs with the render thread on "
|
||||
f"({runs[0][k]} off, {runs[1][k]} on)")
|
||||
ok = False
|
||||
print("PASS" if ok else "FAIL")
|
||||
return 0 if ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
@@ -0,0 +1,281 @@
|
||||
/* Render thread equivalence on a real, hidden OpenGL context.
|
||||
*
|
||||
* Run twice per scale by run_gl_render_thread.py, once with the render thread
|
||||
* off (argv[2] == 0) and once on (1); the runner requires every digest to
|
||||
* match. The scripted stream goes through the renderer facade (gr_*), exactly
|
||||
* as gpu.c drives it, over many frames:
|
||||
* - CPU->VRAM uploads written the way gpu.c writes them (the guest-visible
|
||||
* array first, then the facade with a copy), including the same rect
|
||||
* re-uploaded with new data before the render thread can have drawn the
|
||||
* first (the render thread must stage each payload, not the live array);
|
||||
* full-VRAM uploads push the ring past its end (wrap);
|
||||
* - GP0(E6) mask set / check around draws, and a mask-checked CPU upload
|
||||
* whose per-pixel mask test reads VRAM back (a sync point mid-frame);
|
||||
* - fills, VRAM copies, render-to-texture, every semi-transparency mode,
|
||||
* lines, textured and shaded primitives, draw area / offset changes;
|
||||
* - native-wide: margin draws, wide clears, and per-primitive widescreen
|
||||
* tags that change with every primitive (captured at record time) plus a
|
||||
* stream latch that flips mid-frame (sent as state);
|
||||
* - presents and frame boundaries every frame, with readbacks (GPUREAD
|
||||
* style gr_vram_read, VRAM->CPU transfer_out) in some frames only.
|
||||
* Printed: rb (digest of every value the "guest" read back), digest (native
|
||||
* VRAM at the end), hires (frame at internal resolution), wide (native-wide
|
||||
* surface), and with the thread on its record/acquire counters.
|
||||
* Original source-owned scene; no retail payload. */
|
||||
#include "gpu_gl_renderer.c"
|
||||
#include "mod_texture_banks.c"
|
||||
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;return 0;}
|
||||
uint32_t psx_mod_read_word(uint32_t a){(void)a;return 0;}
|
||||
int g_psx_vram_dirty_tracking=0;
|
||||
uint64_t s_frame_count=0;
|
||||
void gpu_vram_dirty_mark_row_impl(uint32_t y){(void)y;}
|
||||
void gpu_vram_dirty_mark_rect(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
|
||||
void gpu_vram_dirty_mark_all(void){}
|
||||
int psx_netplay_active(void){return 0;}
|
||||
const GpuRenderBackend *vk_backend_get(void){return NULL;}
|
||||
int gpu_display_is_depth24(void){return 0;}
|
||||
void gpu_get_display_info(GpuDisplayInfo *out){memset(out,0,sizeof(*out));out->width=320;out->height=240;}
|
||||
/* Guest state the backend reads per primitive. It changes with every
|
||||
* primitive here, so a replay that read it live (instead of the value
|
||||
* captured when the primitive was recorded) would draw a different image. */
|
||||
static int g_prim_id;
|
||||
static int g_bg_full;
|
||||
int psx_ws_prim_in_backdrop(void){return (g_prim_id % 3) == 0;}
|
||||
int psx_ws_prim_is_tagged(void){return (g_prim_id % 5) == 0;}
|
||||
int gpu_ws_nw_flat_backdrop_enabled(void){return 0;}
|
||||
int gpu_ws_background_requires_full_composite(void){return g_bg_full;}
|
||||
int g_ws_tex_edge_pct=0;
|
||||
void psx_ws_dbg_gate_frame_snapshot(void){}
|
||||
void gpu_depth24_upload_span_reset(void){}
|
||||
int gpu_ws_netplay_local_viewport_width(void){return 0;}
|
||||
int render_pass_netplay_enabled(void){return 0;}
|
||||
uint64_t psx_cycle_count=0;
|
||||
uint32_t g_psx_vblank_cycles=564480u;
|
||||
/* Frontend hooks of the present path (host overlays, pacing, rings). */
|
||||
int host_osd_image(const uint32_t **p,int *w,int *h){*p=NULL;*w=*h=0;return 0;}
|
||||
int host_osd_volume_image(const uint32_t **p,int *w,int *h){*p=NULL;*w=*h=0;return 0;}
|
||||
int host_osd_needs_present(void){return 0;}
|
||||
void host_osd_present_done(void){}
|
||||
int psx_rewind_overlay_image(const uint32_t **p,int *w,int *h){*p=NULL;*w=*h=0;return 0;}
|
||||
float psx_rewind_slide(void){return 0.0f;}
|
||||
int psx_savestate_menu_overlay_image(const uint32_t **p,int *w,int *h){*p=NULL;*w=*h=0;return 0;}
|
||||
int present_shot_take(char *out,int n){(void)out;(void)n;return 0;}
|
||||
void present_shot_done(int ok){(void)ok;}
|
||||
void psx_host_sleep_ms(uint32_t ms){(void)ms;}
|
||||
/* Called on every present. On the render thread it also stands in for a slow
|
||||
* swap, so replay runs well behind recording and anything it read live from
|
||||
* the "guest" (instead of what was recorded) would be a later value. */
|
||||
int psx_present_vsync_owns_cadence(void){
|
||||
if (rt_on_render_thread()) SDL_Delay(4);
|
||||
return 0;
|
||||
}
|
||||
void latency_ring_mark(LatencyStage s){(void)s;}
|
||||
void gpu_timeline_note(uint8_t k,uint32_t a,uint32_t b){(void)k;(void)a;(void)b;}
|
||||
|
||||
static uint16_t vram[1024*512];
|
||||
static int checks, failures;
|
||||
static void check(int ok, const char *label) {
|
||||
checks++;
|
||||
if (!ok) { fprintf(stderr, "FAIL %s\n", label); failures++; }
|
||||
}
|
||||
static uint64_t fnv(const void *pp, size_t n, uint64_t h) {
|
||||
const uint8_t *p = (const uint8_t *)pp;
|
||||
for (size_t i = 0; i < n; i++) { h ^= p[i]; h *= 0x100000001b3ull; }
|
||||
return h;
|
||||
}
|
||||
static uint64_t rb_digest = 0xcbf29ce484222325ull;
|
||||
static uint32_t rng = 0x2468ace1u;
|
||||
static uint32_t rnd(void) { rng ^= rng << 13; rng ^= rng >> 17; rng ^= rng << 5; return rng; }
|
||||
|
||||
/* gpu.c's CPU->VRAM path: write the guest-visible array (honouring the mask
|
||||
* test, which reads VRAM through the facade), then hand the facade a copy. */
|
||||
static uint16_t staging[1024*512];
|
||||
static int s_set_mask, s_check_mask;
|
||||
static void cpu_upload_ex(int x, int y, int w, int h, const uint16_t *src, int catch_up) {
|
||||
for (int r = 0; r < h; r++)
|
||||
for (int c = 0; c < w; c++) {
|
||||
int vx = (x + c) & 1023, vy = (y + r) & 511;
|
||||
uint16_t px = src[r * w + c];
|
||||
if (s_check_mask && (gr_vram_read(vx, vy) & 0x8000)) continue;
|
||||
if (s_set_mask) px |= 0x8000;
|
||||
vram[vy * 1024 + vx] = px;
|
||||
}
|
||||
/* Worst case for a render thread that staged uploads from this array:
|
||||
* it replays an earlier upload of the same rect right here, between
|
||||
* gpu.c's pixel writes and its commit (which re-reads the array). */
|
||||
if (catch_up && gl_renderer_render_thread_active()) rt_drain();
|
||||
for (int r = 0; r < h; r++)
|
||||
for (int c = 0; c < w; c++)
|
||||
staging[r * w + c] = vram[((y + r) & 511) * 1024 + ((x + c) & 1023)];
|
||||
gr_vram_transfer_in(x, y, w, h, staging);
|
||||
}
|
||||
static void cpu_upload(int x, int y, int w, int h, const uint16_t *src) {
|
||||
cpu_upload_ex(x, y, w, h, src, 0);
|
||||
}
|
||||
static void mask_bits(int set, int chk) {
|
||||
s_set_mask = set; s_check_mask = chk;
|
||||
gr_set_mask_bits(set, chk);
|
||||
}
|
||||
|
||||
#define FRAME_W 320
|
||||
#define FRAME_H 240
|
||||
|
||||
static void textures(void) {
|
||||
static uint16_t page[64*64], clut[16];
|
||||
for (int i = 0; i < 64*64; i++) page[i] = (uint16_t)((i * 0x1357u) ^ (i >> 3));
|
||||
for (int i = 0; i < 16; i++) clut[i] = (uint16_t)(i ? (0x0421u * (uint16_t)i) | ((i & 1) << 15) : 0);
|
||||
cpu_upload(512, 0, 64, 64, page);
|
||||
cpu_upload(512, 256, 16, 1, clut);
|
||||
}
|
||||
|
||||
static void frame(int f, int buf_x) {
|
||||
gr_set_draw_area(buf_x, 0, buf_x + FRAME_W - 1, FRAME_H - 1);
|
||||
gr_set_draw_offset(buf_x, 0);
|
||||
gr_wide_set_target(buf_x);
|
||||
gr_fill_rect(buf_x, 0, FRAME_W, FRAME_H, (uint16_t)(0x0842 * (f & 7)));
|
||||
gr_wide_clear(buf_x, 0, FRAME_H, (uint16_t)(0x0842 * (f & 7)));
|
||||
gr_set_color_modulation(128, 128, 128, 0);
|
||||
mask_bits(0, 0);
|
||||
/* Many primitives with tags that change per primitive. */
|
||||
for (int i = 0; i < 600; i++) {
|
||||
g_prim_id = f * 1000 + i;
|
||||
if (i == 300) g_bg_full = (f & 1); /* stream latch flips mid-frame */
|
||||
int x = buf_x + (int)(rnd() % 420) - 50, y = (int)(rnd() % 240);
|
||||
int w = 4 + (int)(rnd() % 40), h = 4 + (int)(rnd() % 30);
|
||||
uint16_t c = (uint16_t)(rnd() & 0x7fff);
|
||||
gr_set_semi_transparency((int)(rnd() % 4 == 0), (int)(rnd() % 4));
|
||||
switch (i % 7) {
|
||||
case 0: gr_draw_flat_rect(x, y, w, h, c); break;
|
||||
case 1: gr_draw_gouraud_triangle(x, y, c, x + w, y + 3, (uint16_t)(c ^ 0x1f), x + 2, y + h, (uint16_t)(c ^ 0x3e0)); break;
|
||||
case 2: gr_draw_textured_rect(x, y, w, h, (int)(rnd() % 32), (int)(rnd() % 32), 512, 256, 0x0008); break;
|
||||
case 3: gr_draw_shaded_textured_triangle(x, y, 0, 0, 0x808080, x + w, y, 63, 4, 0x6090b0,
|
||||
x, y + h, 8, 63, 0xb09060, 512, 256, 0x0008, (int)(i & 1)); break;
|
||||
case 4: gr_draw_line(x, y, x + w * 2, y + h, c); break;
|
||||
case 5: gr_draw_shaded_line(x, y, c, x + 3, y + h, (uint16_t)~c); break;
|
||||
default: gr_draw_flat_triangle(x, y, x + w, y + h, x - 5, y + h / 2, c); break;
|
||||
}
|
||||
g_prim_id = -1; /* the next GP0 command is in flight: no tags */
|
||||
}
|
||||
gr_set_semi_transparency(0, 0);
|
||||
/* Mask set, then mask check over it. */
|
||||
mask_bits(1, 0);
|
||||
gr_draw_flat_rect(buf_x + 40, 40, 30, 30, 0x4210);
|
||||
mask_bits(0, 1);
|
||||
gr_draw_flat_rect(buf_x + 30, 30, 60, 60, 0x7fff);
|
||||
/* A mask-checked CPU upload over the masked rect: the per-pixel test
|
||||
* reads VRAM back (sync point), in some frames only. */
|
||||
if (f % 4 == 1) {
|
||||
static uint16_t patch[24 * 24];
|
||||
for (int i = 0; i < 24 * 24; i++) patch[i] = (uint16_t)(0x1234 + i * 7 + f);
|
||||
cpu_upload(buf_x + 35, 35, 24, 24, patch);
|
||||
}
|
||||
mask_bits(0, 0);
|
||||
/* The same rect uploaded twice with different data back to back. */
|
||||
{
|
||||
static uint16_t a[32 * 16], b[32 * 16];
|
||||
for (int i = 0; i < 32 * 16; i++) { a[i] = (uint16_t)(0x0300 + i + f); b[i] = (uint16_t)(0x6000 ^ (i * 3 + f)); }
|
||||
cpu_upload(600, 100, 32, 16, a);
|
||||
gr_draw_textured_rect(buf_x + 200, 180, 32, 16, 88, 100, 512, 256, 0x0009); /* samples a */
|
||||
cpu_upload_ex(600, 100, 32, 16, b, 1);
|
||||
gr_draw_textured_rect(buf_x + 240, 180, 32, 16, 88, 100, 512, 256, 0x0009); /* samples b */
|
||||
/* The guest keeps writing its array (as gpu.c does for the next
|
||||
* GP0(A0) before that upload completes); nothing may read this. */
|
||||
for (int i = 0; i < 32 * 16; i++) vram[(100 + i / 32) * 1024 + 600 + i % 32] = 0x7c1f;
|
||||
}
|
||||
/* Render to texture, copies. */
|
||||
gr_draw_flat_rect(576, 64, 32, 32, (uint16_t)(0x03e0 + f));
|
||||
gr_draw_textured_rect(buf_x + 100, 200, 32, 32, 0, 64, 512, 256, 0x0009);
|
||||
gr_copy_rect(buf_x + 10, 10, buf_x + 14, 13, 80, 50);
|
||||
/* A half-VRAM upload every frame pushes the ring past its end. */
|
||||
{
|
||||
static uint16_t big[1024 * 256];
|
||||
for (int i = 0; i < 1024 * 256; i++) big[i] = (uint16_t)(rnd() & 0x7fff);
|
||||
cpu_upload(0, 256, 1024, 256, big);
|
||||
}
|
||||
/* Readbacks in some frames (GPUREAD-style and VRAM->CPU). */
|
||||
if (f % 5 == 3) {
|
||||
for (int i = 0; i < 64; i++) {
|
||||
uint16_t v = gr_vram_read(buf_x + (int)(rnd() % FRAME_W), (int)(rnd() % FRAME_H));
|
||||
rb_digest = fnv(&v, 2, rb_digest);
|
||||
}
|
||||
static uint16_t out[64 * 32];
|
||||
gr_vram_transfer_out(buf_x + 50, 50, 64, 32, out);
|
||||
rb_digest = fnv(out, sizeof out, rb_digest);
|
||||
}
|
||||
gl_renderer_present_vram(buf_x, 0, FRAME_W, FRAME_H, 0, 0);
|
||||
gl_renderer_render_thread_frame_boundary();
|
||||
}
|
||||
|
||||
int main(int argc, char **argv) {
|
||||
int scale = argc > 1 ? atoi(argv[1]) : 1;
|
||||
int threaded = argc > 2 && argv[2][0] == '1';
|
||||
int frames = argc > 3 ? atoi(argv[3]) : 80;
|
||||
if (SDL_Init(SDL_INIT_VIDEO) != 0) return 2;
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
#if defined(__APPLE__)
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, SDL_GL_CONTEXT_FORWARD_COMPATIBLE_FLAG);
|
||||
#endif
|
||||
SDL_Window *win = SDL_CreateWindow("Render thread hidden test", 0, 0, 320, 240,
|
||||
SDL_WINDOW_OPENGL | SDL_WINDOW_HIDDEN);
|
||||
if (!win) return 2;
|
||||
gr_set_backend(GR_BACKEND_OPENGL);
|
||||
gr_init(vram);
|
||||
gr_set_scale(scale);
|
||||
gl_renderer_set_swap_interval(0);
|
||||
if (!gl_renderer_init_context(win)) { fprintf(stderr, "FAIL context\n"); return 2; }
|
||||
check(gr_backend() == GR_BACKEND_OPENGL, "OpenGL backend");
|
||||
textures();
|
||||
gr_wide_configure(426, 53);
|
||||
if (threaded) {
|
||||
check(gl_renderer_render_thread_start(2) == 1, "render thread started");
|
||||
check(gl_renderer_render_thread_active(), "render thread active");
|
||||
}
|
||||
for (int f = 0; f < frames; f++) frame(f, (f & 1) ? 512 : 0);
|
||||
|
||||
/* Guest-visible VRAM, the frame at internal resolution, the wide surface. */
|
||||
static uint16_t native[1024 * 512];
|
||||
gr_vram_transfer_out(0, 0, 1024, 512, native);
|
||||
uint64_t digest = fnv(native, sizeof native, 0xcbf29ce484222325ull);
|
||||
check(memcmp(native, vram, sizeof native) == 0, "readback landed in the guest-visible array");
|
||||
int fw = FRAME_W * scale, fh = FRAME_H * scale, ow = 0, oh = 0;
|
||||
uint32_t *img = (uint32_t *)malloc((size_t)fw * fh * 4);
|
||||
int n = img ? gl_renderer_read_display_hires(512, 0, FRAME_W, FRAME_H, img, fw * fh, &ow, &oh) : 0;
|
||||
check(n == fw * fh, "hires readback");
|
||||
uint64_t hires = n ? fnv(img, (size_t)fw * fh * 4, 0xcbf29ce484222325ull) : 0;
|
||||
free(img);
|
||||
uint64_t wide = 0;
|
||||
{
|
||||
int ww = 426 * scale, wh = 512 * scale, gw = 0, gh = 0;
|
||||
uint32_t *wb = (uint32_t *)malloc((size_t)ww * wh * 4);
|
||||
int got = wb ? gr_wide_dump_full(wb, ww * wh, &gw, &gh, 512) : 0;
|
||||
check(got == ww * wh, "wide dump");
|
||||
if (got) wide = fnv(wb, (size_t)ww * wh * 4, 0xcbf29ce484222325ull);
|
||||
free(wb);
|
||||
}
|
||||
if (threaded) {
|
||||
RtStats st;
|
||||
rt_get_stats(&st);
|
||||
printf("rt_records=%llu rt_acquires=%llu rt_frames=%llu rt_high_water=%llu presents=%llu stale=%llu\n",
|
||||
(unsigned long long)st.records, (unsigned long long)st.acquires,
|
||||
(unsigned long long)st.frames_produced, (unsigned long long)st.ring_high_water,
|
||||
(unsigned long long)s_rth_presents, (unsigned long long)s_rth_presents_stale);
|
||||
check(st.records > (uint64_t)frames * 600, "draws were recorded");
|
||||
/* Sync points: the mask-checked uploads, the readback frames and the
|
||||
* final readbacks; never more than one per frame plus the tail. */
|
||||
check(st.acquires >= 2 && st.acquires <= (uint64_t)frames + 4, "acquires bounded");
|
||||
check(st.bytes > ((uint64_t)32 << 20), "ring wrapped (more than its size recorded)");
|
||||
}
|
||||
check(glGetError() == GL_NO_ERROR, "GL error");
|
||||
printf("rb=%016llx\n", (unsigned long long)rb_digest);
|
||||
printf("digest=%016llx\n", (unsigned long long)digest);
|
||||
printf("hires=%016llx\n", (unsigned long long)hires);
|
||||
printf("wide=%016llx\n", (unsigned long long)wide);
|
||||
printf("checks=%d failures=%d\n", checks, failures);
|
||||
gl_renderer_shutdown();
|
||||
check(!gl_renderer_render_thread_active(), "stopped by shutdown");
|
||||
SDL_DestroyWindow(win);
|
||||
SDL_Quit();
|
||||
return failures ? 1 : 0;
|
||||
}
|
||||
Reference in New Issue
Block a user