Files
psxrecomp/runtime/tests/test_gl_render_thread.c
T

439 lines
22 KiB
C

/* 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.
* With argv[4] == "dynres" ([video] dynamic_resolution) the surfaces are
* allocated at the scale and the level steps between frames
* (gl_renderer_step_internal_scale_now after a frame boundary, as the
* controller does): recorded in stream order with the thread on, run at once
* with it off, or at once when the frame is held at a sync point; the
* render thread's frame cost is measured too. Back at the ceiling before the
* final readbacks.
* 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 "gpu_hd_texture_stubs.inc"
#include "mod_texture_banks.c"
#include "gl_batch_policy_lle.c"
#include "gl_batch_policy_hle.c"
#include "gl_batch_policy_select.c"
/* gpu_render.c (the facade) observes draws for HD texture packs: no pack here. */
int gpu_hd_textures_active(void){return 0;}
int gpu_hd_textures_dump_enabled(void){return 0;}
void gpu_hd_textures_observe_draw(uint16_t tp,uint16_t cx,uint16_t cy,const int l[4],
uint32_t w,int semi){(void)tp;(void)cx;(void)cy;(void)l;(void)w;(void)semi;}
void gpu_hd_textures_set_vram(const uint16_t* v){(void)v;}
void gpu_hd_textures_track_upload(int x,int y,int w,int h,const uint16_t* words){
(void)x;(void)y;(void)w;(void)h;(void)words;}
void gpu_hd_textures_invalidate(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
void gpu_hd_textures_begin_upload(int x,int y,int w,int h){(void)x;(void)y;(void)w;(void)h;}
void gpu_hd_textures_begin_copy(int sx,int sy,int dx,int dy,int w,int h){
(void)sx;(void)sy;(void)dx;(void)dy;(void)w;(void)h;}
void gpu_hd_textures_end_copy(void){}
uint32_t psx_mod_gpu_dma_memory_alloc(uint32_t n,uint32_t a){(void)n;(void)a;return 0;}
/* mod_texture_banks.c refuses inside a netplay host-output hook (mod_runtime.cpp); none here. */
int psx_mod_host_output_scope(void){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;}
/* No frontend menu or telemetry consumer in this renderer-only fixture. */
int psx_host_menu_overlay_image(const uint32_t **p,int *w,int *h){*p=NULL;*w=*h=0;return 0;}
void psx_video_telemetry_compose(int r,int s,int w,int h,int dw,int dh,int vw,int vh){
(void)r;(void)s;(void)w;(void)h;(void)dw;(void)dh;(void)vw;(void)vh;}
void psx_video_telemetry_present(int result){(void)result;}
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;}
void psx_host_sleep_micros(unsigned us){(void)us;}
/* 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 int drawable_worker(void *data) {
int *size = data;
gl_drawable_size(&size[0], &size[1]);
return 0;
}
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 int timing_idle;
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);
if (timing_idle && rt_running()) {
rt_drain();
SDL_Delay(30); /* producer pacing after the rendered image is submitted */
}
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;
int dynres = argc > 4 && !strcmp(argv[4], "dynres");
timing_idle = argc > 4 && !strcmp(argv[4], "idle");
/* No video device, window or GL context (headless, Windows over SSH):
* exit 77, which the harness reports as a CTest skip. */
if (SDL_Init(SDL_INIT_VIDEO) != 0) {
fprintf(stderr, "SKIP no video (%s)\n", SDL_GetError());
return 77;
}
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) { fprintf(stderr, "SKIP no window (%s)\n", SDL_GetError()); return 77; }
{ /* Probe with the renderer's attributes: only a probe tells "no
* context on this host" from "the renderer's init broke". */
SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
SDL_GLContext probe = SDL_GL_CreateContext(win);
if (!probe) {
fprintf(stderr, "SKIP no GL 3.3 core context (%s)\n", SDL_GetError());
return 77;
}
SDL_GL_MakeCurrent(win, NULL);
SDL_GL_DeleteContext(probe);
}
gr_set_backend(GR_BACKEND_OPENGL);
gr_init(vram);
gr_set_scale(scale);
if (dynres) gl_renderer_set_dynamic_resolution(1);
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");
/* Compare restoration with real GL state after every colour-pass branch,
* including mask-check, dual-source blending and bloom marking. */
for (int bloom = 0; bloom <= 1; ++bloom)
for (int tex = 0; tex <= 1; ++tex)
for (int semi = -1; semi <= (tex ? 4 : 3); ++semi)
for (int mask = 0; mask <= 1; ++mask)
for (int check_mask = 0; check_mask <= 1; ++check_mask) {
s_bcls_on = bloom;
p_glUseProgram(tex ? s_tex_prog : s_geo_prog);
p_glBindVertexArray(tex ? s_tex_vao : s_geo_vao);
depth_apply_ex(1, tex);
if (tex) tex_draw_passes_ex(0, semi, mask, check_mask, 0, -1);
else {
if (semi >= 0) apply_psx_blend(semi); else glDisable(GL_BLEND);
mask_stencil_ex(mask, check_mask);
BCLS_DRAW(semi, 1, mask, check_mask, -1, glDrawArrays(GL_TRIANGLES, 0, 0));
}
GLboolean stencil = glIsEnabled(GL_STENCIL_TEST), blend = glIsEnabled(GL_BLEND);
depth_restore_ex(1, tex, 0, semi, mask, check_mask);
check(glIsEnabled(GL_STENCIL_TEST) == stencil, "depth pass restores stencil");
check(glIsEnabled(GL_BLEND) == blend, "depth pass restores blend");
}
s_bcls_on = 0;
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");
}
if (dynres) {
check(gl_renderer_dynamic_resolution_ceiling() == scale, "dynres ceiling");
if (threaded) gl_renderer_render_thread_measure(1);
}
if (timing_idle && threaded) gl_renderer_render_thread_measure(1);
static const int levels[] = { 2, 1, 3, 1, 4, 2 };
int nsteps = 0, level = scale;
const uint64_t drawable_queries_before = s_drawable_queries;
for (int f = 0; f < frames; f++) {
frame(f, (f & 1) ? 512 : 0);
if (dynres && f % 7 == 6) { /* includes frames held at a sync point */
int l = levels[(f / 7) % 6];
if (l > scale) l = scale;
if (l != level) nsteps++;
level = l;
check(gl_renderer_step_internal_scale_now(l) == 1, "dynres step");
GlDynresStats ds;
gl_renderer_dynres_stats(&ds);
check(ds.level == l, "dynres level as asked");
check(gr_scale() == l, "gr_scale follows the level");
}
}
if (dynres) {
if (level != scale) nsteps++;
check(gl_renderer_step_internal_scale_now(scale) == 1, "dynres back to the ceiling");
gl_renderer_render_thread_sync("test");
GlDynresStats ds;
gl_renderer_dynres_stats(&ds);
printf("dyn_steps=%llu asked=%d\n", (unsigned long long)ds.steps, nsteps);
check(ds.steps == (uint64_t)nsteps && ds.level == scale, "every step applied");
printf("dyn_allocs hr=%llu wide=%llu\n", (unsigned long long)ds.hr_reallocs,
(unsigned long long)ds.wide_reallocs);
if (dyn_reuse_storage())
check(ds.hr_reallocs == 0 && ds.wide_reallocs == 0,
"ceiling storage reused at every level");
if (threaded) {
GlRthCosts co;
gl_renderer_render_thread_costs(&co);
printf("rth_cost_frames=%llu gpu_frames=%llu dropped=%llu mean_ms=%.3f\n",
(unsigned long long)co.frames, (unsigned long long)co.gpu_frames,
(unsigned long long)co.dropped,
co.frames ? (double)co.cost_ns / (double)co.frames * 1e-6 : 0.0);
check(co.frames > 0, "render-thread frame costs measured");
}
}
if (timing_idle && threaded) {
gl_renderer_render_thread_sync("idle timing check");
GlRthCosts co;
gl_renderer_render_thread_costs(&co);
double gpu_ms = co.gpu_frames ? (double)co.gpu_ns / co.gpu_frames * 1e-6 : 0.0;
printf("idle_gpu_ms=%.3f gpu_frames=%llu\n", gpu_ms, (unsigned long long)co.gpu_frames);
check(co.gpu_frames > 0, "idle timing has completed GPU samples");
check(co.present_wait_ns > 0, "composer wait published independently of render cost");
if (rthf_active_segments())
check(gpu_ms < 15.0, "GPU work excludes the 30ms producer wait");
}
/* 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)");
}
if (drawable_events_enabled())
check(s_drawable_queries == drawable_queries_before, "no per-frame drawable queries");
/* Pixel-size events publish resizes without querying the window on a
* worker or taking its property lock on every frame boundary. */
for (int k = 0; k < 2; ++k) {
SDL_SetWindowSize(win, 400 + k * 80, 300 + k * 60);
SDL_PumpEvents();
gl_renderer_render_thread_frame_boundary();
int expected[2] = {0}, seen[2] = {0};
SDL_GL_GetDrawableSize(win, &expected[0], &expected[1]);
SDL_Thread *worker = SDL_CreateThread(drawable_worker, "drawable-test", seen);
check(worker != NULL, "drawable reader thread created");
if (worker) SDL_WaitThread(worker, NULL);
check(seen[0] == expected[0] && seen[1] == expected[1], "worker sees resized drawable");
}
gl_renderer_render_thread_sync("test_done");
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;
}